1use 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
61const MIN_TAB_WIDTH: usize = 16;
63const MAX_TAB_WIDTH: usize = 32;
64
65const 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
70const FONT_SIZE_INCREMENT: f32 = 4.0;
72
73const 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
88struct RepeatedButtonPressHandler {
93 press_durations_ns: Vec<u64>,
98
99 down_times_ns: Vec<u64>,
102
103 button: ConsumerControlButton,
104
105 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}