virtual_console_lib/
view.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::app::EventProxy;
6use crate::args::{MAX_FONT_SIZE, MIN_FONT_SIZE};
7use crate::colors::{ColorScheme, DARK_COLOR_SCHEME, LIGHT_COLOR_SCHEME, SPECIAL_COLOR_SCHEME};
8use crate::terminal::Terminal;
9use crate::text_grid::{TextGridFacet, TextGridMessages};
10use anyhow::{Error, anyhow};
11use carnelian::drawing::load_font;
12use carnelian::render::Context as RenderContext;
13use carnelian::render::rive::load_rive;
14use carnelian::scene::facets::{FacetId, RiveFacet};
15use carnelian::scene::scene::{Scene, SceneBuilder, SceneOrder};
16use carnelian::{
17    AppSender, Point, Size, ViewAssistant, ViewAssistantContext, ViewAssistantPtr, ViewKey, input,
18};
19use fidl_fuchsia_hardware_display::VirtconMode;
20use fidl_fuchsia_hardware_power_statecontrol::{
21    AdminMarker, AdminSynchronousProxy, ShutdownAction, ShutdownOptions, ShutdownReason,
22};
23use fidl_fuchsia_hardware_pty::WindowSize;
24use fidl_fuchsia_input_report::ConsumerControlButton;
25use fuchsia_component::client::connect_channel_to_protocol;
26
27use futures::future::{FutureExt as _, join_all};
28use pty::key_util::{CodePoint, HidUsage};
29use std::collections::{BTreeMap, BTreeSet};
30use std::io::Write as _;
31use std::mem;
32use std::path::PathBuf;
33use term_model::ansi::TermInfo;
34use term_model::grid::Scroll;
35use term_model::term::color::Rgb;
36use term_model::term::{SizeInfo, TermMode};
37use terminal::{FontSet, cell_size_from_cell_height, get_scale_factor};
38use {fuchsia_async as fasync, rive_rs as rive};
39
40fn is_control_only(modifiers: &input::Modifiers) -> bool {
41    modifiers.control && !modifiers.shift && !modifiers.alt && !modifiers.caps_lock
42}
43
44fn get_input_sequence_for_key_event(
45    event: &input::keyboard::Event,
46    app_cursor: bool,
47) -> Option<String> {
48    match event.phase {
49        input::keyboard::Phase::Pressed | input::keyboard::Phase::Repeat => {
50            match event.code_point {
51                None => HidUsage { hid_usage: event.hid_usage, app_cursor }.into(),
52                Some(code_point) => CodePoint {
53                    code_point: code_point,
54                    control_pressed: is_control_only(&event.modifiers),
55                }
56                .into(),
57            }
58        }
59        _ => None,
60    }
61}
62
63pub enum ViewMessages {
64    AddTerminalMessage(u32, Terminal<EventProxy>, bool),
65    RequestTerminalUpdateMessage(u32),
66}
67
68// Constraints on status bar tabs.
69const MIN_TAB_WIDTH: usize = 16;
70const MAX_TAB_WIDTH: usize = 32;
71
72// Status bar colors.
73const STATUS_COLOR_DEFAULT: Rgb = Rgb { r: 170, g: 170, b: 170 };
74const STATUS_COLOR_ACTIVE: Rgb = Rgb { r: 255, g: 255, b: 85 };
75const STATUS_COLOR_UPDATED: Rgb = Rgb { r: 85, g: 255, b: 85 };
76
77// Amount of change to font size when zooming.
78const FONT_SIZE_INCREMENT: f32 = 4.0;
79
80// Maximum terminal size in cells. We support up to 4 layers per cell.
81const MAX_CELLS: u32 = SceneOrder::MAX.as_u32() / 4;
82
83struct Animation {
84    // Artboard has weak references to data owned by file.
85    _file: rive::File,
86    artboard: rive::Object<rive::Artboard>,
87    instance: rive::animation::LinearAnimationInstance,
88    last_presentation_time: Option<zx::MonotonicInstant>,
89}
90
91struct SceneDetails {
92    scene: Scene,
93    textgrid: Option<FacetId>,
94}
95
96#[derive(Clone, Copy, Eq, PartialEq)]
97struct TerminalStatus {
98    pub has_output: bool,
99    pub at_top: bool,
100    pub at_bottom: bool,
101}
102
103// Handles and records repeated button presses.
104//
105// Repeated button presses are defined as one or more button presses where
106// the interval between two consecutive events doesn't exceed 2 seconds.
107struct RepeatedButtonPressHandler {
108    // Durations of each button press for repeated button presses.
109    //
110    // `press_durations_ns` and `down_times_ns` are guaranteed to be of the
111    // same size.
112    press_durations_ns: Vec<u64>,
113
114    // Timestamps of the press-down event for each button press for repeated
115    // button presses.
116    down_times_ns: Vec<u64>,
117
118    button: ConsumerControlButton,
119
120    // Timestamp of the most recent button down event waiting for
121    // release.
122    pending_down_time_ns: Option<u64>,
123}
124
125impl RepeatedButtonPressHandler {
126    pub fn new(button: ConsumerControlButton) -> Self {
127        Self {
128            press_durations_ns: vec![],
129            down_times_ns: vec![],
130            button,
131            pending_down_time_ns: None,
132        }
133    }
134
135    // Returns true iff the event can be handled by this handler.
136    pub fn handle_button_event(
137        &mut self,
138        event: &input::consumer_control::Event,
139        time_ns: u64,
140    ) -> bool {
141        if event.button != self.button {
142            return false;
143        }
144
145        if event.phase == carnelian::input::consumer_control::Phase::Down {
146            self.pending_down_time_ns = Some(time_ns);
147            return true;
148        }
149
150        // event.phase == carnelian::input::consumer_control::Phase::Up
151        if let None = self.pending_down_time_ns {
152            return false;
153        }
154        let down_time_ns: u64 =
155            self.pending_down_time_ns.take().expect("pending press down time missing");
156        let up_time_ns: u64 = time_ns;
157
158        if up_time_ns < down_time_ns {
159            return false;
160        }
161        let press_duration_ns: u64 = up_time_ns - down_time_ns;
162
163        let last_down_time_ns: u64 = *self.down_times_ns.last().unwrap_or(&0);
164        if down_time_ns < last_down_time_ns {
165            return false;
166        }
167
168        const REPEATED_PRESS_THRESHOLD_NS: u64 =
169            zx::MonotonicDuration::from_seconds(2).into_nanos() as u64;
170
171        // down_time_ns is guaranteed to be >= last_down_time_ns.
172        if down_time_ns - last_down_time_ns > REPEATED_PRESS_THRESHOLD_NS {
173            self.press_durations_ns = vec![press_duration_ns];
174            self.down_times_ns = vec![down_time_ns];
175        } else {
176            self.press_durations_ns.push(press_duration_ns);
177            self.down_times_ns.push(down_time_ns);
178        }
179
180        true
181    }
182}
183
184pub struct VirtualConsoleViewAssistant {
185    app_sender: AppSender,
186    view_key: ViewKey,
187    color_scheme: ColorScheme,
188    round_scene_corners: bool,
189    font_size: f32,
190    dpi: BTreeSet<u32>,
191    cell_size: Size,
192    tab_width: usize,
193    scene_details: Option<SceneDetails>,
194    terminals: BTreeMap<u32, (Terminal<EventProxy>, TerminalStatus)>,
195    font_set: FontSet,
196    animation: Option<Animation>,
197    active_terminal_id: u32,
198    virtcon_mode: VirtconMode,
199    desired_virtcon_mode: VirtconMode,
200    owns_display: bool,
201    active_pointer_id: Option<input::pointer::PointerId>,
202    start_pointer_location: Point,
203    is_primary: bool,
204    power_button_press_handler: RepeatedButtonPressHandler,
205}
206
207const BOOT_ANIMATION_PATH_1: &'static str = "/pkg/data/boot-animation.riv";
208const BOOT_ANIMATION_PATH_2: &'static str = "/boot/data/boot-animation.riv";
209const FONT: &'static str = "/pkg/data/font.ttf";
210const BOLD_FONT_PATH_1: &'static str = "/pkg/data/bold-font.ttf";
211const BOLD_FONT_PATH_2: &'static str = "/boot/data/bold-font.ttf";
212const ITALIC_FONT_PATH_1: &'static str = "/pkg/data/italic-font.ttf";
213const ITALIC_FONT_PATH_2: &'static str = "/boot/data/italic-font.ttf";
214const BOLD_ITALIC_FONT_PATH_1: &'static str = "/pkg/data/bold-italic-font.ttf";
215const BOLD_ITALIC_FONT_PATH_2: &'static str = "/boot/data/bold-italic-font.ttf";
216const FALLBACK_FONT_PREFIX: &'static str = "/pkg/data/fallback-font";
217
218impl VirtualConsoleViewAssistant {
219    pub fn new(
220        app_sender: &AppSender,
221        view_key: ViewKey,
222        color_scheme: ColorScheme,
223        round_scene_corners: bool,
224        font_size: f32,
225        dpi: BTreeSet<u32>,
226        boot_animation: bool,
227        is_primary: bool,
228    ) -> Result<ViewAssistantPtr, Error> {
229        let cell_size = Size::new(8.0, 16.0);
230        let tab_width = MIN_TAB_WIDTH;
231        let scene_details = None;
232        let terminals = BTreeMap::new();
233        let active_terminal_id = 0;
234        let font = load_font(PathBuf::from(FONT))?;
235        let bold_font = load_font(PathBuf::from(BOLD_FONT_PATH_1))
236            .or_else(|_| load_font(PathBuf::from(BOLD_FONT_PATH_2)))
237            .ok();
238        let italic_font = load_font(PathBuf::from(ITALIC_FONT_PATH_1))
239            .or_else(|_| load_font(PathBuf::from(ITALIC_FONT_PATH_2)))
240            .ok();
241        let bold_italic_font = load_font(PathBuf::from(BOLD_ITALIC_FONT_PATH_1))
242            .or_else(|_| load_font(PathBuf::from(BOLD_ITALIC_FONT_PATH_2)))
243            .ok();
244        let mut fallback_fonts = vec![];
245        while let Ok(font) = load_font(PathBuf::from(format!(
246            "{}-{}.ttf",
247            FALLBACK_FONT_PREFIX,
248            fallback_fonts.len() + 1
249        ))) {
250            fallback_fonts.push(font);
251        }
252        let font_set = FontSet::new(font, bold_font, italic_font, bold_italic_font, fallback_fonts);
253        let virtcon_mode = VirtconMode::Forced; // We always start out in forced mode.
254        let (animation, desired_virtcon_mode) = if boot_animation {
255            let file =
256                load_rive(BOOT_ANIMATION_PATH_1).or_else(|_| load_rive(BOOT_ANIMATION_PATH_2))?;
257            let artboard = file.artboard().ok_or_else(|| anyhow!("missing artboard"))?;
258            let artboard_ref = artboard.as_ref();
259            let color_scheme_name = match color_scheme {
260                DARK_COLOR_SCHEME => "dark",
261                LIGHT_COLOR_SCHEME => "light",
262                SPECIAL_COLOR_SCHEME => "special",
263                _ => "other",
264            };
265            // Find animation that matches color scheme or fallback to first animation
266            // if not found.
267            let animation = artboard_ref
268                .animations()
269                .find(|animation| {
270                    let name = animation.cast::<rive::animation::Animation>().as_ref().name();
271                    name == color_scheme_name
272                })
273                .or_else(|| artboard_ref.animations().next())
274                .ok_or_else(|| anyhow!("missing animation"))?;
275            let instance = rive::animation::LinearAnimationInstance::new(animation);
276            let last_presentation_time = None;
277            let animation =
278                Some(Animation { _file: file, artboard, instance, last_presentation_time });
279
280            (animation, VirtconMode::Forced)
281        } else {
282            (None, VirtconMode::Fallback)
283        };
284        let owns_display = true;
285        let active_pointer_id = None;
286        let start_pointer_location = Point::zero();
287        let power_button_press_handler =
288            RepeatedButtonPressHandler::new(ConsumerControlButton::Power);
289
290        Ok(Box::new(VirtualConsoleViewAssistant {
291            app_sender: app_sender.clone(),
292            view_key,
293            color_scheme,
294            round_scene_corners,
295            font_size,
296            dpi,
297            cell_size,
298            tab_width,
299            scene_details,
300            terminals,
301            font_set,
302            animation,
303            active_terminal_id,
304            virtcon_mode,
305            desired_virtcon_mode,
306            owns_display,
307            active_pointer_id,
308            start_pointer_location,
309            is_primary,
310            power_button_press_handler,
311        }))
312    }
313
314    #[cfg(test)]
315    fn new_for_test(animation: bool) -> Result<ViewAssistantPtr, Error> {
316        let app_sender = AppSender::new_for_testing_purposes_only();
317        let dpi: BTreeSet<u32> = [160, 320, 480, 640].iter().cloned().collect();
318        Self::new(
319            &app_sender,
320            Default::default(),
321            ColorScheme::default(),
322            false,
323            14.0,
324            dpi,
325            animation,
326            true,
327        )
328    }
329
330    // Resize all terminals for 'new_size'.
331    fn resize_terminals(&mut self, new_size: &Size, new_font_size: f32) {
332        let cell_size = cell_size_from_cell_height(&self.font_set, new_font_size);
333        let grid_size =
334            Size::new(new_size.width / cell_size.width, new_size.height / cell_size.height).floor();
335        // Clamp width to respect `MAX_CELLS`.
336        let clamped_grid_size = if grid_size.area() > MAX_CELLS as f32 {
337            assert!(
338                grid_size.height <= MAX_CELLS as f32,
339                "terminal height greater than MAX_CELLS: {}",
340                grid_size.height
341            );
342            Size::new(MAX_CELLS as f32 / grid_size.height, grid_size.height).floor()
343        } else {
344            grid_size
345        };
346        let clamped_size = Size::new(
347            clamped_grid_size.width * cell_size.width,
348            clamped_grid_size.height * cell_size.height,
349        );
350        let size = Size::new(clamped_size.width, clamped_size.height - cell_size.height);
351        let size_info = SizeInfo {
352            width: size.width,
353            height: size.height,
354            cell_width: cell_size.width,
355            cell_height: cell_size.height,
356            padding_x: 0.0,
357            padding_y: 0.0,
358            dpr: 1.0,
359        };
360
361        self.cell_size = cell_size;
362
363        for (terminal, _) in self.terminals.values_mut() {
364            terminal.resize(&size_info);
365        }
366
367        // PTY window size (in character cells).
368        let window_size = WindowSize {
369            width: clamped_grid_size.width as u32,
370            height: clamped_grid_size.height as u32 - 1,
371        };
372
373        let ptys: Vec<_> =
374            self.terminals.values().filter_map(|(term, _)| term.pty()).cloned().collect();
375        fasync::Task::local(async move {
376            join_all(ptys.iter().map(|pty| {
377                pty.resize(window_size).map(|result| result.expect("failed to set window size"))
378            }))
379            .map(|vec| vec.into_iter().collect())
380            .await
381        })
382        .detach();
383    }
384
385    // This returns a vector with the status for each terminal. The return value
386    // is suitable for passing to the TextGridFacet.
387    fn get_status(&self) -> Vec<(String, Rgb)> {
388        self.terminals
389            .iter()
390            .map(|(id, (t, status))| {
391                let fg = if *id == self.active_terminal_id {
392                    STATUS_COLOR_ACTIVE
393                } else if status.has_output {
394                    STATUS_COLOR_UPDATED
395                } else {
396                    STATUS_COLOR_DEFAULT
397                };
398
399                let left = if status.at_top { '[' } else { '<' };
400                let right = if status.at_bottom { ']' } else { '>' };
401
402                (format!("{}{}{} {}", left, *id, right, t.title()), fg)
403            })
404            .collect()
405    }
406
407    fn cancel_animation(&mut self) {
408        if self.animation.is_some() {
409            self.desired_virtcon_mode = VirtconMode::Fallback;
410            self.scene_details = None;
411            self.animation = None;
412            self.app_sender.request_render(self.view_key);
413        }
414    }
415
416    fn set_desired_virtcon_mode(&mut self, _context: &ViewAssistantContext) -> Result<(), Error> {
417        if self.desired_virtcon_mode != self.virtcon_mode {
418            self.virtcon_mode = self.desired_virtcon_mode;
419            // The primary view currently controls virtcon mode. More advanced
420            // coordination between views to determine virtcon mode can be added
421            // in the future when it becomes a requirement.
422            if self.is_primary {
423                self.app_sender.set_virtcon_mode(self.virtcon_mode);
424            }
425        }
426        Ok(())
427    }
428
429    fn set_active_terminal(&mut self, id: u32) {
430        if let Some((terminal, status)) = self.terminals.get_mut(&id) {
431            self.active_terminal_id = id;
432            status.has_output = false;
433            let terminal = terminal.clone_term();
434            let new_status = self.get_status();
435            if let Some(scene_details) = &mut self.scene_details {
436                if let Some(textgrid) = &scene_details.textgrid {
437                    scene_details.scene.send_message(
438                        textgrid,
439                        Box::new(TextGridMessages::<EventProxy>::ChangeStatusMessage(new_status)),
440                    );
441                    scene_details.scene.send_message(
442                        textgrid,
443                        Box::new(TextGridMessages::SetTermMessage(terminal)),
444                    );
445                    self.app_sender.request_render(self.view_key);
446                }
447            }
448        }
449    }
450
451    fn next_active_terminal(&mut self) {
452        let first = self.terminals.keys().next();
453        let last = self.terminals.keys().next_back();
454        if let Some((first, last)) = first.and_then(|first| last.map(|last| (first, last))) {
455            let active = self.active_terminal_id;
456            let id = if active == *last { *first } else { active + 1 };
457            self.set_active_terminal(id);
458        }
459    }
460
461    fn previous_active_terminal(&mut self) {
462        let first = self.terminals.keys().next();
463        let last = self.terminals.keys().next_back();
464        if let Some((first, last)) = first.and_then(|first| last.map(|last| (first, last))) {
465            let active = self.active_terminal_id;
466            let id = if active == *first { *last } else { active - 1 };
467            self.set_active_terminal(id);
468        }
469    }
470
471    fn update_status(&mut self) {
472        let new_status = self.get_status();
473        if let Some(scene_details) = &mut self.scene_details {
474            if let Some(textgrid) = &scene_details.textgrid {
475                scene_details.scene.send_message(
476                    textgrid,
477                    Box::new(TextGridMessages::<EventProxy>::ChangeStatusMessage(new_status)),
478                );
479                self.app_sender.request_render(self.view_key);
480            }
481        }
482    }
483
484    fn set_font_size(&mut self, font_size: f32) {
485        self.font_size = font_size;
486        self.scene_details = None;
487        self.app_sender.request_render(self.view_key);
488    }
489
490    fn scroll_active_terminal(&mut self, scroll: Scroll) {
491        if let Some((terminal, _)) = self.terminals.get_mut(&self.active_terminal_id) {
492            terminal.scroll(scroll);
493        }
494    }
495
496    fn trigger_shutdown(&mut self, shutdown_action: ShutdownAction) -> Result<(), Error> {
497        let (tx, rx) = std::sync::mpsc::channel();
498        std::thread::spawn(move || {
499            let res = (|| {
500                let (server_end, client_end) = zx::Channel::create();
501                connect_channel_to_protocol::<AdminMarker>(server_end)?;
502                let admin = AdminSynchronousProxy::new(client_end);
503                Ok(admin.shutdown(
504                    &ShutdownOptions {
505                        action: Some(shutdown_action),
506                        reasons: Some(vec![ShutdownReason::DeveloperRequest]),
507                        ..Default::default()
508                    },
509                    zx::MonotonicInstant::INFINITE,
510                )?)
511            })();
512            let _ = tx.send(res);
513        });
514
515        match rx.recv_timeout(std::time::Duration::from_secs(5)) {
516            Ok(Ok(Ok(()))) => {
517                println!("Shutdown call returned unexpectedly, sleeping forever.");
518                zx::MonotonicInstant::INFINITE.sleep();
519            }
520            Ok(Ok(Err(e))) => println!("Failed to shutdown, status: {}", e),
521            Ok(Err(e)) => return Err(e),
522            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
523                println!("Failed to shutdown due to timeout.");
524            }
525            Err(_) => panic!("Background shutdown thread terminated unexpectedly."),
526        }
527        Ok(())
528    }
529
530    fn handle_device_control_consumer_control_event(
531        &mut self,
532        _context: &mut ViewAssistantContext,
533        consumer_control_event: &input::consumer_control::Event,
534        event_time_ns: u64,
535    ) -> Result<bool, Error> {
536        let power_button_press_handled = self
537            .power_button_press_handler
538            .handle_button_event(consumer_control_event, event_time_ns);
539        if power_button_press_handled {
540            const POWER_KEY_PRESS_COUNT_FOR_REBOOT: usize = 3;
541            if self.power_button_press_handler.press_durations_ns.len()
542                >= POWER_KEY_PRESS_COUNT_FOR_REBOOT
543            {
544                let last_press_duration =
545                    *self.power_button_press_handler.press_durations_ns.last().expect("last");
546
547                const REBOOT_TO_BOOTLOADER_THRESHOLD: u64 =
548                    zx::MonotonicDuration::from_seconds(1).into_nanos() as u64;
549
550                if last_press_duration > REBOOT_TO_BOOTLOADER_THRESHOLD {
551                    self.trigger_shutdown(ShutdownAction::RebootToBootloader)?;
552                } else {
553                    self.trigger_shutdown(ShutdownAction::Reboot)?;
554                }
555            }
556            return Ok(true);
557        }
558
559        Ok(false)
560    }
561
562    fn handle_device_control_keyboard_event(
563        &mut self,
564        context: &mut ViewAssistantContext,
565        keyboard_event: &input::keyboard::Event,
566    ) -> Result<bool, Error> {
567        if keyboard_event.phase == input::keyboard::Phase::Pressed {
568            if keyboard_event.code_point.is_none() {
569                const HID_USAGE_KEY_ESC: u32 = 0x29;
570                const HID_USAGE_KEY_DELETE: u32 = 0x4c;
571
572                let modifiers = &keyboard_event.modifiers;
573                match keyboard_event.hid_usage {
574                    HID_USAGE_KEY_ESC if modifiers.alt => {
575                        self.cancel_animation();
576                        self.desired_virtcon_mode =
577                            if self.desired_virtcon_mode == VirtconMode::Fallback {
578                                VirtconMode::Forced
579                            } else {
580                                VirtconMode::Fallback
581                            };
582                        self.set_desired_virtcon_mode(context)?;
583                        return Ok(true);
584                    }
585                    // Provides a CTRL-ALT-DEL reboot sequence.
586                    HID_USAGE_KEY_DELETE if modifiers.control && modifiers.alt => {
587                        self.trigger_shutdown(ShutdownAction::Reboot)?;
588                        return Ok(true);
589                    }
590                    _ => {}
591                }
592            }
593        }
594
595        Ok(false)
596    }
597
598    fn handle_control_keyboard_event(
599        &mut self,
600        _context: &mut ViewAssistantContext,
601        keyboard_event: &input::keyboard::Event,
602    ) -> Result<bool, Error> {
603        match keyboard_event.phase {
604            input::keyboard::Phase::Pressed | input::keyboard::Phase::Repeat => {
605                let modifiers = &keyboard_event.modifiers;
606                match keyboard_event.code_point {
607                    None => {
608                        const HID_USAGE_KEY_ESC: u32 = 0x29;
609                        const HID_USAGE_KEY_TAB: u32 = 0x2b;
610                        const HID_USAGE_KEY_F1: u32 = 0x3a;
611                        const HID_USAGE_KEY_F10: u32 = 0x43;
612                        const HID_USAGE_KEY_HOME: u32 = 0x4a;
613                        const HID_USAGE_KEY_PAGEUP: u32 = 0x4b;
614                        const HID_USAGE_KEY_END: u32 = 0x4d;
615                        const HID_USAGE_KEY_PAGEDOWN: u32 = 0x4e;
616                        const HID_USAGE_KEY_DOWN: u32 = 0x51;
617                        const HID_USAGE_KEY_UP: u32 = 0x52;
618                        const HID_USAGE_KEY_VOL_DOWN: u32 = 0xe8;
619                        const HID_USAGE_KEY_VOL_UP: u32 = 0xe9;
620
621                        match keyboard_event.hid_usage {
622                            HID_USAGE_KEY_ESC if self.animation.is_some() => {
623                                self.cancel_animation();
624                                return Ok(true);
625                            }
626                            HID_USAGE_KEY_F1..=HID_USAGE_KEY_F10 if modifiers.alt => {
627                                let id = keyboard_event.hid_usage - HID_USAGE_KEY_F1;
628                                self.set_active_terminal(id);
629                                return Ok(true);
630                            }
631                            HID_USAGE_KEY_TAB if modifiers.alt => {
632                                if modifiers.shift {
633                                    self.previous_active_terminal();
634                                } else {
635                                    self.next_active_terminal();
636                                }
637                                return Ok(true);
638                            }
639                            HID_USAGE_KEY_VOL_UP if modifiers.alt => {
640                                self.previous_active_terminal();
641                                return Ok(true);
642                            }
643                            HID_USAGE_KEY_VOL_DOWN if modifiers.alt => {
644                                self.next_active_terminal();
645                                return Ok(true);
646                            }
647                            HID_USAGE_KEY_UP if modifiers.alt => {
648                                self.scroll_active_terminal(Scroll::Lines(1));
649                                return Ok(true);
650                            }
651                            HID_USAGE_KEY_DOWN if modifiers.alt => {
652                                self.scroll_active_terminal(Scroll::Lines(-1));
653                                return Ok(true);
654                            }
655                            HID_USAGE_KEY_PAGEUP if modifiers.shift => {
656                                self.scroll_active_terminal(Scroll::PageUp);
657                                return Ok(true);
658                            }
659                            HID_USAGE_KEY_PAGEDOWN if modifiers.shift => {
660                                self.scroll_active_terminal(Scroll::PageDown);
661                                return Ok(true);
662                            }
663                            HID_USAGE_KEY_HOME if modifiers.shift => {
664                                self.scroll_active_terminal(Scroll::Top);
665                                return Ok(true);
666                            }
667                            HID_USAGE_KEY_END if modifiers.shift => {
668                                self.scroll_active_terminal(Scroll::Bottom);
669                                return Ok(true);
670                            }
671                            _ => {}
672                        }
673                    }
674                    Some(code_point) if modifiers.alt == true => {
675                        const PLUS: u32 = 43;
676                        const EQUAL: u32 = 61;
677                        const MINUS: u32 = 45;
678
679                        match code_point {
680                            PLUS | EQUAL => {
681                                let new_font_size =
682                                    (self.font_size + FONT_SIZE_INCREMENT).min(MAX_FONT_SIZE);
683                                self.set_font_size(new_font_size);
684                                return Ok(true);
685                            }
686                            MINUS => {
687                                let new_font_size =
688                                    (self.font_size - FONT_SIZE_INCREMENT).max(MIN_FONT_SIZE);
689                                self.set_font_size(new_font_size);
690                                return Ok(true);
691                            }
692                            _ => {}
693                        }
694                    }
695                    _ => {}
696                }
697            }
698            _ => {}
699        }
700
701        Ok(false)
702    }
703}
704
705impl ViewAssistant for VirtualConsoleViewAssistant {
706    fn resize(&mut self, _new_size: &Size) -> Result<(), Error> {
707        self.scene_details = None;
708        Ok(())
709    }
710
711    fn render(
712        &mut self,
713        render_context: &mut RenderContext,
714        ready_event: zx::Event,
715        context: &ViewAssistantContext,
716    ) -> Result<(), Error> {
717        let mut scene_details = self.scene_details.take().unwrap_or_else(|| {
718            let mut builder = SceneBuilder::new()
719                .background_color(self.color_scheme.back)
720                .enable_mouse_cursor(false)
721                .round_scene_corners(self.round_scene_corners)
722                .mutable(false);
723
724            let textgrid = if let Some(animation) = &self.animation {
725                builder.facet(Box::new(RiveFacet::new(context.size, animation.artboard.clone())));
726                None
727            } else {
728                let scale_factor = if let Some(info) = context.display_info.as_ref() {
729                    // Use 1.0 scale factor when fallback sizes are used as opposed
730                    // to actual values reported by the display.
731                    if info.using_fallback_size {
732                        1.0
733                    } else {
734                        const MM_PER_INCH: f32 = 25.4;
735
736                        let dpi = context.size.height * MM_PER_INCH / info.vertical_size_mm as f32;
737
738                        get_scale_factor(&self.dpi, dpi)
739                    }
740                } else {
741                    1.0
742                };
743                let cell_height = self.font_size * scale_factor;
744
745                self.resize_terminals(&context.size, cell_height);
746
747                let active_term =
748                    self.terminals.get(&self.active_terminal_id).map(|(t, _)| t.clone_term());
749                let status = self.get_status();
750                let columns = active_term.as_ref().map(|t| t.borrow().cols().0).unwrap_or(1);
751
752                // Determine the status bar tab width based on the current number
753                // of terminals.
754                let tab_width =
755                    (columns as usize / (status.len() + 1)).clamp(MIN_TAB_WIDTH, MAX_TAB_WIDTH);
756
757                let cell_size = cell_size_from_cell_height(&self.font_set, cell_height);
758
759                // Add the text grid to the scene.
760                let textgrid = builder.facet(Box::new(TextGridFacet::new(
761                    self.font_set.clone(),
762                    &cell_size,
763                    self.color_scheme,
764                    active_term,
765                    status,
766                    tab_width,
767                )));
768
769                self.cell_size = cell_size;
770                self.tab_width = tab_width;
771
772                Some(textgrid)
773            };
774
775            SceneDetails { scene: builder.build(), textgrid }
776        });
777
778        if let Some(animation) = &mut self.animation {
779            let presentation_time = context.presentation_time;
780            let elapsed = if let Some(last_presentation_time) = animation.last_presentation_time {
781                const NANOS_PER_SECOND: f32 = 1_000_000_000.0;
782                (presentation_time - last_presentation_time).into_nanos() as f32 / NANOS_PER_SECOND
783            } else {
784                0.0
785            };
786            animation.last_presentation_time = Some(presentation_time);
787
788            let artboard_ref = animation.artboard.as_ref();
789            animation.instance.advance(elapsed);
790            animation.instance.apply(animation.artboard.clone(), 1.0);
791            artboard_ref.advance(elapsed);
792        }
793
794        scene_details.scene.render(render_context, ready_event, context)?;
795        self.scene_details = Some(scene_details);
796
797        if let Some(animation) = &mut self.animation {
798            // Switch to fallback mode when animation ends so primary client can
799            // take over. Otherwise, request another frame.
800            if animation.instance.is_done() {
801                self.desired_virtcon_mode = VirtconMode::Fallback;
802            } else {
803                context.request_render();
804            }
805        }
806
807        self.set_desired_virtcon_mode(context)?;
808
809        Ok(())
810    }
811
812    fn handle_consumer_control_event(
813        &mut self,
814        context: &mut ViewAssistantContext,
815        event: &input::Event,
816        consumer_control_event: &input::consumer_control::Event,
817    ) -> Result<(), Error> {
818        self.handle_device_control_consumer_control_event(
819            context,
820            consumer_control_event,
821            event.event_time,
822        )?;
823        Ok(())
824    }
825
826    fn handle_keyboard_event(
827        &mut self,
828        context: &mut ViewAssistantContext,
829        _event: &input::Event,
830        keyboard_event: &input::keyboard::Event,
831    ) -> Result<(), Error> {
832        if self.handle_device_control_keyboard_event(context, keyboard_event)? {
833            return Ok(());
834        }
835
836        if !self.owns_display {
837            return Ok(());
838        }
839
840        if self.handle_control_keyboard_event(context, keyboard_event)? {
841            return Ok(());
842        }
843
844        if let Some((terminal, _)) = self.terminals.get_mut(&self.active_terminal_id) {
845            // Get input sequence and write it to the active terminal.
846            let app_cursor = terminal.mode().contains(TermMode::APP_CURSOR);
847            if let Some(string) = get_input_sequence_for_key_event(keyboard_event, app_cursor) {
848                terminal
849                    .write_all(string.as_bytes())
850                    .unwrap_or_else(|e| println!("failed to write to terminal: {}", e));
851
852                // Scroll to bottom on input.
853                self.scroll_active_terminal(Scroll::Bottom);
854            }
855        }
856
857        Ok(())
858    }
859
860    fn handle_pointer_event(
861        &mut self,
862        _context: &mut ViewAssistantContext,
863        _event: &input::Event,
864        pointer_event: &input::pointer::Event,
865    ) -> Result<(), Error> {
866        match &pointer_event.phase {
867            input::pointer::Phase::Down(location) => {
868                self.active_pointer_id = Some(pointer_event.pointer_id.clone());
869                self.start_pointer_location = location.to_f32();
870            }
871            input::pointer::Phase::Moved(location) => {
872                if Some(pointer_event.pointer_id.clone()) == self.active_pointer_id {
873                    let location_offset = location.to_f32() - self.start_pointer_location;
874
875                    fn div_and_trunc(value: f32, divisor: f32) -> isize {
876                        (value / divisor).trunc() as isize
877                    }
878
879                    // Movement along X-axis changes active terminal.
880                    let tab_width = self.tab_width as f32 * self.cell_size.width;
881                    let mut terminal_offset = div_and_trunc(location_offset.x, tab_width);
882                    while terminal_offset > 0 {
883                        self.previous_active_terminal();
884                        self.start_pointer_location.x += tab_width;
885                        terminal_offset -= 1;
886                    }
887                    while terminal_offset < 0 {
888                        self.next_active_terminal();
889                        self.start_pointer_location.x -= tab_width;
890                        terminal_offset += 1;
891                    }
892
893                    // Movement along Y-axis scrolls active terminal.
894                    let cell_offset = div_and_trunc(location_offset.y, self.cell_size.height);
895                    if cell_offset != 0 {
896                        self.scroll_active_terminal(Scroll::Lines(cell_offset));
897                        self.start_pointer_location.y += cell_offset as f32 * self.cell_size.height;
898                    }
899                }
900            }
901            input::pointer::Phase::Up => {
902                if Some(pointer_event.pointer_id.clone()) == self.active_pointer_id {
903                    self.active_pointer_id = None;
904                }
905            }
906            _ => (),
907        }
908        Ok(())
909    }
910
911    fn handle_message(&mut self, message: carnelian::Message) {
912        if let Some(message) = message.downcast_ref::<ViewMessages>() {
913            match message {
914                ViewMessages::AddTerminalMessage(id, terminal, make_active) => {
915                    let terminal = terminal.try_clone().expect("failed to clone terminal");
916                    let has_output = true;
917                    let display_offset = terminal.display_offset();
918                    let at_top = display_offset == terminal.history_size();
919                    let at_bottom = display_offset == 0;
920                    self.terminals
921                        .insert(*id, (terminal, TerminalStatus { has_output, at_top, at_bottom }));
922                    // Rebuild the scene after a terminal is added. This should
923                    // be fine as it is rare that a terminal is added.
924                    if self.animation.is_none() {
925                        self.scene_details = None;
926                        self.app_sender.request_render(self.view_key);
927                    }
928                    if *make_active {
929                        self.set_active_terminal(*id);
930                    }
931                }
932                ViewMessages::RequestTerminalUpdateMessage(id) => {
933                    if let Some((terminal, status)) = self.terminals.get_mut(id) {
934                        let has_output = if *id == self.active_terminal_id {
935                            self.app_sender.request_render(self.view_key);
936                            false
937                        } else {
938                            true
939                        };
940                        let display_offset = terminal.display_offset();
941                        let at_top = display_offset == terminal.history_size();
942                        let at_bottom = display_offset == 0;
943                        let new_status = TerminalStatus { has_output, at_top, at_bottom };
944                        let old_status = mem::replace(status, new_status);
945                        if new_status != old_status {
946                            self.update_status();
947                        }
948                    }
949                }
950            }
951        }
952    }
953
954    fn ownership_changed(&mut self, owned: bool) -> Result<(), Error> {
955        self.owns_display = owned;
956        Ok(())
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    #[test]
965    fn can_create_view() -> Result<(), Error> {
966        let animation = false;
967        let _ = VirtualConsoleViewAssistant::new_for_test(animation)?;
968        Ok(())
969    }
970
971    #[test]
972    fn can_create_view_with_animation() -> Result<(), Error> {
973        let animation = true;
974        let _ = VirtualConsoleViewAssistant::new_for_test(animation)?;
975        Ok(())
976    }
977
978    #[test]
979    fn power_button_press_handler_repetitive_presses() {
980        use carnelian::input::consumer_control::Event;
981
982        let mut handler = RepeatedButtonPressHandler::new(ConsumerControlButton::Power);
983        const EVENT_DOWN: Event = Event {
984            phase: carnelian::input::consumer_control::Phase::Down,
985            button: ConsumerControlButton::Power,
986        };
987        const EVENT_UP: Event = Event {
988            phase: carnelian::input::consumer_control::Phase::Up,
989            button: ConsumerControlButton::Power,
990        };
991
992        handler.handle_button_event(&EVENT_DOWN, 1_000_000_000);
993        handler.handle_button_event(&EVENT_UP, 1_100_000_000);
994        assert_eq!(handler.press_durations_ns, vec![100_000_000]);
995        assert_eq!(handler.down_times_ns, vec![1_000_000_000]);
996
997        handler.handle_button_event(&EVENT_DOWN, 1_200_000_000);
998        handler.handle_button_event(&EVENT_UP, 1_400_000_000);
999        assert_eq!(handler.press_durations_ns, vec![100_000_000, 200_000_000]);
1000        assert_eq!(handler.down_times_ns, vec![1_000_000_000, 1_200_000_000]);
1001    }
1002
1003    #[test]
1004    fn power_button_press_handler_press_timeout() {
1005        use carnelian::input::consumer_control::Event;
1006
1007        let mut handler = RepeatedButtonPressHandler::new(ConsumerControlButton::Power);
1008        const EVENT_DOWN: Event = Event {
1009            phase: carnelian::input::consumer_control::Phase::Down,
1010            button: ConsumerControlButton::Power,
1011        };
1012        const EVENT_UP: Event = Event {
1013            phase: carnelian::input::consumer_control::Phase::Up,
1014            button: ConsumerControlButton::Power,
1015        };
1016
1017        handler.handle_button_event(&EVENT_DOWN, 1_000_000_000);
1018        handler.handle_button_event(&EVENT_UP, 1_100_000_000);
1019        assert_eq!(handler.press_durations_ns, vec![100_000_000]);
1020        assert_eq!(handler.down_times_ns, vec![1_000_000_000]);
1021
1022        handler.handle_button_event(&EVENT_DOWN, 5_100_000_000);
1023        handler.handle_button_event(&EVENT_UP, 5_300_000_000);
1024        assert_eq!(handler.press_durations_ns, vec![200_000_000]);
1025        assert_eq!(handler.down_times_ns, vec![5_100_000_000]);
1026    }
1027
1028    #[test]
1029    fn power_button_press_handler_non_power_key_ignored() {
1030        use carnelian::input::consumer_control::Event;
1031
1032        let mut handler = RepeatedButtonPressHandler::new(ConsumerControlButton::Power);
1033        const POWER_EVENT_DOWN: Event = Event {
1034            phase: carnelian::input::consumer_control::Phase::Down,
1035            button: ConsumerControlButton::Power,
1036        };
1037        const POWER_EVENT_UP: Event = Event {
1038            phase: carnelian::input::consumer_control::Phase::Up,
1039            button: ConsumerControlButton::Power,
1040        };
1041        const FUNCTION_EVENT_DOWN: Event = Event {
1042            phase: carnelian::input::consumer_control::Phase::Down,
1043            button: ConsumerControlButton::Function,
1044        };
1045        const FUNCTION_EVENT_UP: Event = Event {
1046            phase: carnelian::input::consumer_control::Phase::Up,
1047            button: ConsumerControlButton::Function,
1048        };
1049
1050        handler.handle_button_event(&POWER_EVENT_DOWN, 1_000_000_000);
1051        handler.handle_button_event(&POWER_EVENT_UP, 1_100_000_000);
1052        assert_eq!(handler.press_durations_ns, vec![100_000_000]);
1053        assert_eq!(handler.down_times_ns, vec![1_000_000_000]);
1054
1055        handler.handle_button_event(&FUNCTION_EVENT_DOWN, 1_200_000_000);
1056        handler.handle_button_event(&FUNCTION_EVENT_UP, 1_300_000_000);
1057        assert_eq!(handler.press_durations_ns, vec![100_000_000]);
1058        assert_eq!(handler.down_times_ns, vec![1_000_000_000]);
1059    }
1060}