| // Copyright 2021 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 crate::keys; |
| use crate::keys::{get_accent, Accent, Key, SpecialKey}; |
| use crate::proxy_view_assistant::ProxyMessages; |
| use anyhow::Error; |
| use carnelian::{ |
| color::Color, |
| drawing::{measure_text_width, FontFace}, |
| input::{self}, |
| make_message, |
| render::Context as RenderContext, |
| scene::{ |
| facets::{ |
| FacetId, SetColorMessage, SetTextMessage, TextFacetOptions, TextHorizontalAlignment, |
| TextVerticalAlignment, |
| }, |
| layout::{ |
| Alignment, CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize, |
| Stack, StackOptions, |
| }, |
| scene::{Scene, SceneBuilder}, |
| }, |
| AppSender, Coord, Message, MessageTarget, Point, Size, ViewAssistant, ViewAssistantContext, |
| ViewKey, |
| }; |
| use euclid::{size2, Size2D}; |
| use fuchsia_zircon::{Duration, Event, Time}; |
| use std::collections::VecDeque; |
| use std::hash::{Hash, Hasher}; |
| |
| /// enum that defines all messages sent with `App::queue_message` that |
| /// the button view assistant will understand and process. |
| pub enum ButtonMessages { |
| Pressed(&'static keys::Key, Time, String), |
| } |
| |
| #[allow(unused)] |
| pub enum KeyboardMessages { |
| NoInput, |
| // Result of input field name and entered text |
| Result(&'static str, String), |
| } |
| |
| #[allow(unused)] |
| struct KeyButton { |
| pub font_size: f32, |
| pub padding: f32, |
| bg_color: Color, |
| bg_color_active: Color, |
| bg_color_disabled: Color, |
| bg_color_special: Color, |
| fg_color: Color, |
| fg_color_disabled: Color, |
| tracking_pointer: Option<input::pointer::PointerId>, |
| active: bool, |
| focused: bool, |
| // Used for shifted and accent keys |
| special_action: bool, |
| key: &'static keys::Key, |
| label_text: String, |
| font_face: FontFace, |
| background: FacetId, |
| label: FacetId, |
| } |
| |
| impl KeyButton { |
| pub fn new( |
| key: &'static keys::Key, |
| font_face: FontFace, |
| font_size: f32, |
| padding: f32, |
| builder: &mut SceneBuilder, |
| ) -> Result<KeyButton, Error> { |
| let options = StackOptions { alignment: Alignment::center(), ..StackOptions::default() }; |
| builder.start_group("key_button", Stack::with_options_ptr(options)); |
| let text = match key { |
| Key::Letter(letter_key) => &letter_key.lower, |
| Key::Special(_special_key, text) => text, |
| }; |
| let label = builder.text( |
| font_face.clone(), |
| text, |
| font_size, |
| Point::zero(), |
| TextFacetOptions { |
| color: Color::white(), |
| horizontal_alignment: TextHorizontalAlignment::Left, |
| vertical_alignment: TextVerticalAlignment::Top, |
| ..TextFacetOptions::default() |
| }, |
| ); |
| let bg_color = Color::from_hash_code("#B7410E")?; |
| let bg_size = match key { |
| Key::Letter(_letter_key) => size2(font_size + padding, font_size + padding), |
| Key::Special(_special_key, text) => { |
| let label_width = measure_text_width(&font_face, font_size, text); |
| size2(label_width + padding * 1.5, font_size + padding) |
| } |
| }; |
| let corner: Coord = Coord::from(5.0); |
| let background = builder.rounded_rectangle(bg_size, corner, bg_color); |
| builder.end_group(); |
| let button = KeyButton { |
| key, |
| font_size, |
| padding, |
| fg_color: Color::white(), |
| bg_color, |
| bg_color_active: Color::from_hash_code("#f0703c")?, |
| fg_color_disabled: Color::from_hash_code("#A0A0A0")?, |
| bg_color_disabled: Color::from_hash_code("#C0C0C0")?, |
| bg_color_special: Color::green(), |
| tracking_pointer: None, |
| active: false, |
| focused: false, |
| special_action: false, |
| label_text: text.to_string(), |
| font_face, |
| background, |
| label, |
| }; |
| |
| Ok(button) |
| } |
| |
| fn update_button_bg_color(&mut self, scene: &mut Scene) { |
| let (label_color, color) = if self.focused { |
| if self.special_action { |
| (self.fg_color, self.bg_color_special) |
| } else if self.active { |
| (self.fg_color, self.bg_color_active) |
| } else { |
| (self.fg_color, self.bg_color) |
| } |
| } else { |
| (self.fg_color_disabled, self.bg_color_disabled) |
| }; |
| scene.send_message(&self.background, Box::new(SetColorMessage { color })); |
| scene.send_message(&self.label, Box::new(SetColorMessage { color: label_color })); |
| } |
| |
| fn set_label(&mut self, scene: &mut Scene, shift: bool, alt: bool, _symbol: bool) { |
| if let keys::Key::Letter(key) = self.key { |
| self.label_text = if shift { |
| key.upper |
| } else if alt { |
| key.alt |
| } else { |
| key.lower |
| } |
| .to_string(); |
| scene.send_message( |
| &self.label, |
| Box::new(SetTextMessage { text: self.label_text.clone() }), |
| ); |
| } |
| } |
| |
| pub fn set_accented_char( |
| &mut self, |
| scene: &mut Scene, |
| accent_char: &Accent, |
| state_shift: bool, |
| ) { |
| self.label_text = |
| if state_shift { accent_char.upper } else { accent_char.lower }.to_string(); |
| scene.send_message(&self.label, Box::new(SetTextMessage { text: self.label_text.clone() })); |
| } |
| |
| pub fn set_special_action(&mut self, scene: &mut Scene, special_action: bool) { |
| if self.special_action != special_action { |
| self.special_action = special_action; |
| self.update_button_bg_color(scene); |
| } |
| } |
| |
| fn set_active(&mut self, scene: &mut Scene, active: bool) { |
| if self.active != active { |
| self.active = active; |
| self.update_button_bg_color(scene); |
| } |
| } |
| |
| pub fn set_focused(&mut self, scene: &mut Scene, focused: bool) { |
| if focused != self.focused { |
| self.focused = focused; |
| self.active = false; |
| self.update_button_bg_color(scene); |
| if !focused { |
| self.tracking_pointer = None; |
| } |
| } |
| } |
| |
| pub fn handle_pointer_event( |
| &mut self, |
| scene: &mut Scene, |
| context: &mut ViewAssistantContext, |
| pointer_event: &input::pointer::Event, |
| ) { |
| if !self.focused { |
| return; |
| } |
| let bounds = scene.get_facet_bounds(&self.background); |
| |
| if self.tracking_pointer.is_none() { |
| match pointer_event.phase { |
| input::pointer::Phase::Down(location) => { |
| self.set_active(scene, 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.set_active(scene, bounds.contains(location.to_f32())); |
| } |
| input::pointer::Phase::Up => { |
| if self.active { |
| context.queue_message(make_message(ButtonMessages::Pressed( |
| self.key, |
| Time::get_monotonic(), |
| self.label_text.clone(), |
| ))); |
| } |
| self.tracking_pointer = None; |
| self.set_active(scene, false); |
| } |
| input::pointer::Phase::Remove => { |
| self.set_active(scene, false); |
| self.tracking_pointer = None; |
| } |
| input::pointer::Phase::Cancel => { |
| self.set_active(scene, false); |
| self.tracking_pointer = None; |
| } |
| _ => (), |
| } |
| } |
| } |
| } |
| } |
| |
| impl PartialEq for KeyButton { |
| fn eq(&self, other: &Self) -> bool { |
| self.label_text == other.label_text |
| } |
| } |
| |
| impl Eq for KeyButton {} |
| |
| impl Hash for KeyButton { |
| fn hash<H: Hasher>(&self, state: &mut H) { |
| self.label_text.hash(state); |
| } |
| } |
| |
| #[allow(unused)] |
| pub struct SceneDetails { |
| buttons: VecDeque<KeyButton>, |
| user_text: FacetId, |
| pub(crate) scene: Scene, |
| } |
| |
| pub struct KeyboardViewAssistant { |
| font_face: FontFace, |
| app_sender: AppSender, |
| view_key: ViewKey, |
| focused: bool, |
| bg_color: Color, |
| user_text: String, |
| field_name: &'static str, |
| scene_details: Option<SceneDetails>, |
| state_shift: bool, |
| shift_time: Time, |
| sticky_shift: bool, |
| accent_key: Option<&'static Key>, |
| state_alt: bool, |
| state_symbols: bool, |
| } |
| |
| impl KeyboardViewAssistant { |
| pub fn new( |
| app_sender: AppSender, |
| view_key: ViewKey, |
| font_face: FontFace, |
| ) -> Result<KeyboardViewAssistant, Error> { |
| let bg_color = Color::from_hash_code("#EBD5B3")?; |
| Ok(KeyboardViewAssistant { |
| font_face, |
| app_sender: app_sender.clone(), |
| view_key, |
| focused: false, |
| bg_color, |
| user_text: String::new(), |
| field_name: "", |
| scene_details: None, |
| state_shift: false, |
| shift_time: Time::ZERO, |
| sticky_shift: false, |
| state_alt: false, |
| accent_key: None, |
| state_symbols: false, |
| }) |
| } |
| |
| #[allow(unused)] |
| pub(crate) fn set_field_name(&mut self, field_name: &'static str) { |
| self.field_name = field_name; |
| } |
| |
| #[allow(unused)] |
| pub(crate) fn set_text_field(&mut self, text: String) { |
| self.user_text = text; |
| } |
| |
| fn remove_last_char(&mut self) { |
| let mut chars = self.user_text.chars(); |
| chars.next_back(); |
| self.user_text = chars.as_str().to_string(); |
| } |
| |
| fn key_press(&mut self, key: &&'static keys::Key, key_cap: &String, time: &Time) { |
| let mut keyboard_changed = false; |
| match key { |
| Key::Letter(letter_key) => { |
| // The default is to use the normal key cap character |
| // Save it here, we may change it later. |
| let mut text = key_cap.as_str(); |
| if let Some(alt_key) = self.accent_key { |
| // An accent has previously been selected |
| // Are we selecting the same or another accent? |
| if self.state_alt && letter_key.is_alt_accent { |
| if self.accent_key == Some(key) { |
| // We want the accent as a character, |
| // Just leave the alt accent state |
| self.accent_key = None; |
| } else { |
| // We want a different accent character |
| self.accent_key = Some(key); |
| } |
| // The accent key will be added (again) later |
| self.remove_last_char(); |
| self.state_alt = false; |
| keyboard_changed = true; |
| } else { |
| self.accent_key = None; |
| // Remove the accent from the input line. |
| self.remove_last_char(); |
| // Now we check for a valid accented character otherwise use the normal key cap letter |
| if let Some(accent) = get_accent(alt_key, key) { |
| // add the accented character |
| if self.state_shift { |
| text = accent.upper; |
| } else { |
| text = accent.lower; |
| } |
| } |
| } |
| } else { |
| if self.state_alt && letter_key.is_alt_accent { |
| self.accent_key = Some(*key); |
| self.state_alt = false; |
| keyboard_changed = true; |
| } |
| } |
| if !self.sticky_shift { |
| self.state_shift = false; |
| keyboard_changed = true; |
| } |
| self.user_text.push_str(text); |
| } |
| Key::Special(key, _text) => match key { |
| SpecialKey::DEL => { |
| let mut chars = self.user_text.chars(); |
| chars.next_back(); |
| self.user_text = chars.into_iter().collect(); |
| if self.accent_key.is_some() { |
| self.accent_key = None; |
| keyboard_changed = true; |
| } |
| } |
| SpecialKey::ENTER => { |
| // Finish this view and return to the calling view |
| self.app_sender.queue_message( |
| MessageTarget::View(self.view_key), |
| make_message(ProxyMessages::PopViewAssistant), |
| ); |
| // Send the calling view the result |
| self.app_sender.queue_message( |
| MessageTarget::View(self.view_key), |
| make_message(KeyboardMessages::Result( |
| self.field_name, |
| self.user_text.clone(), |
| )), |
| ); |
| } |
| SpecialKey::ALT => { |
| self.state_alt = !self.state_alt; |
| if self.state_alt { |
| self.state_shift = false; |
| self.accent_key = None; |
| } |
| keyboard_changed = true; |
| } |
| SpecialKey::SHIFT => { |
| if !self.state_shift { |
| self.shift_time = time.clone(); |
| self.state_shift = true; |
| self.state_alt = false; |
| } else { |
| // Is this a quick double press? |
| if *time - self.shift_time < Duration::from_seconds(1) { |
| self.sticky_shift = true; |
| } else { |
| self.sticky_shift = false; |
| self.state_shift = false; |
| } |
| } |
| keyboard_changed = true; |
| } |
| SpecialKey::SPACE => { |
| self.user_text.push_str(" "); |
| } |
| }, |
| }; |
| if keyboard_changed { |
| if let Some(scene_details) = &mut self.scene_details { |
| for button in &mut scene_details.buttons { |
| button.set_label( |
| &mut scene_details.scene, |
| self.state_shift, |
| self.state_alt, |
| self.state_symbols, |
| ); |
| |
| if let Key::Special(SpecialKey::SHIFT, _) = button.key { |
| button.set_special_action(&mut scene_details.scene, self.state_shift) |
| } |
| if let Key::Special(SpecialKey::ALT, _) = button.key { |
| button.set_special_action(&mut scene_details.scene, self.state_alt); |
| } |
| |
| if let Key::Letter(letter_key) = button.key { |
| // Reset background first |
| button.set_special_action(&mut scene_details.scene, false); |
| // Highlight accent keys if necessary |
| if letter_key.is_alt_accent { |
| button.set_special_action(&mut scene_details.scene, self.state_alt); |
| } |
| |
| // Highlight possible keys that will go with the selected accent character |
| if let Some(accent_key) = self.accent_key { |
| if let Some(accent_char) = get_accent(accent_key, button.key) { |
| button.set_accented_char( |
| &mut scene_details.scene, |
| accent_char, |
| self.state_shift, |
| ); |
| button.set_special_action(&mut scene_details.scene, true); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if let Some(scene_details) = self.scene_details.as_mut() { |
| scene_details.scene.send_message( |
| &scene_details.user_text, |
| Box::new(SetTextMessage { text: self.user_text.clone() }), |
| ); |
| } |
| } |
| |
| pub fn keyboard_scene(&mut self, context: &ViewAssistantContext) -> SceneDetails { |
| let scene_details = self.scene_details.take().unwrap_or_else(|| { |
| let target_size = context.size; |
| let min_dimension = target_size.width.min(target_size.height); |
| let font_size = (min_dimension / 5.0).ceil().min(40.0); |
| let padding = (min_dimension / 20.0).ceil().max(8.0); |
| let mut builder = SceneBuilder::new().background_color(self.bg_color); |
| let mut user_text = None; |
| let mut buttons: VecDeque<KeyButton> = VecDeque::with_capacity(50); |
| builder |
| .group() |
| .column() |
| .max_size() |
| .main_align(MainAxisAlignment::SpaceEvenly) |
| .contents(|builder| { |
| builder.start_group( |
| "text_row", |
| Flex::with_options_ptr(FlexOptions::row( |
| MainAxisSize::Max, |
| MainAxisAlignment::Start, |
| CrossAxisAlignment::End, |
| )), |
| ); |
| builder.space(Size2D { width: 10.0, height: 10.0, _unit: Default::default() }); |
| builder.text( |
| self.font_face.clone(), |
| &format!("Enter {}: ", self.field_name), |
| 35.0, |
| Point::zero(), |
| TextFacetOptions { |
| horizontal_alignment: TextHorizontalAlignment::Left, |
| vertical_alignment: TextVerticalAlignment::Top, |
| color: Color::new(), |
| ..TextFacetOptions::default() |
| }, |
| ); |
| user_text = Some(builder.text( |
| self.font_face.clone(), |
| &format!("{}", self.user_text), |
| 35.0, |
| Point::zero(), |
| TextFacetOptions { |
| horizontal_alignment: TextHorizontalAlignment::Left, |
| vertical_alignment: TextVerticalAlignment::Bottom, |
| color: Color::new(), |
| ..TextFacetOptions::default() |
| }, |
| )); |
| builder.end_group(); |
| |
| for row in keys::KEYBOARD { |
| builder.start_group( |
| "row", |
| Flex::with_options_ptr(FlexOptions::row( |
| MainAxisSize::Max, |
| MainAxisAlignment::SpaceEvenly, |
| CrossAxisAlignment::Center, |
| )), |
| ); |
| for key in *row { |
| let button = KeyButton::new( |
| key, |
| self.font_face.clone(), |
| font_size, |
| padding, |
| builder, |
| ) |
| .expect("KeyButton"); |
| buttons.push_back(button); |
| } |
| builder.end_group(); |
| } |
| }); |
| let mut scene = builder.build(); |
| scene.layout(target_size); |
| for button in buttons.iter_mut() { |
| button.set_focused(&mut scene, self.focused); |
| } |
| SceneDetails { scene, user_text: user_text.expect("user_text"), buttons } |
| }); |
| scene_details |
| } |
| } |
| |
| impl ViewAssistant for KeyboardViewAssistant { |
| fn resize(&mut self, _new_size: &Size) -> Result<(), Error> { |
| self.scene_details = None; |
| Ok(()) |
| } |
| |
| fn render( |
| &mut self, |
| render_context: &mut RenderContext, |
| ready_event: Event, |
| context: &ViewAssistantContext, |
| ) -> Result<(), Error> { |
| let mut scene_details = self.keyboard_scene(context); |
| |
| scene_details.scene.render(render_context, ready_event, context)?; |
| self.scene_details = Some(scene_details); |
| context.request_render(); |
| Ok(()) |
| } |
| |
| fn handle_message(&mut self, message: Message) { |
| if let Some(button_message) = message.downcast_ref::<ButtonMessages>() { |
| match button_message { |
| ButtonMessages::Pressed(key, time, letter) => { |
| self.key_press(key, letter, time); |
| } |
| } |
| } |
| } |
| |
| fn handle_pointer_event( |
| &mut self, |
| context: &mut ViewAssistantContext, |
| _event: &input::Event, |
| pointer_event: &input::pointer::Event, |
| ) -> Result<(), Error> { |
| if let Some(scene_details) = self.scene_details.as_mut() { |
| for button in scene_details.buttons.iter_mut() { |
| button.handle_pointer_event(&mut scene_details.scene, context, &pointer_event); |
| } |
| context.request_render(); |
| } |
| Ok(()) |
| } |
| |
| fn handle_focus_event( |
| &mut self, |
| context: &mut ViewAssistantContext, |
| focused: bool, |
| ) -> Result<(), Error> { |
| self.focused = focused; |
| if let Some(scene_details) = self.scene_details.as_mut() { |
| for button in scene_details.buttons.iter_mut() { |
| button.set_focused(&mut scene_details.scene, focused); |
| } |
| } |
| context.request_render(); |
| Ok(()) |
| } |
| } |