use crate::appearance::Appearance; use crate::pane_group::SplitPaneState; use crate::settings::EnforceMinimumContrast; use crate::terminal::blockgrid_renderer::GridRenderParams; use crate::terminal::find::TerminalFindModel; use crate::terminal::grid_renderer::CellGlyphCache; use crate::terminal::meta_shortcuts::handle_keystroke_despite_composing; use crate::terminal::model::escape_sequences::{ maybe_kitty_keyboard_escape_sequence, KeystrokeWithDetails, ToEscapeSequence, }; use crate::terminal::model::grid::grid_handler::{Link, TermMode}; use crate::terminal::model::grid::{Dimensions, RespectDisplayedOutput}; use crate::terminal::model::index::Point; use crate::terminal::model::mouse::{MouseAction, MouseButton, MouseState}; use crate::terminal::model::selection::{SelectAction, SelectionPoint}; use crate::terminal::model::terminal_model::WithinModel; use crate::terminal::model::SecretHandle; use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode; use crate::terminal::shared_session::presence_manager::{ text_selection_color, PresenceManager, MUTED_PARTICIPANT_COLOR, }; use crate::terminal::view::{ ActiveSessionState, TerminalAction, TerminalEditor, TerminalViewRenderContext, }; use crate::terminal::{grid_renderer, SizeInfo}; use crate::terminal::{heights_approx_eq, TerminalModel}; use num_traits::Float as _; use parking_lot::FairMutex; use pathfinder_geometry::vector::vec2f; use vec1::Vec1; use galaxy_core::features::FeatureFlag; use galaxy_util::user_input::UserInput; use galaxyui::elements::new_scrollable::{NewScrollableElement, ScrollableAxis}; use galaxyui::event::{KeyState, ModifiersState}; use galaxyui::platform::keyboard::KeyCode; use galaxyui::text::SelectionType; use super::{should_intercept_mouse, should_intercept_scroll}; use std::ops::{Deref as _, Range}; use std::sync::Arc; use galaxyui::elements::{Axis, Fill, Point as UiPoint, ScrollData, ScrollableElement}; use galaxyui::fonts::Properties; use galaxyui::geometry::rect::RectF; use galaxyui::geometry::vector::Vector2F; use galaxyui::units::{IntoLines, IntoPixels, Lines, Pixels}; use galaxyui::{ end_trace, event::{DispatchedEvent, InBoundsExt}, record_trace_event, start_trace, AfterLayoutContext, AppContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use galaxyui::{ClipBounds, EntityId, ModelHandle}; const CLI_SUBAGENT_HORIZONTAL_MARGIN: f32 = 8.; const CLI_SUBAGENT_VERTICAL_MARGIN: f32 = 8.; pub struct AltScreenElement { model: Arc>, find_model: ModelHandle, is_terminal_focused: bool, is_terminal_selecting: bool, size: Option, bounds: Option, origin: Option, highlighted_url: Option, link_tool_tip: Option, grid_render_params: GridRenderParams, /// Optional handle to a hovered secret. hovered_secret: Option, /// Used to save the position of the active cursor. terminal_view_id: EntityId, pane_state: SplitPaneState, active_session_state: ActiveSessionState, selection_range: Option>>, presence_manager: Option>, // Fields needed for vertical scrolling for shared session viewer when window is smaller than sharer's scroll_top: Lines, max_scroll_top: Option, visible_lines: Option, cursor_hint_text: Option>, cli_subagent_view: Option>, /// Voice input toggle key code for CLI agent footer integration. #[cfg_attr(not(feature = "voice_input"), allow(unused))] voice_input_toggle_key_code: Option, } impl AltScreenElement { #[allow(clippy::too_many_arguments)] pub fn new( model: Arc>, terminal_view_render_context: TerminalViewRenderContext, find_model: ModelHandle, enforce_minimum_contrast: EnforceMinimumContrast, selection_range: Option>>, appearance: &Appearance, scroll_top: Lines, cursor_hint_text: Option>, cli_subagent_view: Option>, ) -> Self { let highlighted_url = terminal_view_render_context .highlighted_url .map(TryInto::try_into) .and_then(Result::ok); let link_tool_tip = terminal_view_render_context .link_tool_tip .map(TryInto::try_into) .and_then(Result::ok); Self { model, find_model, is_terminal_focused: terminal_view_render_context.is_terminal_focused, is_terminal_selecting: terminal_view_render_context.is_terminal_selecting, terminal_view_id: terminal_view_render_context.terminal_view_id, selection_range, size: None, bounds: None, origin: None, hovered_secret: terminal_view_render_context.hovered_secret, highlighted_url, link_tool_tip, pane_state: terminal_view_render_context.pane_state, active_session_state: terminal_view_render_context.active_session_state, grid_render_params: GridRenderParams { warp_theme: appearance.theme().clone(), font_family: appearance.monospace_font_family(), font_size: appearance.monospace_font_size(), font_weight: appearance.monospace_font_weight(), line_height_ratio: appearance.ui_builder().line_height_ratio(), enforce_minimum_contrast, obfuscate_secrets: terminal_view_render_context.obfuscate_secrets, size_info: terminal_view_render_context.size_info, cell_size: Vector2F::new( terminal_view_render_context .size_info .cell_width_px() .as_f32(), terminal_view_render_context .size_info .cell_height_px() .as_f32(), ), use_ligature_rendering: false, hide_cursor_cell: false, }, presence_manager: None, scroll_top, visible_lines: None, max_scroll_top: None, cursor_hint_text, cli_subagent_view, voice_input_toggle_key_code: None, } } pub fn with_ligature_rendering(mut self) -> Self { self.grid_render_params.use_ligature_rendering = true; self } pub fn with_hide_cursor_cell(mut self) -> Self { self.grid_render_params.hide_cursor_cell = true; self } pub fn with_shared_session_presence( mut self, presence_manager: Option>, ) -> Self { self.presence_manager = presence_manager; self } /// Sets the voice input toggle key code for CLI agent footer integration. #[cfg(feature = "voice_input")] pub fn with_voice_input_toggle_key(mut self, key_code: Option) -> Self { self.voice_input_toggle_key_code = key_code; self } fn key_down(&mut self, chars: &str, ctx: &mut EventContext) -> bool { if self.is_terminal_focused && !chars.is_empty() && chars.chars().all(|c| c.is_control()) { ctx.dispatch_typed_action(TerminalAction::KeyDown(chars.to_string())); true } else { false } } fn typed_characters(&mut self, chars: &str, ctx: &mut EventContext) -> bool { if self.is_terminal_focused && !chars.is_empty() { ctx.dispatch_typed_action(TerminalAction::TypedCharacters(chars.to_string())); } true } fn set_marked_text( &mut self, marked_text: &str, selected_range: &Range, ctx: &mut EventContext, ) -> bool { if self.is_terminal_focused { ctx.dispatch_typed_action(TerminalAction::SetMarkedText { marked_text: UserInput::new(marked_text), selected_range: selected_range.clone(), }); } true } fn clear_marked_text(&mut self, ctx: &mut EventContext) -> bool { if self.is_terminal_focused { ctx.dispatch_typed_action(TerminalAction::ClearMarkedText); } true } fn drag_and_drop_file(&mut self, paths: &[String], ctx: &mut EventContext) -> bool { if self.is_terminal_focused && !paths.is_empty() { let paths = paths.iter().map(ToOwned::to_owned).collect(); ctx.dispatch_typed_action(TerminalAction::DragAndDropFiles(paths)); return true; } false } fn middle_mouse_down(&self, local_position: Vector2F, ctx: &mut EventContext) -> bool { let point = self.coord_to_point(local_position); ctx.dispatch_typed_action(TerminalAction::MiddleClickOnGrid { position: Some(WithinModel::AltScreen(Point { col: point.col, row: point.row, })), }); true } fn left_mouse_down( &self, mouse_state: MouseState, local_position: Vector2F, click_count: u32, ctx: &mut EventContext, app: &AppContext, ) -> bool { if !self.pane_state.is_focused() { return false; } ctx.dispatch_typed_action(TerminalAction::Focus); // On mobile, request soft keyboard so users can input. if galaxyui::platform::is_mobile_device() { ctx.request_soft_keyboard(); } let point = self.coord_to_point(local_position); let side = self .grid_render_params .size_info .get_mouse_side(local_position); let selection_type = if FeatureFlag::RectSelection.is_enabled() { SelectionType::from_mouse_event(*mouse_state.modifiers(), click_count) } else { SelectionType::from_click_count(click_count) }; if should_intercept_mouse(&self.model.lock(), mouse_state.modifiers().shift, app) { ctx.dispatch_typed_action(TerminalAction::AltSelect(SelectAction::Begin { point, side, selection_type, position: local_position, })); } else { ctx.dispatch_typed_action(TerminalAction::MaybeClearAltSelect); ctx.dispatch_typed_action(TerminalAction::AltMouseAction(mouse_state.set_point(point))); } true } fn right_mouse_down( &self, mouse_state: MouseState, local_position: Vector2F, ctx: &mut EventContext, app: &AppContext, ) -> bool { if self.active_session_state != ActiveSessionState::Active { return false; } let point = self.coord_to_point(local_position); if should_intercept_mouse(&self.model.lock(), mouse_state.modifiers().shift, app) { ctx.dispatch_typed_action(TerminalAction::AltScreenContextMenu { position: local_position, }); } else { ctx.dispatch_typed_action(TerminalAction::AltMouseAction(mouse_state.set_point(point))); } true } fn mouse_moved( &self, local_position: Vector2F, is_synthetic: bool, app: &AppContext, ctx: &mut EventContext, ) -> bool { if self.active_session_state != ActiveSessionState::Active { return false; } let point = self.coord_to_point(local_position); // If SGR_MOUSE is set -- we consider user to be in an editor like vim or nano. let from_editor = match self.model.lock().is_term_mode_set(TermMode::SGR_MOUSE) { true => TerminalEditor::Yes, false => TerminalEditor::No, }; let grid_point = WithinModel::AltScreen(Point { col: point.col, row: point.row, }); if get_secret_obfuscation_mode(app).is_visually_obfuscated() { let secret_handle = self .model .lock() .secret_at_point(&grid_point) .map(|(handle, _)| handle); ctx.dispatch_typed_action(TerminalAction::MaybeHoverSecret { secret_handle }); } ctx.dispatch_typed_action(TerminalAction::MaybeLinkHover { position: Some(grid_point), from_editor, }); // For alt-screen PTY purposes, we ignore synthetic mouse events! // This is especially relevant for mouse drags - we do not want mouse moved // events being handled at the same time as mouse dragged events. if !is_synthetic && self.model.lock().is_term_mode_set(TermMode::MOUSE_MOTION) { // Note: it is counter-intuitive to use "pressed" as a state here for mouse hover motions, however, // we're largely just following standards from Alacritty/other terminals and the original terminal specs. // We intend to combine the mouse button and mouse action enums to avoid "weird" or "impossible" combinations, // see Linear issue at https://linear.app/warpdotdev/issue/CORE-1039/combine-the-mousebutton-and-mouseaction-enums-to-avoid-impossible. let mouse_state = MouseState::new(MouseButton::Move, MouseAction::Pressed, Default::default()); ctx.dispatch_typed_action(TerminalAction::AltMouseAction(mouse_state.set_point(point))); } // Allow the event to continue propagating. false } /// Called when the mouse is moved outside of the element. fn mouse_out(&self, ctx: &mut EventContext) -> bool { ctx.dispatch_typed_action(TerminalAction::MaybeLinkHover { position: None, from_editor: TerminalEditor::No, }); true } fn mouse_up( &self, mouse_state: MouseState, local_position: Vector2F, ctx: &mut EventContext, app: &AppContext, ) -> bool { if self.active_session_state != ActiveSessionState::Active { return false; } let point = self.coord_to_point(local_position); ctx.dispatch_typed_action(TerminalAction::ClickOnGrid { position: WithinModel::AltScreen(Point { col: point.col, row: point.row, }), modifiers: *mouse_state.modifiers(), }); if self.is_terminal_selecting { ctx.dispatch_typed_action(TerminalAction::AltSelect(SelectAction::End)); } if !should_intercept_mouse(&self.model.lock(), mouse_state.modifiers().shift, app) { ctx.dispatch_typed_action(TerminalAction::AltMouseAction(mouse_state.set_point(point))); } true } fn mouse_dragged( &self, mouse_state: MouseState, local_position: Vector2F, ctx: &mut EventContext, app: &AppContext, ) -> bool { if self.active_session_state != ActiveSessionState::Active { return false; } let mut is_mouse_dragged = false; let point = self.coord_to_point(local_position); if self.is_terminal_selecting && self.bounds.is_some() { let side = self .grid_render_params .size_info .get_mouse_side(local_position); ctx.dispatch_typed_action(TerminalAction::AltSelect(SelectAction::Update { point, // Scrolling is not yet implemented in AltScreen so leave as 0 for now delta: Lines::zero(), side, position: local_position, })); is_mouse_dragged = true; } if !should_intercept_mouse(&self.model.lock(), mouse_state.modifiers().shift, app) { ctx.dispatch_typed_action(TerminalAction::AltMouseAction(mouse_state.set_point(point))); } is_mouse_dragged } fn on_scroll( &mut self, local_position: Vector2F, delta: Vector2F, precise: bool, ctx: &mut EventContext, app: &AppContext, ) -> bool { let cell_height = self.grid_render_params.size_info.cell_height_px; let delta = if precise { // Handle Trackpad Scroll by converting pixel height into lines. delta.y().into_pixels().to_lines(cell_height) } else { // Handle Mouse Scroll, whose delta is already in terms of lines. delta.y().into_lines() }; // The alt screen can be vertically scrollable iff we're a shared session reader // and our window is smaller than the sharer's. if self.model.lock().shared_session_status().is_reader() { ScrollableElement::scroll(self, delta.to_pixels(cell_height), ctx); } ctx.dispatch_typed_action(TerminalAction::MaybeDismissToolTip { from_keybinding: false, }); let delta = self .model .lock() .alt_screen_mut() .accumulate_lines_to_scroll(delta); if should_intercept_scroll(&self.model.lock(), app) { ctx.dispatch_typed_action(TerminalAction::AltScroll { delta }); } else { let point = self.coord_to_point(local_position); ctx.dispatch_typed_action(TerminalAction::AltMouseAction( MouseState::new( MouseButton::Wheel, MouseAction::Scrolled { delta }, Default::default(), ) .set_point(point), )); } true } /// Return a Vector2F that can be used to adjust a position to account for vertical scrolling. fn vertical_scroll_pixels(&self) -> Vector2F { vec2f( 0., self.scroll_top.as_f64() as f32 * self.line_height().as_f32(), ) } /// Converts a pixel coordinate to a point in the `AltScreen` coordinate space. fn coord_to_point(&self, coord: Vector2F) -> Point { let model = self.model.lock(); let grid = model.alt_screen().grid_handler(); let total_height = grid.total_rows(); let size = self.grid_render_params.size_info; let column = ((coord.x() - size.padding_x_px.as_f32()) / size.cell_width_px().as_f32()) .max(0.) .min(grid.columns() as f32 - 1.) as usize; let row = (coord.y() / size.cell_height_px().as_f32()) .max(0.) .min(total_height as f32 - 1.) as usize; Point::new(row, column) } /// Renders our own selection. fn render_selections(&self, size_info: &SizeInfo, origin: Vector2F, ctx: &mut PaintContext) { let text_selection_color = self .grid_render_params .warp_theme .text_selection_color() .into_solid(); if let Some(ranges) = &self.selection_range { for range in ranges { let start = SelectionPoint { row: range.start.row.into_lines(), col: range.start.col, }; let end = SelectionPoint { row: range.end.row.into_lines(), col: range.end.col, }; grid_renderer::render_selection( &start, &end, size_info, Lines::zero(), origin, text_selection_color, ctx, ); } }; } /// Renders any shared session participants' selections. fn render_participant_selections( &self, size_info: &SizeInfo, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext, ) { if let Some(presence_manager) = &self.presence_manager { let is_self_reconnecting = presence_manager.as_ref(app).is_reconnecting(); for participant in presence_manager.as_ref(app).all_present_participants() { let session_sharing_protocol::common::Selection::AltScreenText { start, end, is_reversed, } = &participant.info.selection else { continue; }; let start = SelectionPoint { row: start.row.into_lines(), col: start.col, }; let end = SelectionPoint { row: end.row.into_lines(), col: end.col, }; let participant_color = if is_self_reconnecting { MUTED_PARTICIPANT_COLOR } else { participant.color }; grid_renderer::render_selection( &start, &end, size_info, Lines::zero(), origin, text_selection_color(participant_color), ctx, ); let cursor_point = if *is_reversed { &start } else { &end }; grid_renderer::render_selection_cursor( cursor_point, size_info, Lines::zero(), origin, participant_color, !*is_reversed, ctx, ); } } } fn total_lines(&self) -> Lines { self.grid_render_params.size_info.rows().into_lines() } fn line_height(&self) -> Pixels { self.grid_render_params.size_info.cell_height_px() } #[cfg(feature = "voice_input")] fn maybe_handle_voice_toggle( &self, key_code: &KeyCode, state: &KeyState, ctx: &mut EventContext, ) -> bool { if let Some(voice_input_toggle_key_code) = self.voice_input_toggle_key_code { if *key_code == voice_input_toggle_key_code { ctx.dispatch_typed_action(TerminalAction::ToggleCLIAgentVoiceInput( voice_input::VoiceInputToggledFrom::Key { state: *state }, )); return true; } } false } #[cfg(not(feature = "voice_input"))] fn maybe_handle_voice_toggle( &self, _key_code: &KeyCode, _state: &KeyState, _ctx: &mut EventContext, ) -> bool { false } } impl Element for AltScreenElement { fn layout( &mut self, constraint: SizeConstraint, ctx: &mut LayoutContext, app: &AppContext, ) -> Vector2F { self.size = Some(constraint.max); if let Some(cursor_hint_text) = &mut self.cursor_hint_text { cursor_hint_text.layout(constraint, ctx, app); } if let Some(cli_subagent_view) = &mut self.cli_subagent_view { cli_subagent_view.layout( SizeConstraint { min: vec2f(0., 0.), max: vec2f( constraint.max.x() * 0.3 - CLI_SUBAGENT_HORIZONTAL_MARGIN, constraint.max.y() - CLI_SUBAGENT_VERTICAL_MARGIN * 3., ), }, ctx, app, ); } constraint.max } fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) { let size = self.size.expect("Size should be set in `layout()`"); self.visible_lines = Some(size.y().into_pixels().to_lines(self.line_height()).floor()); self.max_scroll_top = Some(self.total_lines() - self.visible_lines.unwrap()); // After resizing the window to be larger, the max_scroll_top could have decreased, // so we need to make sure scroll_top is in bounds. self.scroll_top = self.scroll_top.min(self.max_scroll_top.unwrap()); // We want to make sure to call after_layout on each of the elements that were actually laid out. if let Some(cli_subagent_view) = &mut self.cli_subagent_view { if cli_subagent_view.size().is_some() { cli_subagent_view.after_layout(ctx, app); } } } fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) { start_trace!("alt_screen_element:paint"); self.bounds = Some(RectF::new( origin, self.size.expect("Size should be set before paint"), )); self.origin = Some(UiPoint::from_vec2f(origin, ctx.scene.z_index())); let model = self.model.lock(); let override_colors = model.override_colors(); let grid = model.alt_screen().grid_handler(); let cell_size = Vector2F::new( self.grid_render_params.size_info.cell_width_px().as_f32(), self.grid_render_params.size_info.cell_height_px().as_f32(), ); let mut glyphs = CellGlyphCache::default(); let properties = Properties::default(); let padding_x = self.grid_render_params.size_info.padding_x_px(); let find_model = self.find_model.as_ref(app); let find_run = find_model .is_find_bar_open() .then(|| find_model.alt_screen_find_run()) .flatten(); let alt_screen_matches = find_run.map(|run| run.matches().iter().rev()); let focused_match_range = find_run.and_then(|run| run.focused_match_range()); let obfuscate_secrets = get_secret_obfuscation_mode(app).and(&grid.get_secret_obfuscation()); let mut sampler = model.alt_screen().bg_color_sampler.lock(); if let Some(bg_color) = sampler.most_common() { if !bg_color.is_fully_transparent() { if let Some(bounds) = self.bounds { ctx.scene .draw_rect_without_hit_recording(bounds) .with_background(Fill::Solid(bg_color)); } } } sampler.reset(); // Render grid cells. Since the alt screen has no scrollback we can always start at index 0. record_trace_event!("alt_screen_element:paint:preparing_to_render_grid"); let start_row = self.scroll_top.as_f64(); let end_row = (start_row + self .visible_lines .expect("should be set after layout") .as_f64()) .min(grid.visible_rows() as f64); let adjusted_grid_origin = origin - self.vertical_scroll_pixels(); let cursor_visible = model.alt_screen().is_mode_set(TermMode::SHOW_CURSOR); grid_renderer::render_grid( grid, start_row.floor() as usize, end_row.ceil() as usize, &model.colors(), &override_colors, &self.grid_render_params.warp_theme, properties, self.grid_render_params.font_family, self.grid_render_params.font_size, self.grid_render_params.line_height_ratio, cell_size, padding_x, adjusted_grid_origin, &mut glyphs, 255, /* alpha */ self.highlighted_url.as_ref(), self.link_tool_tip.as_ref(), alt_screen_matches, focused_match_range, self.grid_render_params.enforce_minimum_contrast, obfuscate_secrets, self.hovered_secret, self.grid_render_params.use_ligature_rendering, cursor_visible.then(|| model.alt_screen().cursor_style().shape), RespectDisplayedOutput::Yes, &model.image_id_to_metadata, Some(&mut sampler), self.grid_render_params.hide_cursor_cell, ctx, app, ); record_trace_event!("alt_screen_element:paint:grid_rendered"); // Render cursor if the escape sequence is set. // Also suppress the cursor when hide_cursor_cell is active (CLI agent rich input is open). if cursor_visible && !self.grid_render_params.hide_cursor_cell { grid_renderer::render_cursor( &self.grid_render_params, grid.cursor_render_point(), grid.is_cursor_on_wide_char(), model.alt_screen().cursor_style(), padding_x, adjusted_grid_origin, self.grid_render_params.warp_theme.cursor().into(), ctx, self.terminal_view_id, self.cursor_hint_text.as_mut(), app, ); } record_trace_event!("alt_screen_element:paint:cursor_rendered"); self.render_selections( &self.grid_render_params.size_info, adjusted_grid_origin, ctx, ); self.render_participant_selections( &self.grid_render_params.size_info, adjusted_grid_origin, ctx, app, ); if let Some(cli_subagent_view) = &mut self.cli_subagent_view { ctx.scene.start_layer(ClipBounds::ActiveLayer); let size = cli_subagent_view .size() .expect("Subagent output was laid out already."); cli_subagent_view.paint( vec2f( self.bounds.expect("bounds set during paint.").max_x() - CLI_SUBAGENT_HORIZONTAL_MARGIN - size.x(), self.bounds.expect("bounds set during paint.").max_y() - CLI_SUBAGENT_VERTICAL_MARGIN - size.y(), ), ctx, app, ); ctx.scene.stop_layer(); } record_trace_event!("alt_screen_element:paint:selection_rendered"); end_trace!(); } fn size(&self) -> Option { self.size } fn dispatch_event( &mut self, event: &DispatchedEvent, ctx: &mut EventContext, app: &AppContext, ) -> bool { if let Some(cli_subagent_view) = &mut self.cli_subagent_view { if cli_subagent_view.dispatch_event(event, ctx, app) { return true; } } let bounds = self .bounds .expect("Bounds should be set before event dispatching"); let in_bounds = event.raw_event().in_bounds(bounds); let vertical_scroll_pixels = self.vertical_scroll_pixels(); // Helper function to convert a global (window-space) position to a // local (element-space) one. let to_local = |position| position - bounds.origin() + vertical_scroll_pixels; let z_index = self.z_index().expect("Z-index should exist."); let Some(event_at_z_index) = event.at_z_index(z_index, ctx) else { // Only proceed if there's a relevant event at this z-index. return false; }; match event_at_z_index { Event::KeyDown { keystroke, chars, details, is_composing, } => { if !self.is_terminal_focused || (*is_composing && !handle_keystroke_despite_composing(keystroke)) { return false; } if let Some(escape_sequence) = (KeystrokeWithDetails { keystroke, key_without_modifiers: details.key_without_modifiers.as_deref(), chars: Some(chars.as_str()), }) .to_escape_sequence(self.model.lock().deref()) { ctx.dispatch_typed_action(TerminalAction::ControlSequence(escape_sequence)); return true; } self.key_down(chars, ctx) } Event::ScrollWheel { position, delta, precise, modifiers: ModifiersState { ctrl: false, .. }, } if in_bounds => self.on_scroll(to_local(*position), *delta, *precise, ctx, app), Event::LeftMouseDown { position, click_count, modifiers, .. } if in_bounds => self.left_mouse_down( MouseState::new(MouseButton::Left, MouseAction::Pressed, *modifiers), to_local(*position), *click_count, ctx, app, ), Event::RightMouseDown { position, cmd, shift, .. } if in_bounds => self.right_mouse_down( MouseState::new( MouseButton::Right, MouseAction::Pressed, ModifiersState { alt: false, cmd: *cmd, shift: *shift, ctrl: false, func: false, }, ), to_local(*position), ctx, app, ), Event::LeftMouseUp { position, modifiers, .. } if in_bounds => self.mouse_up( MouseState::new(MouseButton::Left, MouseAction::Released, *modifiers), to_local(*position), ctx, app, ), Event::LeftMouseDragged { position, modifiers, .. } if in_bounds => self.mouse_dragged( MouseState::new(MouseButton::LeftDrag, MouseAction::Pressed, *modifiers), to_local(*position), ctx, app, ), Event::MouseMoved { position, is_synthetic, .. } => { if in_bounds { self.mouse_moved(to_local(*position), *is_synthetic, app, ctx) } else { self.mouse_out(ctx) } } Event::MiddleMouseDown { position, .. } if in_bounds => { self.middle_mouse_down(to_local(*position), ctx) } Event::TypedCharacters { chars } => self.typed_characters(chars, ctx), Event::DragAndDropFiles { paths, .. } if in_bounds => { self.drag_and_drop_file(paths, ctx) } Event::SetMarkedText { marked_text, selected_range, } => self.set_marked_text(marked_text, selected_range, ctx), Event::ClearMarkedText => self.clear_marked_text(ctx), Event::ModifierKeyChanged { key_code, state } => { if self.is_terminal_focused { let is_press = matches!(state, KeyState::Pressed); if let Some(escape_sequence) = maybe_kitty_keyboard_escape_sequence( self.model.lock().deref(), key_code, is_press, ) { ctx.dispatch_typed_action(TerminalAction::ControlSequence(escape_sequence)); return true; } self.maybe_handle_voice_toggle(key_code, state, ctx) } else { false } } _ => false, } } fn origin(&self) -> Option { self.origin } } impl NewScrollableElement for AltScreenElement { fn axis(&self) -> ScrollableAxis { ScrollableAxis::Vertical } fn scroll_data(&self, _axis: Axis, app: &AppContext) -> Option { ScrollableElement::scroll_data(self, app) } fn scroll(&mut self, delta: Pixels, _axis: Axis, ctx: &mut EventContext) { ScrollableElement::scroll(self, delta, ctx) } fn axis_should_handle_scroll_wheel(&self, axis: Axis) -> bool { matches!(axis, Axis::Horizontal) } } impl ScrollableElement for AltScreenElement { fn scroll_data(&self, _app: &AppContext) -> Option { let line_height = self.line_height(); let visible_lines = self.visible_lines.expect("should be set after layout"); let total_lines = self.total_lines(); // If the number of visible_lines is within a rounding error of total // lines, just set them to be exactly equal so the scrollable element // knows not to render a scrollbar in that case. Otherwise, we risk // seeing spurious scrollbars because of our issues with f32 rounding // errors. let visible_px = if heights_approx_eq(visible_lines, total_lines) { total_lines.to_pixels(line_height) } else { visible_lines.to_pixels(line_height) }; Some(ScrollData { scroll_start: self.scroll_top.to_pixels(line_height), visible_px, total_size: total_lines.to_pixels(line_height), }) } fn scroll(&mut self, delta: Pixels, ctx: &mut EventContext) { self.scroll_top = (self.scroll_top - delta.to_lines(self.line_height())) .max(Lines::zero()) .min(self.max_scroll_top.unwrap()); ctx.dispatch_typed_action(TerminalAction::SharedSessionViewerAltScroll { new_scroll_top: self.scroll_top, }); } }