1use 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
68const MIN_TAB_WIDTH: usize = 16;
70const MAX_TAB_WIDTH: usize = 32;
71
72const 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
77const FONT_SIZE_INCREMENT: f32 = 4.0;
79
80const MAX_CELLS: u32 = SceneOrder::MAX.as_u32() / 4;
82
83struct Animation {
84 _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
103struct RepeatedButtonPressHandler {
108 press_durations_ns: Vec<u64>,
113
114 down_times_ns: Vec<u64>,
117
118 button: ConsumerControlButton,
119
120 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 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 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 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; 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}