Skip to main content

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