|  | // 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. | 
|  |  | 
|  | use anyhow::Error; | 
|  | use argh::FromArgs; | 
|  | use carnelian::{ | 
|  | color::Color, | 
|  | drawing::{ | 
|  | load_font, path_for_corner_knockouts, path_for_rectangle, DisplayRotation, FontFace, | 
|  | 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, Message, Point, Rect, Size, ViewAssistant, ViewAssistantContext, | 
|  | ViewAssistantPtr, ViewKey, | 
|  | }; | 
|  | use euclid::default::Vector2D; | 
|  | use fuchsia_zircon::{AsHandleRef, Event, Signals, Time}; | 
|  | use std::path::PathBuf; | 
|  |  | 
|  | fn display_rotation_from_str(s: &str) -> Result<DisplayRotation, String> { | 
|  | match s { | 
|  | "0" => Ok(DisplayRotation::Deg0), | 
|  | "90" => Ok(DisplayRotation::Deg90), | 
|  | "180" => Ok(DisplayRotation::Deg180), | 
|  | "270" => Ok(DisplayRotation::Deg270), | 
|  | _ => Err(format!("Invalid DisplayRotation {}", s)), | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Button Sample | 
|  | #[derive(Debug, FromArgs)] | 
|  | #[argh(name = "recovery")] | 
|  | struct Args { | 
|  | /// rotate | 
|  | #[argh(option, from_str_fn(display_rotation_from_str))] | 
|  | rotation: Option<DisplayRotation>, | 
|  | } | 
|  |  | 
|  | /// 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 { | 
|  | display_rotation: DisplayRotation, | 
|  | } | 
|  |  | 
|  | impl AppAssistant for ButtonAppAssistant { | 
|  | fn setup(&mut self) -> Result<(), Error> { | 
|  | let args: Args = argh::from_env(); | 
|  | self.display_rotation = args.rotation.unwrap_or(DisplayRotation::Deg0); | 
|  | Ok(()) | 
|  | } | 
|  |  | 
|  | fn create_view_assistant(&mut self, _: ViewKey) -> Result<ViewAssistantPtr, Error> { | 
|  | Ok(Box::new(ButtonViewAssistant::new()?)) | 
|  | } | 
|  |  | 
|  | fn get_display_rotation(&self) -> DisplayRotation { | 
|  | self.display_rotation | 
|  | } | 
|  | } | 
|  |  | 
|  | fn raster_for_rectangle(bounds: &Rect, render_context: &mut RenderContext) -> Raster { | 
|  | let mut raster_builder = render_context.raster_builder().expect("raster_builder"); | 
|  | raster_builder.add(&path_for_rectangle(bounds, render_context), None); | 
|  | raster_builder.build() | 
|  | } | 
|  |  | 
|  | fn raster_for_corner_knockouts( | 
|  | bounds: &Rect, | 
|  | corner_radius: Coord, | 
|  | render_context: &mut RenderContext, | 
|  | ) -> Raster { | 
|  | let path = path_for_corner_knockouts(bounds, corner_radius, render_context); | 
|  | let mut raster_builder = render_context.raster_builder().expect("raster_builder"); | 
|  | raster_builder.add(&path, None); | 
|  | raster_builder.build() | 
|  | } | 
|  |  | 
|  | struct RasterAndStyle { | 
|  | location: Point, | 
|  | raster: Raster, | 
|  | style: Style, | 
|  | } | 
|  |  | 
|  | struct Button { | 
|  | 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, | 
|  | face: FontFace, | 
|  | label: Option<Text>, | 
|  | } | 
|  |  | 
|  | impl Button { | 
|  | pub fn new(text: &str) -> Result<Button, Error> { | 
|  | let face = load_font(PathBuf::from("/pkg/data/fonts/RobotoSlab-Regular.ttf"))?; | 
|  | let button = Button { | 
|  | 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(), | 
|  | label_text: text.to_string(), | 
|  | face, | 
|  | 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 } | 
|  | }; | 
|  |  | 
|  | self.label = Some(Text::new( | 
|  | render_context, | 
|  | &self.label_text, | 
|  | self.font_size as f32, | 
|  | 100, | 
|  | &self.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, 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 | 
|  | .translate(Vector2D::new(context.size.width * 0.5, context.size.height * 0.7)); | 
|  |  | 
|  | 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_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, | 
|  | 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 composition = Composition::new(bg_color); | 
|  | Ok(ButtonViewAssistant { | 
|  | focused: false, | 
|  | bg_color, | 
|  | button: Button::new(BUTTON_LABEL)?, | 
|  | red_light: false, | 
|  | composition, | 
|  | }) | 
|  | } | 
|  |  | 
|  | fn target_size(&self, size: Size) -> Size { | 
|  | size | 
|  | } | 
|  |  | 
|  | fn button_center(&self, size: Size) -> Point { | 
|  | Point::new(size.width * 0.5, size.height * 0.7) | 
|  | } | 
|  | } | 
|  |  | 
|  | 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); | 
|  |  | 
|  | // 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; | 
|  |  | 
|  | let corner_knockouts = | 
|  | raster_for_corner_knockouts(&Rect::from_size(target_size), 10.0, render_context); | 
|  |  | 
|  | let corner_knockouts_layer = Layer { | 
|  | raster: corner_knockouts, | 
|  | style: Style { | 
|  | fill_rule: FillRule::NonZero, | 
|  | fill: Fill::Solid(Color::new()), | 
|  | blend_mode: BlendMode::Over, | 
|  | }, | 
|  | }; | 
|  |  | 
|  | // 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); | 
|  |  | 
|  | let indicator_raster = | 
|  | raster_for_rectangle(&Rect::new(Point::zero(), indicator_size), render_context) | 
|  | .translate(indicator_pos.to_vector().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(); | 
|  |  | 
|  | // Calculate the label location in presentation space | 
|  | let label_location = button_center + label_raster_and_style.location.to_vector(); | 
|  |  | 
|  | // Create layers from the rasters, styles and transformed locations. | 
|  | let button_layer = Layer { | 
|  | raster: button_raster_and_style.raster.translate(button_location.to_vector().to_i32()), | 
|  | style: button_raster_and_style.style, | 
|  | }; | 
|  | let label_layer = Layer { | 
|  | raster: label_raster_and_style.raster.translate(label_location.to_vector().to_i32()), | 
|  | style: label_raster_and_style.style, | 
|  | }; | 
|  | self.composition.replace( | 
|  | .., | 
|  | std::iter::once(corner_knockouts_layer) | 
|  | .chain(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> { | 
|  | self.button.handle_pointer_event(context, &pointer_event); | 
|  | context.request_render(); | 
|  | Ok(()) | 
|  | } | 
|  |  | 
|  | fn handle_focus_event( | 
|  | &mut self, | 
|  | context: &mut ViewAssistantContext, | 
|  | focused: bool, | 
|  | ) -> Result<(), Error> { | 
|  | self.focused = focused; | 
|  | context.request_render(); | 
|  | Ok(()) | 
|  | } | 
|  | } | 
|  |  | 
|  | fn main() -> Result<(), Error> { | 
|  | fuchsia_trace_provider::trace_provider_create_with_fdio(); | 
|  | App::run(make_app_assistant::<ButtonAppAssistant>()) | 
|  | } |