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