blob: 12304da133f20d2c40754304e24386ab9358adc3 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This button sample implements a rotation scheme that is a prototype
// for how Carnelian and apps will deal with drawing to the frame buffer
// on devices where the screen is mounted rotated relative to the viewing
// angle.
use anyhow::Error;
use carnelian::{
color::Color,
drawing::{
make_font_description, path_for_rectangle, DisplayAligned, DisplayRotation, GlyphMap,
Paint, Text,
},
input::{self},
make_app_assistant, make_message,
render::{
BlendMode, Composition, Context as RenderContext, Fill, FillRule, Layer, PreClear, Raster,
RenderExt, Style,
},
App, AppAssistant, Coord, IntPoint, Message, Point, Rect, Size, ViewAssistant,
ViewAssistantContext, ViewAssistantPtr, ViewKey, ViewMessages,
};
use euclid::Transform2D;
use fuchsia_zircon::{AsHandleRef, ClockId, Event, Signals, Time};
/// enum that defines all messages sent with `App::queue_message` that
/// the button view assistant will understand and process.
pub enum ButtonMessages {
Pressed(Time),
}
#[derive(Default)]
struct ButtonAppAssistant;
impl AppAssistant for ButtonAppAssistant {
fn setup(&mut self) -> Result<(), Error> {
Ok(())
}
fn create_view_assistant(&mut self, _: ViewKey) -> Result<ViewAssistantPtr, Error> {
Ok(Box::new(ButtonViewAssistant::new()?))
}
}
fn raster_for_rectangle(
bounds: &Rect,
display_rotation: &DisplayRotation,
render_context: &mut RenderContext,
) -> Raster {
let rotation_transform =
display_rotation.rotation().and_then(|transform| Some(transform.to_untyped()));
let mut raster_builder = render_context.raster_builder().expect("raster_builder");
raster_builder.add(&path_for_rectangle(bounds, render_context), rotation_transform.as_ref());
raster_builder.build()
}
struct RasterAndStyle {
location: Point,
raster: Raster,
style: Style,
}
struct Button {
display_rotation: DisplayRotation,
pub font_size: u32,
pub padding: f32,
bounds: Rect,
bg_color: Color,
bg_color_active: Color,
bg_color_disabled: Color,
fg_color: Color,
fg_color_disabled: Color,
tracking_pointer: Option<input::pointer::PointerId>,
active: bool,
focused: bool,
glyphs: GlyphMap,
label_text: String,
label: Option<Text>,
}
impl Button {
pub fn new(text: &str, display_rotation: DisplayRotation) -> Result<Button, Error> {
let button = Button {
display_rotation,
font_size: 20,
padding: 5.0,
bounds: Rect::zero(),
fg_color: Color::white(),
bg_color: Color::from_hash_code("#B7410E")?,
bg_color_active: Color::from_hash_code("#f0703c")?,
fg_color_disabled: Color::from_hash_code("#A0A0A0")?,
bg_color_disabled: Color::from_hash_code("#C0C0C0")?,
tracking_pointer: None,
active: false,
focused: false,
glyphs: GlyphMap::new_with_rotation(display_rotation),
label_text: text.to_string(),
label: None,
};
Ok(button)
}
pub fn set_focused(&mut self, focused: bool) {
self.focused = focused;
if !focused {
self.active = false;
self.tracking_pointer = None;
}
}
fn create_rasters_and_styles(
&mut self,
render_context: &mut RenderContext,
) -> Result<(RasterAndStyle, RasterAndStyle), Error> {
// set up paint with different backgrounds depending on whether the button
// is active. The active state is true when a pointer has gone down in the
// button's bounds and the pointer has not moved outside the bounds since.
let paint = if self.focused {
Paint {
fg: self.fg_color,
bg: if self.active { self.bg_color_active } else { self.bg_color },
}
} else {
Paint { fg: self.fg_color_disabled, bg: self.bg_color_disabled }
};
let font_description = make_font_description(self.font_size, 0);
self.label = Some(Text::new(
render_context,
&self.label_text,
self.font_size as f32,
100,
font_description.face,
&mut self.glyphs,
));
let label = self.label.as_ref().expect("label");
// calculate button size based on label's text size
// plus padding.
let bounding_box_size = label.bounding_box.size;
let button_label_size = Size::new(bounding_box_size.width, self.font_size as f32);
let double_padding = 2.0 * self.padding;
let button_size = button_label_size + Size::new(double_padding, double_padding);
let half_size = Size::new(button_size.width * 0.5, button_size.height * 0.5);
let button_origin = Point::zero() - half_size.to_vector();
let button_bounds = Rect::new(button_origin, button_size).round_out();
// record bounds for hit testing
self.bounds = button_bounds;
// Calculate the label offset in display aligned coordinates, since the label,
// as a raster, is pre-rotated and we just need to translate it to align with the buttons
// bounding box.
let center = self.bounds.center();
let label_center = label.bounding_box.center().to_vector();
let label_offset = center - label_center;
let raster = raster_for_rectangle(&self.bounds, &self.display_rotation, render_context);
let button_raster_and_style = RasterAndStyle {
location: Point::zero(),
raster,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(paint.bg),
blend_mode: BlendMode::Over,
},
};
let label_raster_and_style = RasterAndStyle {
location: label_offset,
raster: label.raster.clone(),
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(paint.fg),
blend_mode: BlendMode::Over,
},
};
Ok((button_raster_and_style, label_raster_and_style))
}
pub fn handle_pointer_event(
&mut self,
context: &mut ViewAssistantContext,
pointer_event: &input::pointer::Event,
) {
if !self.focused {
return;
}
let bounds = self.bounds;
if self.tracking_pointer.is_none() {
match pointer_event.phase {
input::pointer::Phase::Down(location) => {
self.active = bounds.contains(location.to_f32());
if self.active {
self.tracking_pointer = Some(pointer_event.pointer_id.clone());
}
}
_ => (),
}
} else {
let tracking_pointer = self.tracking_pointer.as_ref().expect("tracking_pointer");
if tracking_pointer == &pointer_event.pointer_id {
match pointer_event.phase {
input::pointer::Phase::Moved(location) => {
self.active = bounds.contains(location.to_f32());
}
input::pointer::Phase::Up => {
if self.active {
context.queue_message(make_message(ButtonMessages::Pressed(
Time::get(ClockId::Monotonic),
)));
}
self.tracking_pointer = None;
self.active = false;
}
input::pointer::Phase::Remove => {
self.active = false;
self.tracking_pointer = None;
}
input::pointer::Phase::Cancel => {
self.active = false;
self.tracking_pointer = None;
}
_ => (),
}
}
}
}
}
struct ButtonViewAssistant {
focused: bool,
bg_color: Color,
button: Button,
display_rotation: DisplayRotation,
red_light: bool,
composition: Composition,
}
const BUTTON_LABEL: &'static str = "Depress Me";
impl ButtonViewAssistant {
fn new() -> Result<ButtonViewAssistant, Error> {
let bg_color = Color::from_hash_code("#EBD5B3")?;
let display_rotation = DisplayRotation::Deg0;
let composition = Composition::new(bg_color);
Ok(ButtonViewAssistant {
focused: false,
bg_color,
display_rotation,
button: Button::new(BUTTON_LABEL, display_rotation)?,
red_light: false,
composition,
})
}
fn rotate(&mut self) -> Result<(), Error> {
self.display_rotation = match self.display_rotation {
DisplayRotation::Deg0 => DisplayRotation::Deg90,
DisplayRotation::Deg90 => DisplayRotation::Deg180,
DisplayRotation::Deg180 => DisplayRotation::Deg270,
DisplayRotation::Deg270 => DisplayRotation::Deg0,
};
self.button = Button::new(BUTTON_LABEL, self.display_rotation)?;
Ok(())
}
fn target_size(&self, size: Size) -> Size {
match self.display_rotation {
DisplayRotation::Deg90 => Size::new(size.height, size.width),
DisplayRotation::Deg180 => Size::new(size.width, size.height),
DisplayRotation::Deg270 => Size::new(size.height, size.width),
DisplayRotation::Deg0 => size,
}
}
fn button_center(&self, size: Size) -> Point {
Point::new(size.width * 0.5, size.height * 0.7)
}
fn transform_pointer_location(
location: IntPoint,
button_center: Point,
transform: &Transform2D<Coord, DisplayAligned, euclid::UnknownUnit>,
) -> IntPoint {
let transformed_location = transform
.transform_point(location.to_f32().cast_unit::<DisplayAligned>())
.cast_unit::<euclid::UnknownUnit>();
(transformed_location - button_center.to_vector()).to_i32()
}
}
impl ViewAssistant for ButtonViewAssistant {
fn render(
&mut self,
render_context: &mut RenderContext,
ready_event: Event,
context: &ViewAssistantContext,
) -> Result<(), Error> {
// Emulate the size that Carnelian passes when the display is rotated
let target_size = self.target_size(context.size);
// Create a presentation to display tranformation
let transform = self.display_rotation.transform(&target_size);
// Calculate all locations in the presentation-aligned coordinate space
let center_x = target_size.width * 0.5;
let min_dimension = target_size.width.min(target_size.height);
let font_size = (min_dimension / 5.0).ceil().min(64.0) as u32;
let padding = (min_dimension / 20.0).ceil().max(8.0);
self.button.padding = padding;
self.button.font_size = font_size;
// Position and size the indicator in presentation space
let indicator_y = target_size.height / 5.0;
let indicator_len = target_size.height.min(target_size.width) / 8.0;
let indicator_size = Size::new(indicator_len * 2.0, indicator_len);
let indicator_pos = Point::new(center_x - indicator_len, indicator_y - indicator_len / 2.0);
// Now transform the indicator location to the display aligned coordinate system
let display_indicator_pos = if let Some(transform) = transform {
transform.transform_point(indicator_pos)
} else {
indicator_pos.cast_unit::<DisplayAligned>()
};
// create a raster for the indicator. raster_for_rectangle will transform it
// based on the display orientation. After, translate it to the correct position.
let indicator_raster = raster_for_rectangle(
&Rect::new(Point::zero(), indicator_size),
&self.display_rotation,
render_context,
)
.translate(display_indicator_pos.to_vector().cast_unit::<euclid::UnknownUnit>().to_i32());
let indicator_color = if self.red_light {
Color::from_hash_code("#ff0000")?
} else {
Color::from_hash_code("#00ff00")?
};
// Create a layer for the indicator using its pre-transformed raster and
// transformed position.
let indicator_layer = Layer {
raster: indicator_raster,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(indicator_color),
blend_mode: BlendMode::Over,
},
};
let button_center = self.button_center(target_size);
self.button.set_focused(self.focused);
// Let the button render itself, returning rasters, styles and zero-relative
// positions.
let (button_raster_and_style, label_raster_and_style) =
self.button.create_rasters_and_styles(render_context)?;
// Calculate the button location in presentation space
let button_location = button_center + button_raster_and_style.location.to_vector();
// Transform it to display space.
let button_location = if let Some(transform) = transform {
transform.transform_point(button_location)
} else {
button_location.cast_unit::<DisplayAligned>()
};
// Calculate the label location in presentation space
let label_location = button_center + label_raster_and_style.location.to_vector();
// Transform it to display space.
let label_location = if let Some(transform) = transform {
transform.transform_point(label_location)
} else {
label_location.cast_unit::<DisplayAligned>()
};
// Create layers from the rasters, styles and transformed locations.
let button_layer = Layer {
raster: button_raster_and_style
.raster
.translate(button_location.to_vector().cast_unit::<euclid::UnknownUnit>().to_i32()),
style: button_raster_and_style.style,
};
let label_layer = Layer {
raster: label_raster_and_style
.raster
.translate(label_location.to_vector().cast_unit::<euclid::UnknownUnit>().to_i32()),
style: label_raster_and_style.style,
};
self.composition.replace(
..,
std::iter::once(label_layer)
.chain(std::iter::once(button_layer))
.chain(std::iter::once(indicator_layer)),
);
let image = render_context.get_current_image(context);
let ext =
RenderExt { pre_clear: Some(PreClear { color: self.bg_color }), ..Default::default() };
render_context.render(&self.composition, None, image, &ext);
ready_event.as_handle_ref().signal(Signals::NONE, Signals::EVENT_SIGNALED)?;
Ok(())
}
fn handle_message(&mut self, message: Message) {
if let Some(button_message) = message.downcast_ref::<ButtonMessages>() {
match button_message {
ButtonMessages::Pressed(value) => {
println!("value = {:#?}", value);
self.red_light = !self.red_light
}
}
}
}
fn handle_pointer_event(
&mut self,
context: &mut ViewAssistantContext,
_event: &input::Event,
pointer_event: &input::pointer::Event,
) -> Result<(), Error> {
let target_size = self.target_size(context.size);
let button_center = self.button_center(target_size);
// create an inverse transform to convert pointer event locations
// into presentation space.
let transform = self
.display_rotation
.transform(&target_size)
.and_then(|transform| transform.inverse())
.unwrap_or(Transform2D::identity());
let translated_pointer_event = match pointer_event.phase {
input::pointer::Phase::Down(location) => input::pointer::Event {
phase: input::pointer::Phase::Down(Self::transform_pointer_location(
location,
button_center,
&transform,
)),
..pointer_event.clone()
},
input::pointer::Phase::Moved(location) => input::pointer::Event {
phase: input::pointer::Phase::Moved(Self::transform_pointer_location(
location,
button_center,
&transform,
)),
..pointer_event.clone()
},
_ => pointer_event.clone(),
};
self.button.handle_pointer_event(context, &translated_pointer_event);
context.queue_message(make_message(ViewMessages::Update));
Ok(())
}
fn handle_focus_event(&mut self, focused: bool) -> Result<(), Error> {
self.focused = focused;
Ok(())
}
fn handle_keyboard_event(
&mut self,
context: &mut ViewAssistantContext,
_event: &input::Event,
keyboard_event: &input::keyboard::Event,
) -> Result<(), Error> {
if let Some(code_point) = keyboard_event.code_point {
if code_point == ' ' as u32 && keyboard_event.phase == input::keyboard::Phase::Pressed {
self.rotate()?;
context.queue_message(make_message(ViewMessages::Update));
}
}
Ok(())
}
}
fn main() -> Result<(), Error> {
App::run(make_app_assistant::<ButtonAppAssistant>())
}