| // 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::button::ButtonMessages; |
| use crate::constants::constants::{ |
| ACTIVE_BUTTON_COLOR, BACKGROUND_GREY, BACKGROUND_WHITE, BUTTON_COLOR, KEY_SPECIAL_COLOR, |
| TEXT_COLOR, TEXT_FIELD_FONT_SIZE, |
| }; |
| use crate::keys::{get_accent, Accent, Key, SpecialKey, KEYBOARD}; |
| #[cfg(not(feature = "ota_ui"))] |
| use crate::proxy_view_assistant::ProxyMessages; |
| use crate::text_field::{SceneBuilderTextFieldExt, TextField, TextFieldOptions, TextVisibility}; |
| use anyhow::Error; |
| use carnelian::color::Color; |
| use carnelian::drawing::{measure_text_width, FontFace}; |
| use carnelian::input::{self}; |
| use carnelian::render::Context as RenderContext; |
| use carnelian::scene::facets::{ |
| FacetId, SetColorMessage, SetTextMessage, TextFacetOptions, TextHorizontalAlignment, |
| TextVerticalAlignment, |
| }; |
| use carnelian::scene::layout::{ |
| Alignment, CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize, Stack, |
| StackOptions, |
| }; |
| use carnelian::scene::scene::{Scene, SceneBuilder}; |
| use carnelian::{ |
| make_message, AppSender, Coord, Message, MessageTarget, Point, Size, ViewAssistant, |
| ViewAssistantContext, ViewKey, |
| }; |
| use euclid::{size2, Size2D}; |
| #[cfg(feature = "ota_ui")] |
| use recovery_util::ota::state_machine::Event as StateMachineEvent; |
| use std::collections::VecDeque; |
| use std::hash::{Hash, Hasher}; |
| use zx::{Event, MonotonicDuration, MonotonicInstant}; |
| |
| type FieldName = String; |
| type InputText = String; |
| |
| // TODO(b/259497403): Calculate hardcoded values from screen width |
| const TEXT_FIELD_WIDTH: f32 = 960.0; |
| |
| /// enum that defines all messages sent with `App::queue_message` that |
| /// the button view assistant will understand and process. |
| pub enum KeyMessages { |
| Pressed(&'static Key, MonotonicInstant, String), |
| } |
| |
| #[allow(unused)] |
| pub enum KeyboardMessages { |
| NoInput, |
| Result(FieldName, InputText), |
| } |
| |
| struct KeyButton { |
| 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 Key, |
| label_text: String, |
| background: FacetId, |
| label: FacetId, |
| } |
| |
| impl KeyButton { |
| pub fn new( |
| key: &'static 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: TEXT_COLOR, |
| horizontal_alignment: TextHorizontalAlignment::Left, |
| vertical_alignment: TextVerticalAlignment::Top, |
| ..TextFacetOptions::default() |
| }, |
| ); |
| 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_f32); |
| let background = builder.rounded_rectangle(bg_size, corner, BUTTON_COLOR); |
| builder.end_group(); |
| let button = KeyButton { |
| key, |
| fg_color: TEXT_COLOR, |
| bg_color: BACKGROUND_GREY, |
| bg_color_active: ACTIVE_BUTTON_COLOR, |
| fg_color_disabled: TEXT_COLOR, |
| bg_color_disabled: BACKGROUND_GREY, |
| bg_color_special: KEY_SPECIAL_COLOR, |
| tracking_pointer: None, |
| active: false, |
| focused: false, |
| special_action: false, |
| label_text: text.to_string(), |
| 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 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(KeyMessages::Pressed( |
| self.key, |
| MonotonicInstant::get(), |
| 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); |
| } |
| } |
| |
| pub struct SceneDetails { |
| text_field: TextField, |
| buttons: VecDeque<KeyButton>, |
| pub(crate) scene: Scene, |
| } |
| |
| pub struct KeyboardViewAssistant { |
| font_face: FontFace, |
| app_sender: AppSender, |
| view_key: ViewKey, |
| focused: bool, |
| field_name: FieldName, |
| user_text: InputText, |
| privacy: TextVisibility, |
| scene_details: Option<SceneDetails>, |
| state_shift: bool, |
| shift_time: MonotonicInstant, |
| 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> { |
| Ok(KeyboardViewAssistant { |
| font_face, |
| app_sender: app_sender, |
| view_key, |
| focused: false, |
| field_name: String::new(), |
| user_text: String::new(), |
| privacy: TextVisibility::Always, |
| scene_details: None, |
| state_shift: false, |
| shift_time: MonotonicInstant::ZERO, |
| sticky_shift: false, |
| state_alt: false, |
| accent_key: None, |
| state_symbols: false, |
| }) |
| } |
| |
| pub fn set_field_name(&mut self, field_name: FieldName) { |
| self.field_name = field_name.clone(); |
| if let Some(scene_details) = &mut self.scene_details { |
| scene_details.text_field.set_title(field_name); |
| } |
| } |
| |
| pub fn set_text_field(&mut self, text: InputText) { |
| self.user_text = text.clone(); |
| if let Some(scene_details) = &mut self.scene_details { |
| scene_details.text_field.set_text(text); |
| } |
| } |
| |
| pub fn set_privacy(&mut self, privacy: TextVisibility) { |
| self.privacy = privacy; |
| if let Some(scene_details) = &mut self.scene_details { |
| scene_details.text_field.set_privacy(privacy); |
| } |
| } |
| |
| 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 Key, key_cap: &String, time: &MonotonicInstant) { |
| 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 => { |
| #[cfg(feature = "ota_ui")] |
| self.app_sender.queue_message( |
| MessageTarget::View(self.view_key), |
| make_message(StateMachineEvent::UserInput(self.user_text.clone())), |
| ); |
| // Finish this view and return to the calling view |
| #[cfg(not(feature = "ota_ui"))] |
| self.app_sender.queue_message( |
| MessageTarget::View(self.view_key), |
| make_message(ProxyMessages::PopViewAssistant), |
| ); |
| // Send the calling view the result |
| #[cfg(not(feature = "ota_ui"))] |
| self.app_sender.queue_message( |
| MessageTarget::View(self.view_key), |
| make_message(KeyboardMessages::Result( |
| self.field_name.clone(), |
| 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 < MonotonicDuration::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.text_field.update_text(&mut scene_details.scene, 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(BACKGROUND_WHITE); |
| let mut text_field: Option<TextField> = 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_field_row", |
| Flex::with_options_ptr(FlexOptions::row( |
| MainAxisSize::Max, |
| MainAxisAlignment::Start, |
| CrossAxisAlignment::End, |
| )), |
| ); |
| // TODO(b/259497403): Calculate hardcoded values from screen width |
| builder.space(Size2D { width: 20.0, height: 10.0, _unit: Default::default() }); |
| text_field = Some(builder.text_field( |
| self.field_name.to_string(), |
| self.user_text.to_string(), |
| self.privacy, |
| size2(TEXT_FIELD_WIDTH, TEXT_FIELD_FONT_SIZE * 3.0), |
| TextFieldOptions { |
| text_size: TEXT_FIELD_FONT_SIZE, |
| ..TextFieldOptions::default() |
| }, |
| )); |
| |
| builder.end_group(); // text_field_row |
| |
| for row in 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); |
| } |
| let mut text_field = text_field.expect("text field should have been set properly"); |
| text_field.set_focused(&mut scene, self.focused); |
| SceneDetails { scene, text_field, 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(key_message) = message.downcast_ref::<KeyMessages>() { |
| match key_message { |
| KeyMessages::Pressed(key, time, letter) => { |
| self.key_press(key, letter, time); |
| } |
| } |
| } |
| if let Some(button_message) = message.downcast_ref::<ButtonMessages>() { |
| match button_message { |
| ButtonMessages::Pressed(_time, button_text) if button_text == &self.field_name => { |
| self.privacy = self.privacy.toggle(); |
| self.scene_details = None; |
| } |
| &_ => {} |
| } |
| } |
| } |
| |
| 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); |
| } |
| scene_details.text_field.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); |
| } |
| scene_details.text_field.set_focused(&mut scene_details.scene, focused); |
| } |
| context.request_render(); |
| Ok(()) |
| } |
| } |