2023-12-20 13:31:10 -07:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2023-12-21 15:09:18 -07:00
|
|
|
use alacritty_terminal::{
|
2025-10-29 17:37:02 +02:00
|
|
|
grid::Dimensions,
|
2023-12-21 22:21:01 -07:00
|
|
|
index::{Column as TermColumn, Point as TermPoint, Side as TermSide},
|
2023-12-21 15:09:18 -07:00
|
|
|
selection::{Selection, SelectionType},
|
2025-09-10 14:33:46 +02:00
|
|
|
term::{TermMode, cell::Flags},
|
2024-08-23 15:01:47 +03:00
|
|
|
vte::ansi::{CursorShape, NamedColor},
|
2023-12-21 15:09:18 -07:00
|
|
|
};
|
2023-12-20 13:31:10 -07:00
|
|
|
use cosmic::{
|
2025-09-10 14:33:46 +02:00
|
|
|
Renderer,
|
|
|
|
|
cosmic_theme::palette::{WithAlpha, blend::Compose},
|
2026-04-08 15:30:10 -06:00
|
|
|
iced::core::{
|
2026-07-29 21:51:03 +05:30
|
|
|
Border, Shell,
|
|
|
|
|
border::Radius,
|
2023-12-20 13:31:10 -07:00
|
|
|
clipboard::Clipboard,
|
2025-04-27 12:54:46 +09:00
|
|
|
input_method::{self, InputMethod},
|
2024-02-05 14:18:19 -05:00
|
|
|
keyboard::key::Named,
|
2023-12-20 13:31:10 -07:00
|
|
|
layout::{self, Layout},
|
2024-01-11 11:33:39 -07:00
|
|
|
renderer::{self, Quad, Renderer as _},
|
|
|
|
|
text::Renderer as _,
|
2024-01-11 13:20:16 -07:00
|
|
|
widget::{
|
2025-09-10 14:33:46 +02:00
|
|
|
self, Id, Widget,
|
2024-10-22 12:31:02 -06:00
|
|
|
operation::{self, Operation},
|
2025-09-10 14:33:46 +02:00
|
|
|
tree,
|
2024-01-11 13:20:16 -07:00
|
|
|
},
|
2023-12-20 13:31:10 -07:00
|
|
|
},
|
2026-04-08 15:30:10 -06:00
|
|
|
iced::{
|
|
|
|
|
Color, Element, Length, Padding, Point, Rectangle, Size, Vector,
|
|
|
|
|
advanced::graphics::text::Raw,
|
2026-04-10 18:09:25 +02:00
|
|
|
event::Event,
|
2026-04-08 15:30:10 -06:00
|
|
|
keyboard::{Event as KeyEvent, Key, Modifiers},
|
|
|
|
|
mouse::{self, Button, Event as MouseEvent, ScrollDelta},
|
|
|
|
|
},
|
2024-01-11 11:33:39 -07:00
|
|
|
theme::Theme,
|
2023-12-20 13:31:10 -07:00
|
|
|
};
|
2026-04-08 15:30:10 -06:00
|
|
|
use cosmic::{iced::core::SmolStr, widget::menu::key_bind::KeyBind};
|
2024-01-02 12:21:46 -07:00
|
|
|
use cosmic_text::LayoutGlyph;
|
2024-01-11 23:22:39 +03:00
|
|
|
use indexmap::IndexSet;
|
2023-12-20 13:31:10 -07:00
|
|
|
use std::{
|
2024-08-22 16:08:38 +03:00
|
|
|
array,
|
2023-12-20 13:31:10 -07:00
|
|
|
cell::Cell,
|
|
|
|
|
cmp,
|
2024-02-14 08:42:54 +01:00
|
|
|
collections::HashMap,
|
2023-12-20 13:31:10 -07:00
|
|
|
sync::Mutex,
|
|
|
|
|
time::{Duration, Instant},
|
|
|
|
|
};
|
|
|
|
|
|
yoda: elements de bloc U+2580-259F peints en quads natifs
Le glyphe de police d'un bloc ne couvre que ~1 em alors que la cellule
fait line_height (~1.4 em) : rangées de blocs trouees par l'interligne,
bords creneles, rendu flou aux echelles fractionnees.
Desormais le caractere est remplace par une espace (aucun glyphe) et le
remplissage voyage dans Metadata jusqu'au tracé, ou terminal_box le
peint en quad GPU couvrant exactement la fraction de cellule (huitiemes,
quadrants, trames alpha). Consecutive cells de meme metadata fusionnent
en un seul quad : jointif par construction, aligne sur la grille, net a
toute echelle. C'est l'approche de kitty/WezTerm. Tout TUI a blocs
profite (bannieres, jauges htop/btop, barres de progression).
Couverture totale de la plage + geometrie bornee, 2 tests.
2026-09-04 16:53:35 +02:00
|
|
|
use crate::{
|
|
|
|
|
Action, Terminal, TerminalScroll,
|
|
|
|
|
menu::MenuState,
|
|
|
|
|
terminal::{BlockFill, Metadata},
|
|
|
|
|
};
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2025-11-07 06:47:42 +02:00
|
|
|
const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100);
|
|
|
|
|
|
|
|
|
|
/// Drives repeated drag updates while the pointer is outside the widget.
|
|
|
|
|
struct DragAutoscroll {
|
|
|
|
|
active: bool,
|
|
|
|
|
pointer: Option<Point>,
|
|
|
|
|
last_tick: Instant,
|
|
|
|
|
interval: Duration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DragAutoscroll {
|
|
|
|
|
fn new(interval: Duration) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
active: false,
|
|
|
|
|
pointer: None,
|
|
|
|
|
last_tick: Instant::now(),
|
|
|
|
|
interval,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn start(&mut self, pointer: Point) {
|
|
|
|
|
self.pointer = Some(pointer);
|
|
|
|
|
if !self.active {
|
|
|
|
|
self.active = true;
|
|
|
|
|
self.last_tick = Instant::now();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_pointer(&mut self, pointer: Point) {
|
|
|
|
|
if self.active {
|
|
|
|
|
self.pointer = Some(pointer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stop(&mut self) {
|
|
|
|
|
self.active = false;
|
|
|
|
|
self.pointer = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_active(&self) -> bool {
|
|
|
|
|
self.active
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the stored pointer when the next tick is due.
|
|
|
|
|
fn next_due(&mut self) -> Option<(Point, f32)> {
|
|
|
|
|
if self.active && self.last_tick.elapsed() >= self.interval {
|
|
|
|
|
let elapsed = self.last_tick.elapsed();
|
|
|
|
|
self.last_tick = Instant::now();
|
|
|
|
|
let ticks = (elapsed.as_secs_f32() / self.interval.as_secs_f32()).max(1.0);
|
|
|
|
|
self.pointer.map(|pointer| (pointer, ticks))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
pub struct TerminalBox<'a, Message> {
|
|
|
|
|
terminal: &'a Mutex<Terminal>,
|
2024-01-11 13:20:16 -07:00
|
|
|
id: Option<Id>,
|
2024-02-12 12:36:30 -07:00
|
|
|
border: Border,
|
2023-12-20 13:31:10 -07:00
|
|
|
padding: Padding,
|
2024-11-06 18:44:38 +01:00
|
|
|
show_headerbar: bool,
|
2026-04-06 18:38:10 -06:00
|
|
|
pane_border_radius: Option<Radius>,
|
2023-12-20 13:31:10 -07:00
|
|
|
click_timing: Duration,
|
2026-09-08 13:49:17 -06:00
|
|
|
context_menu_open: bool,
|
2025-10-17 07:56:43 +02:00
|
|
|
on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>,
|
2024-01-22 16:35:23 +01:00
|
|
|
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
|
2024-02-12 12:36:30 -07:00
|
|
|
opacity: Option<f32>,
|
2024-01-22 16:35:23 +01:00
|
|
|
mouse_inside_boundary: Option<bool>,
|
2024-01-21 21:13:13 +01:00
|
|
|
on_middle_click: Option<Box<dyn Fn() -> Message + 'a>>,
|
2024-11-17 01:47:50 +01:00
|
|
|
on_open_hyperlink: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
2025-01-21 17:07:45 +01:00
|
|
|
on_window_focused: Option<Box<dyn Fn() -> Message + 'a>>,
|
|
|
|
|
on_window_unfocused: Option<Box<dyn Fn() -> Message + 'a>>,
|
2025-12-29 20:42:52 -08:00
|
|
|
key_binds: &'a HashMap<KeyBind, Action>,
|
2025-10-06 15:02:34 -04:00
|
|
|
sharp_corners: bool,
|
2025-10-24 17:56:10 -04:00
|
|
|
disabled: bool,
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a, Message> TerminalBox<'a, Message>
|
|
|
|
|
where
|
|
|
|
|
Message: Clone,
|
|
|
|
|
{
|
2025-12-29 20:42:52 -08:00
|
|
|
pub fn new(terminal: &'a Mutex<Terminal>, key_binds: &'a HashMap<KeyBind, Action>) -> Self {
|
2023-12-20 13:31:10 -07:00
|
|
|
Self {
|
|
|
|
|
terminal,
|
2024-01-11 13:20:16 -07:00
|
|
|
id: None,
|
2024-02-12 12:36:30 -07:00
|
|
|
border: Border::default(),
|
2023-12-20 13:31:10 -07:00
|
|
|
padding: Padding::new(0.0),
|
2024-11-06 18:44:38 +01:00
|
|
|
show_headerbar: true,
|
2026-04-06 18:38:10 -06:00
|
|
|
pane_border_radius: None,
|
2023-12-20 13:31:10 -07:00
|
|
|
click_timing: Duration::from_millis(500),
|
2026-09-08 13:49:17 -06:00
|
|
|
context_menu_open: false,
|
2023-12-20 13:31:10 -07:00
|
|
|
on_context_menu: None,
|
2024-01-22 16:35:23 +01:00
|
|
|
on_mouse_enter: None,
|
2024-02-12 12:36:30 -07:00
|
|
|
opacity: None,
|
2024-01-22 16:35:23 +01:00
|
|
|
mouse_inside_boundary: None,
|
2024-01-21 21:13:13 +01:00
|
|
|
on_middle_click: None,
|
2025-12-29 20:42:52 -08:00
|
|
|
key_binds,
|
2024-11-17 01:47:50 +01:00
|
|
|
on_open_hyperlink: None,
|
2025-01-21 17:07:45 +01:00
|
|
|
on_window_focused: None,
|
|
|
|
|
on_window_unfocused: None,
|
2025-10-06 15:02:34 -04:00
|
|
|
sharp_corners: false,
|
2025-10-24 17:56:10 -04:00
|
|
|
disabled: false,
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 13:20:16 -07:00
|
|
|
pub fn id(mut self, id: Id) -> Self {
|
|
|
|
|
self.id = Some(id);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-12 12:36:30 -07:00
|
|
|
pub fn border<B: Into<Border>>(mut self, border: B) -> Self {
|
|
|
|
|
self.border = border.into();
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
|
|
|
|
self.padding = padding.into();
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-06 18:44:38 +01:00
|
|
|
pub fn show_headerbar(mut self, show_headerbar: bool) -> Self {
|
|
|
|
|
self.show_headerbar = show_headerbar;
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 18:38:10 -06:00
|
|
|
pub fn pane_border_radius(mut self, pane_border_radius: Option<Radius>) -> Self {
|
|
|
|
|
self.pane_border_radius = pane_border_radius;
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
pub fn click_timing(mut self, click_timing: Duration) -> Self {
|
|
|
|
|
self.click_timing = click_timing;
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 13:49:17 -06:00
|
|
|
pub fn context_menu_open(mut self, open: bool) -> Self {
|
|
|
|
|
self.context_menu_open = open;
|
2023-12-20 13:31:10 -07:00
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-17 07:56:43 +02:00
|
|
|
pub fn on_context_menu(
|
|
|
|
|
mut self,
|
|
|
|
|
on_context_menu: impl Fn(Option<MenuState>) -> Message + 'a,
|
|
|
|
|
) -> Self {
|
2023-12-20 13:31:10 -07:00
|
|
|
self.on_context_menu = Some(Box::new(on_context_menu));
|
|
|
|
|
self
|
|
|
|
|
}
|
2024-01-21 21:13:13 +01:00
|
|
|
|
2024-01-22 16:35:23 +01:00
|
|
|
pub fn on_mouse_enter(mut self, on_mouse_enter: impl Fn() -> Message + 'a) -> Self {
|
|
|
|
|
self.on_mouse_enter = Some(Box::new(on_mouse_enter));
|
|
|
|
|
self
|
|
|
|
|
}
|
2024-02-02 10:35:45 +01:00
|
|
|
|
2024-01-21 21:13:13 +01:00
|
|
|
pub fn on_middle_click(mut self, on_middle_click: impl Fn() -> Message + 'a) -> Self {
|
|
|
|
|
self.on_middle_click = Some(Box::new(on_middle_click));
|
|
|
|
|
self
|
|
|
|
|
}
|
2024-02-21 09:26:09 +01:00
|
|
|
|
2024-02-12 12:36:30 -07:00
|
|
|
pub fn opacity(mut self, opacity: f32) -> Self {
|
|
|
|
|
self.opacity = Some(opacity);
|
|
|
|
|
self
|
|
|
|
|
}
|
2024-11-17 01:47:50 +01:00
|
|
|
|
2025-10-06 15:02:34 -04:00
|
|
|
pub fn sharp_corners(mut self, sharp_corners: bool) -> Self {
|
|
|
|
|
self.sharp_corners = sharp_corners;
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-17 01:47:50 +01:00
|
|
|
pub fn on_open_hyperlink(
|
|
|
|
|
mut self,
|
|
|
|
|
on_open_hyperlink: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
self.on_open_hyperlink = on_open_hyperlink;
|
|
|
|
|
self
|
|
|
|
|
}
|
2025-01-21 17:07:45 +01:00
|
|
|
|
|
|
|
|
pub fn on_window_focused(mut self, on_window_focused: impl Fn() -> Message + 'a) -> Self {
|
|
|
|
|
self.on_window_focused = Some(Box::new(on_window_focused));
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn on_window_unfocused(mut self, on_window_unfocused: impl Fn() -> Message + 'a) -> Self {
|
|
|
|
|
self.on_window_unfocused = Some(Box::new(on_window_unfocused));
|
|
|
|
|
self
|
|
|
|
|
}
|
2025-10-24 17:56:10 -04:00
|
|
|
|
|
|
|
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
|
|
|
self.disabled = disabled;
|
|
|
|
|
self
|
|
|
|
|
}
|
2025-04-27 12:54:46 +09:00
|
|
|
|
2025-05-02 07:01:55 +09:00
|
|
|
fn input_method<'b>(
|
|
|
|
|
&self,
|
|
|
|
|
state: &'b State,
|
|
|
|
|
layout: Layout<'_>,
|
|
|
|
|
terminal: &std::sync::MutexGuard<'_, Terminal>,
|
|
|
|
|
) -> InputMethod<&'b str> {
|
2025-04-27 12:54:46 +09:00
|
|
|
if !state.is_focused {
|
|
|
|
|
return InputMethod::Disabled;
|
|
|
|
|
}
|
2026-09-08 13:49:17 -06:00
|
|
|
if self.context_menu_open {
|
2026-04-11 10:23:51 +09:00
|
|
|
return InputMethod::Disabled;
|
|
|
|
|
}
|
2025-04-27 12:54:46 +09:00
|
|
|
|
2025-05-02 07:01:55 +09:00
|
|
|
let view_position = layout.position() + [self.padding.left, self.padding.top].into();
|
|
|
|
|
|
|
|
|
|
// Draw cursor
|
|
|
|
|
let cursor = terminal.term.lock().renderable_content().cursor;
|
2025-05-04 11:21:43 +09:00
|
|
|
let mut col = cursor.point.column.0;
|
2025-05-02 07:01:55 +09:00
|
|
|
let line = cursor.point.line.0;
|
|
|
|
|
let width = terminal.size().cell_width;
|
2026-02-26 01:05:30 +09:00
|
|
|
let mut cursor_position = Vector::<f32>::ZERO;
|
|
|
|
|
let mut line_height = 0.0;
|
2025-05-04 11:21:43 +09:00
|
|
|
terminal.with_buffer(|buffer| {
|
|
|
|
|
let layout = buffer.layout_runs().nth(line as usize).unwrap();
|
|
|
|
|
for glyph in layout.glyphs {
|
2026-02-26 01:05:30 +09:00
|
|
|
cursor_position.x += glyph.w;
|
2025-05-04 11:21:43 +09:00
|
|
|
let ch_width = if glyph.w > width { 2 } else { 1 };
|
|
|
|
|
if ch_width > col {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
col -= ch_width;
|
|
|
|
|
}
|
2026-02-26 01:05:30 +09:00
|
|
|
cursor_position.y = layout.line_top;
|
|
|
|
|
line_height = layout.line_height;
|
2025-05-04 11:21:43 +09:00
|
|
|
});
|
|
|
|
|
|
2025-04-27 12:54:46 +09:00
|
|
|
InputMethod::Enabled {
|
2026-02-26 01:05:30 +09:00
|
|
|
cursor: Rectangle::new(view_position + cursor_position, Size::new(1.0, line_height)),
|
2025-04-27 12:54:46 +09:00
|
|
|
purpose: input_method::Purpose::Normal,
|
|
|
|
|
preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:42:52 -08:00
|
|
|
pub fn terminal_box<'a, Message>(
|
|
|
|
|
terminal: &'a Mutex<Terminal>,
|
|
|
|
|
key_binds: &'a HashMap<KeyBind, Action>,
|
|
|
|
|
) -> TerminalBox<'a, Message>
|
2023-12-20 13:31:10 -07:00
|
|
|
where
|
|
|
|
|
Message: Clone,
|
|
|
|
|
{
|
2025-12-29 20:42:52 -08:00
|
|
|
TerminalBox::new(terminal, key_binds)
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
2024-02-05 14:18:19 -05:00
|
|
|
impl<'a, Message> Widget<Message, cosmic::Theme, Renderer> for TerminalBox<'a, Message>
|
2023-12-20 13:31:10 -07:00
|
|
|
where
|
|
|
|
|
Message: Clone,
|
|
|
|
|
{
|
|
|
|
|
fn tag(&self) -> tree::Tag {
|
|
|
|
|
tree::Tag::of::<State>()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn state(&self) -> tree::State {
|
|
|
|
|
tree::State::new(State::new())
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-05 14:18:19 -05:00
|
|
|
fn size(&self) -> Size<Length> {
|
|
|
|
|
Size::new(Length::Fill, Length::Fill)
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn layout(
|
2026-03-18 12:21:57 -04:00
|
|
|
&mut self,
|
2023-12-20 13:31:10 -07:00
|
|
|
_tree: &mut widget::Tree,
|
|
|
|
|
_renderer: &Renderer,
|
|
|
|
|
limits: &layout::Limits,
|
|
|
|
|
) -> layout::Node {
|
|
|
|
|
let limits = limits.width(Length::Fill).height(Length::Fill);
|
|
|
|
|
|
|
|
|
|
let mut terminal = self.terminal.lock().unwrap();
|
2024-01-04 14:25:31 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
//TODO: set size?
|
2024-01-04 14:25:31 -07:00
|
|
|
|
|
|
|
|
// Update if needed
|
|
|
|
|
if terminal.needs_update {
|
|
|
|
|
terminal.update();
|
|
|
|
|
terminal.needs_update = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate layout lines
|
2023-12-20 13:31:10 -07:00
|
|
|
terminal.with_buffer(|buffer| {
|
|
|
|
|
let mut layout_lines = 0;
|
2024-04-14 21:09:14 +01:00
|
|
|
for line in &buffer.lines {
|
2024-04-15 21:15:30 +01:00
|
|
|
if let Some(layout) = line.layout_opt() {
|
|
|
|
|
layout_lines += layout.len()
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let height = layout_lines as f32 * buffer.metrics().line_height;
|
|
|
|
|
let size = Size::new(limits.max().width, height);
|
|
|
|
|
|
2024-02-05 14:18:19 -05:00
|
|
|
layout::Node::new(limits.resolve(Length::Fill, Length::Fill, size))
|
2023-12-20 13:31:10 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 13:20:16 -07:00
|
|
|
fn operate(
|
2026-03-18 12:21:57 -04:00
|
|
|
&mut self,
|
2024-01-11 13:20:16 -07:00
|
|
|
tree: &mut widget::Tree,
|
2026-03-18 12:21:57 -04:00
|
|
|
layout: Layout<'_>,
|
2024-01-11 13:20:16 -07:00
|
|
|
_renderer: &Renderer,
|
2024-10-22 12:31:02 -06:00
|
|
|
operation: &mut dyn Operation,
|
2024-01-11 13:20:16 -07:00
|
|
|
) {
|
|
|
|
|
let state = tree.state.downcast_mut::<State>();
|
|
|
|
|
|
2026-03-18 12:21:57 -04:00
|
|
|
operation.focusable(self.id.as_ref(), layout.bounds(), state);
|
2024-01-11 13:20:16 -07:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
fn mouse_interaction(
|
|
|
|
|
&self,
|
|
|
|
|
tree: &widget::Tree,
|
|
|
|
|
layout: Layout<'_>,
|
|
|
|
|
cursor_position: mouse::Cursor,
|
|
|
|
|
_viewport: &Rectangle,
|
|
|
|
|
_renderer: &Renderer,
|
|
|
|
|
) -> mouse::Interaction {
|
2026-03-18 12:21:57 -04:00
|
|
|
if self.disabled {
|
|
|
|
|
return mouse::Interaction::default();
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
let state = tree.state.downcast_ref::<State>();
|
|
|
|
|
|
2024-02-05 15:59:07 +01:00
|
|
|
if let Some(Dragging::Scrollbar { .. }) = &state.dragging {
|
|
|
|
|
return mouse::Interaction::Idle;
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
|
|
|
|
let terminal = self.terminal.lock().unwrap();
|
|
|
|
|
let buffer_size = terminal.with_buffer(|buffer| buffer.size());
|
|
|
|
|
|
2023-12-20 19:54:18 -07:00
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
2024-06-12 09:50:07 -06:00
|
|
|
if x >= 0.0
|
|
|
|
|
&& x < buffer_size.0.unwrap_or(0.0)
|
|
|
|
|
&& y >= 0.0
|
|
|
|
|
&& y < buffer_size.1.unwrap_or(0.0)
|
|
|
|
|
{
|
2025-10-10 10:36:10 +02:00
|
|
|
if state.modifiers.contains(Modifiers::CTRL) {
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
|
|
|
|
|
|
|
|
|
let location = terminal
|
|
|
|
|
.viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize)));
|
2025-12-30 13:51:27 -08:00
|
|
|
if get_hyperlink(&terminal, location).is_some() {
|
2025-10-10 10:36:10 +02:00
|
|
|
return mouse::Interaction::Pointer;
|
|
|
|
|
}
|
2024-11-17 01:47:50 +01:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
return mouse::Interaction::Text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 12:21:57 -04:00
|
|
|
mouse::Interaction::default()
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn draw(
|
|
|
|
|
&self,
|
|
|
|
|
tree: &widget::Tree,
|
|
|
|
|
renderer: &mut Renderer,
|
2024-01-11 11:33:39 -07:00
|
|
|
theme: &Theme,
|
2023-12-21 09:53:31 -07:00
|
|
|
_style: &renderer::Style,
|
2023-12-20 13:31:10 -07:00
|
|
|
layout: Layout<'_>,
|
2024-01-11 11:33:39 -07:00
|
|
|
cursor_position: mouse::Cursor,
|
2023-12-20 13:31:10 -07:00
|
|
|
viewport: &Rectangle,
|
|
|
|
|
) {
|
|
|
|
|
let instant = Instant::now();
|
|
|
|
|
|
|
|
|
|
let state = tree.state.downcast_ref::<State>();
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
let cosmic_theme = theme.cosmic();
|
2026-04-06 18:38:10 -06:00
|
|
|
let corner_radius = if let Some(r) = self.pane_border_radius {
|
|
|
|
|
r.into()
|
|
|
|
|
} else if self.sharp_corners {
|
|
|
|
|
// matches the corners to the window border
|
2025-10-06 15:02:34 -04:00
|
|
|
cosmic_theme.radius_0()
|
|
|
|
|
} else {
|
2026-04-06 18:38:10 -06:00
|
|
|
cosmic_theme
|
|
|
|
|
.radius_s()
|
|
|
|
|
.map(|x| if x < 4.0 { x - 1.0 } else { x + 3.0 })
|
|
|
|
|
};
|
|
|
|
|
// When there's a headerbar and no pane border, only round the bottom corners
|
|
|
|
|
let border_radius: Radius = if self.show_headerbar && self.pane_border_radius.is_none() {
|
|
|
|
|
[0.0, 0.0, corner_radius[2], corner_radius[3]].into()
|
|
|
|
|
} else {
|
|
|
|
|
corner_radius.into()
|
|
|
|
|
};
|
2024-04-15 20:44:25 +01:00
|
|
|
let scrollbar_w = f32::from(cosmic_theme.spacing.space_xxs);
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2024-02-05 15:59:07 +01:00
|
|
|
let view_position = layout.position() + [self.padding.left, self.padding.top].into();
|
2023-12-20 13:31:10 -07:00
|
|
|
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32)
|
2026-03-18 12:21:57 -04:00
|
|
|
- self.padding.x() as i32
|
2023-12-20 13:31:10 -07:00
|
|
|
- scrollbar_w as i32;
|
2023-12-20 14:57:44 -07:00
|
|
|
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32)
|
2026-03-18 12:21:57 -04:00
|
|
|
- self.padding.y() as i32;
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
if view_w <= 0 || view_h <= 0 {
|
2026-03-19 00:10:26 -06:00
|
|
|
// Pane too small for content, but still fill background
|
|
|
|
|
let terminal = self.terminal.lock().unwrap();
|
|
|
|
|
let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
|
|
|
|
|
renderer.fill_quad(
|
|
|
|
|
Quad {
|
|
|
|
|
bounds: layout.bounds(),
|
|
|
|
|
border: Border {
|
2026-04-06 18:38:10 -06:00
|
|
|
radius: border_radius,
|
2026-03-19 00:10:26 -06:00
|
|
|
width: self.border.width,
|
|
|
|
|
color: self.border.color,
|
|
|
|
|
},
|
2026-03-24 13:03:28 -06:00
|
|
|
snap: true,
|
2026-03-19 00:10:26 -06:00
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
Color::from_rgba(
|
2026-07-09 14:32:41 -06:00
|
|
|
f32::from(meta.bg.r()) / 255.0,
|
|
|
|
|
f32::from(meta.bg.g()) / 255.0,
|
|
|
|
|
f32::from(meta.bg.b()) / 255.0,
|
2026-03-19 00:10:26 -06:00
|
|
|
match self.opacity {
|
|
|
|
|
Some(opacity) => opacity,
|
2026-07-09 14:32:41 -06:00
|
|
|
None => f32::from(meta.bg.a()) / 255.0,
|
2026-03-19 00:10:26 -06:00
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
2023-12-20 13:31:10 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 14:25:31 -07:00
|
|
|
let mut terminal = self.terminal.lock().unwrap();
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
// Ensure terminal is the right size
|
2023-12-20 19:54:18 -07:00
|
|
|
terminal.resize(view_w as u32, view_h as u32);
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2024-01-04 14:25:31 -07:00
|
|
|
// Update if needed
|
|
|
|
|
if terminal.needs_update {
|
|
|
|
|
terminal.update();
|
|
|
|
|
terminal.needs_update = false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
// Render default background
|
|
|
|
|
{
|
2024-01-11 23:22:39 +03:00
|
|
|
let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
|
2023-12-20 13:31:10 -07:00
|
|
|
renderer.fill_quad(
|
|
|
|
|
Quad {
|
2024-02-12 12:36:30 -07:00
|
|
|
bounds: layout.bounds(),
|
2024-11-05 13:21:05 +01:00
|
|
|
border: Border {
|
2026-04-06 18:38:10 -06:00
|
|
|
radius: border_radius,
|
2024-11-05 13:21:05 +01:00
|
|
|
width: self.border.width,
|
|
|
|
|
color: self.border.color,
|
|
|
|
|
},
|
2026-03-24 13:03:28 -06:00
|
|
|
snap: true,
|
2024-02-05 14:18:19 -05:00
|
|
|
..Default::default()
|
2023-12-20 13:31:10 -07:00
|
|
|
},
|
2026-03-18 12:21:57 -04:00
|
|
|
Color::from_rgba(
|
2026-07-09 14:32:41 -06:00
|
|
|
f32::from(meta.bg.r()) / 255.0,
|
|
|
|
|
f32::from(meta.bg.g()) / 255.0,
|
|
|
|
|
f32::from(meta.bg.b()) / 255.0,
|
2024-02-12 12:36:30 -07:00
|
|
|
match self.opacity {
|
|
|
|
|
Some(opacity) => opacity,
|
2026-07-09 14:32:41 -06:00
|
|
|
None => f32::from(meta.bg.a()) / 255.0,
|
2024-02-12 12:36:30 -07:00
|
|
|
},
|
2024-01-17 18:44:40 +01:00
|
|
|
),
|
2023-12-20 13:31:10 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render cell backgrounds that do not match default
|
|
|
|
|
terminal.with_buffer(|buffer| {
|
|
|
|
|
for run in buffer.layout_runs() {
|
2024-01-11 23:22:39 +03:00
|
|
|
struct BgRect<'a> {
|
2024-01-02 12:21:46 -07:00
|
|
|
default_metadata: usize,
|
|
|
|
|
metadata: usize,
|
2024-01-11 23:22:39 +03:00
|
|
|
glyph_font_size: f32,
|
2024-01-02 12:21:46 -07:00
|
|
|
start_x: f32,
|
|
|
|
|
end_x: f32,
|
|
|
|
|
line_height: f32,
|
|
|
|
|
line_top: f32,
|
|
|
|
|
view_position: Point,
|
2024-01-11 23:22:39 +03:00
|
|
|
metadata_set: &'a IndexSet<Metadata>,
|
2024-01-02 12:21:46 -07:00
|
|
|
}
|
|
|
|
|
|
2024-01-11 23:22:39 +03:00
|
|
|
impl<'a> BgRect<'a> {
|
2024-01-02 12:21:46 -07:00
|
|
|
fn update<Renderer: renderer::Renderer>(
|
|
|
|
|
&mut self,
|
|
|
|
|
glyph: &LayoutGlyph,
|
|
|
|
|
renderer: &mut Renderer,
|
2024-01-17 18:44:40 +01:00
|
|
|
is_focused: bool,
|
2024-01-02 12:21:46 -07:00
|
|
|
) {
|
|
|
|
|
if glyph.metadata == self.metadata {
|
|
|
|
|
self.end_x = glyph.x + glyph.w;
|
|
|
|
|
} else {
|
2024-01-17 18:44:40 +01:00
|
|
|
self.fill(renderer, is_focused);
|
2024-01-02 12:21:46 -07:00
|
|
|
self.metadata = glyph.metadata;
|
2024-01-11 23:22:39 +03:00
|
|
|
self.glyph_font_size = glyph.font_size;
|
2024-01-02 12:21:46 -07:00
|
|
|
self.start_x = glyph.x;
|
|
|
|
|
self.end_x = glyph.x + glyph.w;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-17 19:03:39 +01:00
|
|
|
fn fill<Renderer: renderer::Renderer>(
|
|
|
|
|
&mut self,
|
|
|
|
|
renderer: &mut Renderer,
|
2026-08-05 14:09:24 -06:00
|
|
|
_is_focused: bool,
|
2024-01-17 19:03:39 +01:00
|
|
|
) {
|
2024-01-11 23:22:39 +03:00
|
|
|
let cosmic_text_to_iced_color = |color: cosmic_text::Color| {
|
2026-03-18 12:21:57 -04:00
|
|
|
Color::from_rgba(
|
2024-04-15 20:44:25 +01:00
|
|
|
f32::from(color.r()) / 255.0,
|
|
|
|
|
f32::from(color.g()) / 255.0,
|
|
|
|
|
f32::from(color.b()) / 255.0,
|
|
|
|
|
f32::from(color.a()) / 255.0,
|
2024-01-11 23:22:39 +03:00
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
macro_rules! mk_pos_offset {
|
|
|
|
|
($x_offset:expr, $bottom_offset:expr) => {
|
2024-01-17 16:41:35 +01:00
|
|
|
Vector::new(
|
2026-07-29 15:49:10 -05:00
|
|
|
(self.start_x + $x_offset).floor(),
|
2024-01-17 16:41:35 +01:00
|
|
|
self.line_top + self.line_height - $bottom_offset,
|
|
|
|
|
)
|
2024-01-11 23:22:39 +03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macro_rules! mk_quad {
|
|
|
|
|
($pos_offset:expr, $style_line_height:expr, $width:expr) => {
|
|
|
|
|
Quad {
|
|
|
|
|
bounds: Rectangle::new(
|
2024-01-17 16:41:35 +01:00
|
|
|
self.view_position + $pos_offset,
|
|
|
|
|
Size::new($width, $style_line_height),
|
|
|
|
|
),
|
2024-02-05 14:18:19 -05:00
|
|
|
..Default::default()
|
2024-01-11 23:22:39 +03:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
($pos_offset:expr, $style_line_height:expr) => {
|
2026-07-29 15:49:10 -05:00
|
|
|
mk_quad!(
|
|
|
|
|
$pos_offset,
|
|
|
|
|
$style_line_height,
|
|
|
|
|
(self.end_x - self.start_x).ceil()
|
|
|
|
|
)
|
2024-01-11 23:22:39 +03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let metadata = &self.metadata_set[self.metadata];
|
2024-02-12 12:36:30 -07:00
|
|
|
if metadata.bg != self.metadata_set[self.default_metadata].bg {
|
|
|
|
|
renderer.fill_quad(
|
|
|
|
|
mk_quad!(mk_pos_offset!(0.0, self.line_height), self.line_height),
|
2026-07-09 14:32:41 -06:00
|
|
|
cosmic_text_to_iced_color(metadata.bg),
|
2024-02-12 12:36:30 -07:00
|
|
|
);
|
|
|
|
|
}
|
2024-01-11 23:22:39 +03:00
|
|
|
|
yoda: elements de bloc U+2580-259F peints en quads natifs
Le glyphe de police d'un bloc ne couvre que ~1 em alors que la cellule
fait line_height (~1.4 em) : rangées de blocs trouees par l'interligne,
bords creneles, rendu flou aux echelles fractionnees.
Desormais le caractere est remplace par une espace (aucun glyphe) et le
remplissage voyage dans Metadata jusqu'au tracé, ou terminal_box le
peint en quad GPU couvrant exactement la fraction de cellule (huitiemes,
quadrants, trames alpha). Consecutive cells de meme metadata fusionnent
en un seul quad : jointif par construction, aligne sur la grille, net a
toute echelle. C'est l'approche de kitty/WezTerm. Tout TUI a blocs
profite (bannieres, jauges htop/btop, barres de progression).
Couverture totale de la plage + geometrie bornee, 2 tests.
2026-09-04 16:53:35 +02:00
|
|
|
// Éléments de bloc peints nativement : fractions de la
|
|
|
|
|
// cellule (ou de la course fusionnée, à géométrie
|
|
|
|
|
// séparable), bords calés sur la grille — zéro glyphe,
|
|
|
|
|
// zéro interligne.
|
|
|
|
|
if let Some(block) = metadata.block {
|
|
|
|
|
let span_w = self.end_x - self.start_x;
|
|
|
|
|
let mut color = cosmic_text_to_iced_color(metadata.block_color);
|
|
|
|
|
let rects = match block {
|
|
|
|
|
BlockFill::Rects(rects) => rects,
|
|
|
|
|
BlockFill::Shade(quarters) => {
|
|
|
|
|
color.a *= f32::from(quarters) / 4.0;
|
|
|
|
|
[(0, 8, 0, 8), (8, 8, 8, 8)]
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
for (x0, x1, y0, y1) in rects {
|
|
|
|
|
if x0 >= x1 || y0 >= y1 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let left = self.start_x + span_w * f32::from(x0) / 8.0;
|
|
|
|
|
let right = self.start_x + span_w * f32::from(x1) / 8.0;
|
|
|
|
|
let top = self.line_top + self.line_height * f32::from(y0) / 8.0;
|
|
|
|
|
let bottom = self.line_top + self.line_height * f32::from(y1) / 8.0;
|
|
|
|
|
renderer.fill_quad(
|
|
|
|
|
Quad {
|
|
|
|
|
bounds: Rectangle::new(
|
|
|
|
|
self.view_position + Vector::new(left.floor(), top),
|
|
|
|
|
Size::new(right.ceil() - left.floor(), bottom - top),
|
|
|
|
|
),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
color,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 23:22:39 +03:00
|
|
|
if !metadata.flags.is_empty() {
|
2025-08-13 19:03:55 +01:00
|
|
|
let style_line_height = (self.glyph_font_size / 10.0).clamp(1.0, 16.0);
|
2024-01-11 23:22:39 +03:00
|
|
|
|
|
|
|
|
let line_color = cosmic_text_to_iced_color(metadata.underline_color);
|
|
|
|
|
|
|
|
|
|
if metadata.flags.contains(Flags::STRIKEOUT) {
|
|
|
|
|
let bottom_offset = (self.line_height - style_line_height) / 2.0;
|
|
|
|
|
let pos_offset = mk_pos_offset!(0.0, bottom_offset);
|
|
|
|
|
let underline_quad = mk_quad!(pos_offset, style_line_height);
|
|
|
|
|
renderer.fill_quad(underline_quad, line_color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if metadata.flags.contains(Flags::UNDERLINE) {
|
|
|
|
|
let bottom_offset = style_line_height * 2.0;
|
|
|
|
|
let pos_offset = mk_pos_offset!(0.0, bottom_offset);
|
|
|
|
|
let underline_quad = mk_quad!(pos_offset, style_line_height);
|
|
|
|
|
renderer.fill_quad(underline_quad, line_color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if metadata.flags.contains(Flags::DOUBLE_UNDERLINE) {
|
|
|
|
|
let style_line_height = style_line_height / 2.0;
|
|
|
|
|
let gap = style_line_height.max(1.5);
|
|
|
|
|
let bottom_offset = (style_line_height + gap) * 2.0;
|
|
|
|
|
|
|
|
|
|
let pos_offset1 = mk_pos_offset!(0.0, bottom_offset);
|
|
|
|
|
let underline1_quad = mk_quad!(pos_offset1, style_line_height);
|
|
|
|
|
|
2024-01-17 16:41:35 +01:00
|
|
|
let pos_offset2 = mk_pos_offset!(0.0, bottom_offset / 2.0);
|
2024-01-11 23:22:39 +03:00
|
|
|
let underline2_quad = mk_quad!(pos_offset2, style_line_height);
|
|
|
|
|
|
|
|
|
|
renderer.fill_quad(underline1_quad, line_color);
|
|
|
|
|
renderer.fill_quad(underline2_quad, line_color);
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-22 16:08:38 +03:00
|
|
|
// rects is a slice of (width, Option<y>), `None` means a gap.
|
2024-08-20 21:40:41 +03:00
|
|
|
let mut draw_repeated = |rects: &[(f32, Option<f32>)]| {
|
2024-01-11 23:22:39 +03:00
|
|
|
let full_width = self.end_x - self.start_x;
|
2024-08-22 16:08:38 +03:00
|
|
|
let pattern_len: f32 = rects.iter().map(|x| x.0).sum(); // total length of the pattern
|
|
|
|
|
let mut accu_width = 0.0;
|
2024-08-20 21:40:41 +03:00
|
|
|
let mut index = {
|
2024-08-22 16:08:38 +03:00
|
|
|
let in_pattern = self.start_x % pattern_len;
|
2024-08-20 21:40:41 +03:00
|
|
|
|
|
|
|
|
let mut sum = 0.0;
|
|
|
|
|
let mut index = 0;
|
|
|
|
|
for (i, rect) in rects.iter().enumerate() {
|
|
|
|
|
sum += rect.0;
|
|
|
|
|
if in_pattern < sum {
|
|
|
|
|
let width = sum - in_pattern;
|
|
|
|
|
if let Some(height) = rect.1 {
|
2024-08-22 16:08:38 +03:00
|
|
|
// draw first rect cropped to span
|
|
|
|
|
let pos_offset = mk_pos_offset!(accu_width, height);
|
2024-08-20 21:40:41 +03:00
|
|
|
let underline_quad =
|
|
|
|
|
mk_quad!(pos_offset, style_line_height, width);
|
|
|
|
|
renderer.fill_quad(underline_quad, line_color);
|
|
|
|
|
}
|
|
|
|
|
index = i + 1;
|
2024-08-22 16:08:38 +03:00
|
|
|
accu_width += width;
|
2024-08-20 21:40:41 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-22 16:08:38 +03:00
|
|
|
index // index of first full rect
|
2024-08-20 21:40:41 +03:00
|
|
|
};
|
2024-08-22 16:08:38 +03:00
|
|
|
while accu_width < full_width {
|
2024-08-20 21:40:41 +03:00
|
|
|
let (width, x) = rects[index % rects.len()];
|
2024-08-22 16:08:38 +03:00
|
|
|
let cropped_width = width.min(full_width - accu_width);
|
2024-08-20 21:40:41 +03:00
|
|
|
if let Some(height) = x {
|
2024-08-22 16:08:38 +03:00
|
|
|
let pos_offset = mk_pos_offset!(accu_width, height);
|
2024-08-20 21:40:41 +03:00
|
|
|
let underline_quad =
|
2024-08-22 16:08:38 +03:00
|
|
|
mk_quad!(pos_offset, style_line_height, cropped_width);
|
2024-08-20 21:40:41 +03:00
|
|
|
renderer.fill_quad(underline_quad, line_color);
|
|
|
|
|
}
|
2024-08-22 16:08:38 +03:00
|
|
|
accu_width += cropped_width;
|
2024-08-20 21:40:41 +03:00
|
|
|
index += 1;
|
2024-01-11 23:22:39 +03:00
|
|
|
}
|
2024-08-20 21:40:41 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if metadata.flags.contains(Flags::DOTTED_UNDERLINE) {
|
|
|
|
|
let bottom_offset = style_line_height * 2.0;
|
2024-08-22 16:08:38 +03:00
|
|
|
let dot = (2.0, Some(bottom_offset));
|
|
|
|
|
let gap = (2.0, None);
|
|
|
|
|
draw_repeated(&[dot, gap]);
|
2024-01-11 23:22:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if metadata.flags.contains(Flags::DASHED_UNDERLINE) {
|
|
|
|
|
let bottom_offset = style_line_height * 2.0;
|
2024-08-22 18:38:50 +03:00
|
|
|
let dash = (6.0, Some(bottom_offset));
|
2024-08-22 16:08:38 +03:00
|
|
|
let gap = (3.0, None);
|
2024-08-22 18:38:50 +03:00
|
|
|
draw_repeated(&[dash, gap]);
|
2024-01-11 23:22:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if metadata.flags.contains(Flags::UNDERCURL) {
|
|
|
|
|
let style_line_height = style_line_height.floor();
|
|
|
|
|
let bottom_offset = style_line_height * 1.5;
|
2024-08-22 16:08:38 +03:00
|
|
|
let pattern: [(f32, Option<f32>); 8] =
|
|
|
|
|
array::from_fn(|i| match i {
|
|
|
|
|
3..=5 => (1.0, Some(bottom_offset + style_line_height)),
|
|
|
|
|
2 | 6 => (
|
|
|
|
|
1.0,
|
|
|
|
|
Some(bottom_offset + 2.0 * style_line_height / 3.0),
|
|
|
|
|
),
|
|
|
|
|
1 | 7 => (
|
|
|
|
|
1.0,
|
|
|
|
|
Some(bottom_offset + 1.0 * style_line_height / 3.0),
|
|
|
|
|
),
|
2024-08-22 18:38:50 +03:00
|
|
|
0 => (1.0, Some(bottom_offset)),
|
|
|
|
|
_ => unreachable!(),
|
2024-08-22 16:08:38 +03:00
|
|
|
});
|
2024-08-20 21:40:41 +03:00
|
|
|
draw_repeated(&pattern)
|
2024-01-11 23:22:39 +03:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-02 12:21:46 -07:00
|
|
|
|
|
|
|
|
let default_metadata = terminal.default_attrs().metadata;
|
2024-01-11 23:22:39 +03:00
|
|
|
let metadata_set = &terminal.metadata_set;
|
2024-01-02 12:21:46 -07:00
|
|
|
let mut bg_rect = BgRect {
|
|
|
|
|
default_metadata,
|
|
|
|
|
metadata: default_metadata,
|
2024-01-11 23:22:39 +03:00
|
|
|
glyph_font_size: 0.0,
|
2024-01-02 12:21:46 -07:00
|
|
|
start_x: 0.0,
|
|
|
|
|
end_x: 0.0,
|
|
|
|
|
line_height: buffer.metrics().line_height,
|
|
|
|
|
line_top: run.line_top,
|
|
|
|
|
view_position,
|
2024-01-11 23:22:39 +03:00
|
|
|
metadata_set,
|
2024-01-02 12:21:46 -07:00
|
|
|
};
|
2024-04-14 21:09:14 +01:00
|
|
|
for glyph in run.glyphs {
|
2024-01-17 18:44:40 +01:00
|
|
|
bg_rect.update(glyph, renderer, state.is_focused);
|
2024-01-02 12:21:46 -07:00
|
|
|
}
|
2024-01-17 18:44:40 +01:00
|
|
|
bg_rect.fill(renderer, state.is_focused);
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
renderer.fill_raw(Raw {
|
|
|
|
|
buffer: terminal.buffer_weak(),
|
|
|
|
|
position: view_position,
|
2026-03-18 12:21:57 -04:00
|
|
|
color: Color::from_rgba(1.0, 1.0, 1.0, 1.0), // TODO
|
2023-12-20 13:31:10 -07:00
|
|
|
clip_bounds: Rectangle::new(view_position, Size::new(view_w as f32, view_h as f32)),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Draw scrollbar
|
2023-12-21 10:26:17 -07:00
|
|
|
if let Some((start, end)) = terminal.scrollbar() {
|
|
|
|
|
let scrollbar_y = start * view_h as f32;
|
|
|
|
|
let scrollbar_h = end * view_h as f32 - scrollbar_y;
|
|
|
|
|
let scrollbar_rect = Rectangle::new(
|
|
|
|
|
[view_w as f32, scrollbar_y].into(),
|
|
|
|
|
Size::new(scrollbar_w, scrollbar_h),
|
|
|
|
|
);
|
2024-01-11 11:33:39 -07:00
|
|
|
|
2024-02-05 15:59:07 +01:00
|
|
|
let pressed = matches!(&state.dragging, Some(Dragging::Scrollbar { .. }));
|
2024-01-11 11:33:39 -07:00
|
|
|
|
|
|
|
|
let mut hover = false;
|
|
|
|
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
|
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
if x >= scrollbar_rect.x && x < (scrollbar_rect.x + scrollbar_rect.width) {
|
|
|
|
|
hover = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut scrollbar_draw = scrollbar_rect + Vector::new(view_position.x, view_position.y);
|
|
|
|
|
if !hover && !pressed {
|
|
|
|
|
// Decrease draw width and keep centered when not hovered or pressed
|
|
|
|
|
scrollbar_draw.width /= 2.0;
|
|
|
|
|
scrollbar_draw.x += scrollbar_draw.width / 2.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// neutral_6, 0.7
|
|
|
|
|
let base_color = cosmic_theme
|
|
|
|
|
.palette
|
|
|
|
|
.neutral_6
|
|
|
|
|
.without_alpha()
|
|
|
|
|
.with_alpha(0.7);
|
|
|
|
|
let scrollbar_color: Color = if pressed {
|
|
|
|
|
// pressed_state_color, 0.5
|
|
|
|
|
cosmic_theme
|
2026-07-06 09:26:53 -04:00
|
|
|
.background(theme.transparent)
|
2024-01-11 11:33:39 -07:00
|
|
|
.component
|
|
|
|
|
.pressed
|
|
|
|
|
.without_alpha()
|
|
|
|
|
.with_alpha(0.5)
|
|
|
|
|
.over(base_color)
|
|
|
|
|
.into()
|
|
|
|
|
} else if hover {
|
|
|
|
|
// hover_state_color, 0.2
|
|
|
|
|
cosmic_theme
|
2026-07-06 09:26:53 -04:00
|
|
|
.background(theme.transparent)
|
2024-01-11 11:33:39 -07:00
|
|
|
.component
|
|
|
|
|
.hover
|
|
|
|
|
.without_alpha()
|
|
|
|
|
.with_alpha(0.2)
|
|
|
|
|
.over(base_color)
|
|
|
|
|
.into()
|
|
|
|
|
} else {
|
|
|
|
|
base_color.into()
|
2023-12-21 10:26:17 -07:00
|
|
|
};
|
2024-01-11 11:33:39 -07:00
|
|
|
|
2023-12-21 10:26:17 -07:00
|
|
|
renderer.fill_quad(
|
|
|
|
|
Quad {
|
2024-01-11 11:33:39 -07:00
|
|
|
bounds: scrollbar_draw,
|
2024-02-05 14:18:19 -05:00
|
|
|
border: Border {
|
|
|
|
|
radius: (scrollbar_draw.width / 2.0).into(),
|
|
|
|
|
width: 0.0,
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
2023-12-21 10:26:17 -07:00
|
|
|
},
|
2024-01-11 11:33:39 -07:00
|
|
|
scrollbar_color,
|
2023-12-21 10:26:17 -07:00
|
|
|
);
|
2024-01-11 11:33:39 -07:00
|
|
|
|
2023-12-21 10:26:17 -07:00
|
|
|
state.scrollbar_rect.set(scrollbar_rect);
|
|
|
|
|
} else {
|
|
|
|
|
state.scrollbar_rect.set(Rectangle::default())
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2026-01-16 17:47:38 +01:00
|
|
|
// Draw cursor (only when not scrolled, as cursor is at bottom of active area)
|
2024-08-23 14:48:42 +03:00
|
|
|
{
|
2026-01-16 17:47:38 +01:00
|
|
|
let term = terminal.term.lock();
|
|
|
|
|
let display_offset = term.grid().display_offset();
|
|
|
|
|
let cursor = term.renderable_content().cursor;
|
|
|
|
|
drop(term);
|
|
|
|
|
|
|
|
|
|
// Skip drawing cursor when scrolled - the cursor is below the visible viewport
|
|
|
|
|
if display_offset > 0 {
|
|
|
|
|
// Cursor is off-screen when scrolled up
|
|
|
|
|
} else {
|
|
|
|
|
let col = cursor.point.column.0;
|
|
|
|
|
let line = cursor.point.line.0;
|
|
|
|
|
let color = terminal.term.lock().colors()[NamedColor::Cursor]
|
|
|
|
|
.or(terminal.colors()[NamedColor::Cursor])
|
|
|
|
|
.map(|rgb| Color::from_rgb8(rgb.r, rgb.g, rgb.b))
|
|
|
|
|
.unwrap_or(Color::WHITE); // TODO default color from theme?
|
|
|
|
|
let width = terminal.size().cell_width;
|
|
|
|
|
let height = terminal.size().cell_height;
|
|
|
|
|
let top_left = view_position
|
|
|
|
|
+ Vector::new((col as f32 * width).floor(), (line as f32 * height).floor());
|
|
|
|
|
match cursor.shape {
|
|
|
|
|
CursorShape::Beam => {
|
|
|
|
|
let quad = Quad {
|
|
|
|
|
bounds: Rectangle::new(top_left, Size::new(1.0, height)),
|
2025-01-21 17:07:45 +01:00
|
|
|
..Default::default()
|
2026-01-16 17:47:38 +01:00
|
|
|
};
|
|
|
|
|
renderer.fill_quad(quad, color);
|
|
|
|
|
}
|
|
|
|
|
CursorShape::Underline => {
|
|
|
|
|
let quad = Quad {
|
|
|
|
|
bounds: Rectangle::new(
|
|
|
|
|
view_position
|
|
|
|
|
+ Vector::new(
|
|
|
|
|
(col as f32 * width).floor(),
|
|
|
|
|
((line + 1) as f32 * height).floor(),
|
|
|
|
|
),
|
|
|
|
|
Size::new(width, 1.0),
|
|
|
|
|
),
|
2025-01-21 17:07:45 +01:00
|
|
|
..Default::default()
|
2026-01-16 17:47:38 +01:00
|
|
|
};
|
|
|
|
|
renderer.fill_quad(quad, color);
|
|
|
|
|
}
|
|
|
|
|
CursorShape::Block if !state.is_focused => {
|
|
|
|
|
let quad = Quad {
|
|
|
|
|
bounds: Rectangle::new(top_left, Size::new(width, height)),
|
|
|
|
|
border: Border {
|
|
|
|
|
width: 1.0,
|
|
|
|
|
color,
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
renderer.fill_quad(quad, Color::TRANSPARENT);
|
|
|
|
|
}
|
|
|
|
|
CursorShape::HollowBlock => {
|
|
|
|
|
let quad = Quad {
|
|
|
|
|
bounds: Rectangle::new(top_left, Size::new(width, height)),
|
|
|
|
|
border: Border {
|
|
|
|
|
width: 1.0,
|
|
|
|
|
color,
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
renderer.fill_quad(quad, Color::TRANSPARENT);
|
|
|
|
|
}
|
|
|
|
|
CursorShape::Block | CursorShape::Hidden => {} // Block is handled seperately
|
2025-01-21 17:07:45 +01:00
|
|
|
}
|
2024-08-23 14:06:57 +03:00
|
|
|
}
|
2024-08-23 14:48:42 +03:00
|
|
|
}
|
2024-08-23 14:06:57 +03:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
let duration = instant.elapsed();
|
2024-01-04 14:25:31 -07:00
|
|
|
log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration);
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 12:21:57 -04:00
|
|
|
fn update(
|
2023-12-20 13:31:10 -07:00
|
|
|
&mut self,
|
|
|
|
|
tree: &mut widget::Tree,
|
2026-03-18 12:21:57 -04:00
|
|
|
event: &Event,
|
2023-12-20 13:31:10 -07:00
|
|
|
layout: Layout<'_>,
|
|
|
|
|
cursor_position: mouse::Cursor,
|
|
|
|
|
_renderer: &Renderer,
|
2024-01-21 21:13:13 +01:00
|
|
|
_clipboard: &mut dyn Clipboard,
|
2023-12-20 13:31:10 -07:00
|
|
|
shell: &mut Shell<'_, Message>,
|
|
|
|
|
_viewport: &Rectangle<f32>,
|
2026-03-18 12:21:57 -04:00
|
|
|
) {
|
2025-10-24 17:56:10 -04:00
|
|
|
if self.disabled {
|
2026-03-18 12:21:57 -04:00
|
|
|
return;
|
2025-10-24 17:56:10 -04:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
let state = tree.state.downcast_mut::<State>();
|
|
|
|
|
let scrollbar_rect = state.scrollbar_rect.get();
|
2023-12-21 21:37:39 -07:00
|
|
|
let mut terminal = self.terminal.lock().unwrap();
|
2023-12-20 13:31:10 -07:00
|
|
|
let buffer_size = terminal.with_buffer(|buffer| buffer.size());
|
|
|
|
|
|
2024-01-02 12:21:46 -07:00
|
|
|
let is_app_cursor = terminal.term.lock().mode().contains(TermMode::APP_CURSOR);
|
2024-01-30 08:59:19 +01:00
|
|
|
let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE);
|
2023-12-20 13:31:10 -07:00
|
|
|
match event {
|
2025-01-21 17:07:45 +01:00
|
|
|
Event::Window(event) => match event {
|
|
|
|
|
cosmic::iced::window::Event::Focused => {
|
|
|
|
|
if let Some(on_window_focused) = &self.on_window_focused {
|
|
|
|
|
shell.publish(on_window_focused());
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-07 06:47:42 +02:00
|
|
|
cosmic::iced::window::Event::RedrawRequested(_) => {
|
|
|
|
|
if is_mouse_mode {
|
|
|
|
|
state.autoscroll.stop();
|
|
|
|
|
} else {
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some((pointer, multiplier)) = state.autoscroll.next_due()
|
|
|
|
|
&& update_buffer_drag(
|
2025-11-07 06:47:42 +02:00
|
|
|
state,
|
|
|
|
|
&mut terminal,
|
|
|
|
|
buffer_size,
|
|
|
|
|
pointer,
|
|
|
|
|
layout.bounds(),
|
|
|
|
|
self.padding,
|
|
|
|
|
multiplier,
|
2026-04-10 18:40:26 +02:00
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
shell.capture_event();
|
|
|
|
|
}
|
2025-11-07 06:47:42 +02:00
|
|
|
if state.autoscroll.is_active() {
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.request_redraw();
|
2025-11-07 06:47:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-02 07:01:55 +09:00
|
|
|
shell.request_input_method(&self.input_method(state, layout, &terminal));
|
2025-11-07 06:47:42 +02:00
|
|
|
}
|
2025-01-21 17:07:45 +01:00
|
|
|
cosmic::iced::window::Event::Unfocused => {
|
|
|
|
|
state.is_focused = false;
|
2025-11-07 06:47:42 +02:00
|
|
|
state.autoscroll.stop();
|
2025-01-21 17:07:45 +01:00
|
|
|
if let Some(on_window_unfocused) = &self.on_window_unfocused {
|
|
|
|
|
shell.publish(on_window_unfocused());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
2023-12-20 13:31:10 -07:00
|
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
2024-02-05 14:18:19 -05:00
|
|
|
key: Key::Named(named),
|
2026-05-26 15:40:59 -04:00
|
|
|
physical_key,
|
2025-01-07 22:55:14 -08:00
|
|
|
modified_key: Key::Named(modified_named),
|
2023-12-20 13:31:10 -07:00
|
|
|
modifiers,
|
2024-10-05 21:17:27 +02:00
|
|
|
text,
|
2024-02-05 14:18:19 -05:00
|
|
|
..
|
2025-01-07 22:55:14 -08:00
|
|
|
}) if state.is_focused && named == modified_named => {
|
2024-04-15 20:37:44 +01:00
|
|
|
for key_bind in self.key_binds.keys() {
|
2026-05-28 13:10:17 -06:00
|
|
|
if key_bind.matches(*modifiers, &Key::Named(*named), Some(physical_key)) {
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
|
|
|
|
return;
|
2024-01-28 19:01:26 +01:00
|
|
|
}
|
2024-02-14 08:42:54 +01:00
|
|
|
}
|
2024-07-27 14:37:38 +02:00
|
|
|
|
2024-02-06 17:43:17 +01:00
|
|
|
let mod_no = calculate_modifier_number(state);
|
2024-02-05 14:18:19 -05:00
|
|
|
let escape_code = match named {
|
|
|
|
|
Named::Insert => csi("2", "~", mod_no),
|
|
|
|
|
Named::Delete => csi("3", "~", mod_no),
|
2024-04-08 08:44:32 -06:00
|
|
|
Named::PageUp => {
|
|
|
|
|
if modifiers.shift() {
|
|
|
|
|
terminal.scroll(TerminalScroll::PageUp);
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
csi("5", "~", mod_no)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Named::PageDown => {
|
|
|
|
|
if modifiers.shift() {
|
|
|
|
|
terminal.scroll(TerminalScroll::PageDown);
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
csi("6", "~", mod_no)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::ArrowUp => {
|
2024-02-06 17:43:17 +01:00
|
|
|
if is_app_cursor {
|
|
|
|
|
ss3("A", mod_no)
|
|
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
csi2("A", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
2023-12-20 14:26:31 -07:00
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::ArrowDown => {
|
2024-02-06 17:43:17 +01:00
|
|
|
if is_app_cursor {
|
|
|
|
|
ss3("B", mod_no)
|
|
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
csi2("B", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
2023-12-20 14:26:31 -07:00
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::ArrowRight => {
|
2024-02-06 17:43:17 +01:00
|
|
|
if is_app_cursor {
|
|
|
|
|
ss3("C", mod_no)
|
|
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
csi2("C", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::ArrowLeft => {
|
2024-02-06 17:43:17 +01:00
|
|
|
if is_app_cursor {
|
|
|
|
|
ss3("D", mod_no)
|
|
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
csi2("D", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::End => {
|
2024-04-08 08:44:32 -06:00
|
|
|
if modifiers.shift() {
|
|
|
|
|
terminal.scroll(TerminalScroll::Bottom);
|
|
|
|
|
None
|
|
|
|
|
} else if is_app_cursor {
|
2024-02-08 16:15:14 -07:00
|
|
|
ss3("F", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
csi2("F", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::Home => {
|
2024-04-08 08:44:32 -06:00
|
|
|
if modifiers.shift() {
|
|
|
|
|
terminal.scroll(TerminalScroll::Top);
|
|
|
|
|
None
|
|
|
|
|
} else if is_app_cursor {
|
2024-02-08 16:15:14 -07:00
|
|
|
ss3("H", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
csi2("H", mod_no)
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::F1 => ss3("P", mod_no),
|
|
|
|
|
Named::F2 => ss3("Q", mod_no),
|
|
|
|
|
Named::F3 => ss3("R", mod_no),
|
|
|
|
|
Named::F4 => ss3("S", mod_no),
|
|
|
|
|
Named::F5 => csi("15", "~", mod_no),
|
|
|
|
|
Named::F6 => csi("17", "~", mod_no),
|
|
|
|
|
Named::F7 => csi("18", "~", mod_no),
|
|
|
|
|
Named::F8 => csi("19", "~", mod_no),
|
|
|
|
|
Named::F9 => csi("20", "~", mod_no),
|
|
|
|
|
Named::F10 => csi("21", "~", mod_no),
|
|
|
|
|
Named::F11 => csi("23", "~", mod_no),
|
|
|
|
|
Named::F12 => csi("24", "~", mod_no),
|
2024-02-06 17:43:17 +01:00
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
if let Some(escape_code) = escape_code {
|
|
|
|
|
terminal.input_scroll(escape_code);
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
|
|
|
|
|
|
|
|
|
return;
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Special handle Enter, Escape, Backspace and Tab as described in
|
|
|
|
|
//https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-key-event-encoding
|
|
|
|
|
//Also special handle Ctrl-_ to behave like xterm
|
|
|
|
|
let alt_prefix = if modifiers.alt() { "\x1B" } else { "" };
|
2024-02-05 14:18:19 -05:00
|
|
|
match named {
|
2024-02-19 07:38:24 +01:00
|
|
|
Named::Backspace => {
|
|
|
|
|
let code = if modifiers.control() { "\x08" } else { "\x7f" };
|
2024-07-27 14:37:38 +02:00
|
|
|
terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-02-19 07:38:24 +01:00
|
|
|
Named::Enter => {
|
2024-07-27 14:37:38 +02:00
|
|
|
terminal.input_scroll(format!("{}{}", alt_prefix, "\x0D").into_bytes());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-02-19 07:38:24 +01:00
|
|
|
Named::Escape => {
|
2024-02-06 17:43:17 +01:00
|
|
|
//Escape with any modifier will cancel selection
|
2024-01-11 14:37:18 -07:00
|
|
|
let had_selection = {
|
|
|
|
|
let mut term = terminal.term.lock();
|
|
|
|
|
term.selection.take().is_some()
|
|
|
|
|
};
|
|
|
|
|
if had_selection {
|
|
|
|
|
terminal.update();
|
|
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
terminal.input_scroll(format!("{}{}", alt_prefix, "\x1B").into_bytes());
|
2024-01-11 14:37:18 -07:00
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-10-05 21:17:27 +02:00
|
|
|
|
2024-02-05 14:18:19 -05:00
|
|
|
Named::Tab => {
|
2024-02-06 17:43:17 +01:00
|
|
|
let code = if modifiers.shift() { "\x1b[Z" } else { "\x09" };
|
2024-07-27 14:37:38 +02:00
|
|
|
terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-22 08:45:53 -07:00
|
|
|
}
|
2024-02-06 17:43:17 +01:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
2026-03-18 12:21:57 -04:00
|
|
|
state.modifiers = *modifiers;
|
2025-10-10 12:56:56 +02:00
|
|
|
|
2025-12-30 13:51:27 -08:00
|
|
|
if modifiers.contains(Modifiers::CTRL)
|
|
|
|
|
|| terminal.active_regex_match.is_some()
|
|
|
|
|
|| terminal.active_hyperlink_id.is_some()
|
|
|
|
|
{
|
2025-10-10 12:56:56 +02:00
|
|
|
//Might need to update the url regex highlight,
|
|
|
|
|
//so we need to calculate the mouse position
|
2026-01-12 11:04:12 -07:00
|
|
|
let location = if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
|
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
2025-10-10 12:56:56 +02:00
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
|
|
|
|
Some(terminal.viewport_to_point(TermPoint::new(
|
|
|
|
|
row as usize,
|
|
|
|
|
TermColumn(col as usize),
|
|
|
|
|
)))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2025-10-16 09:27:00 +02:00
|
|
|
update_active_regex_match(&mut terminal, location, Some(&state.modifiers));
|
2025-10-10 12:56:56 +02:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
|
|
|
text,
|
|
|
|
|
modifiers,
|
|
|
|
|
key,
|
2026-05-26 15:40:59 -04:00
|
|
|
physical_key,
|
2026-03-18 12:21:57 -04:00
|
|
|
..
|
2026-03-19 11:44:06 -06:00
|
|
|
}) if state.is_focused && *key == Key::Character(SmolStr::new(" ")) => {
|
2026-03-18 12:21:57 -04:00
|
|
|
//Special handle Enter, Escape, Backspace and Tab as described in
|
|
|
|
|
//https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-key-event-encoding
|
|
|
|
|
//Also special handle Ctrl-_ to behave like xterm
|
|
|
|
|
let alt_prefix = if modifiers.alt() { "\x1B" } else { "" };
|
|
|
|
|
|
|
|
|
|
// Keep this instead of hardcoding the space to allow for dead keys
|
|
|
|
|
let character = text
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|c| c.chars().next())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
if modifiers.control() {
|
|
|
|
|
// Send NUL character (\x00) for Ctrl + Space
|
|
|
|
|
terminal.input_scroll(b"\x00".to_vec());
|
|
|
|
|
} else {
|
|
|
|
|
terminal.input_scroll(format!("{}{}", alt_prefix, character).into_bytes());
|
|
|
|
|
}
|
|
|
|
|
shell.capture_event();
|
|
|
|
|
}
|
2024-02-05 14:18:19 -05:00
|
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
|
|
|
text,
|
|
|
|
|
modifiers,
|
|
|
|
|
key,
|
2026-05-26 15:40:59 -04:00
|
|
|
physical_key,
|
2026-05-23 15:32:13 +01:00
|
|
|
modified_key,
|
2024-02-05 14:18:19 -05:00
|
|
|
..
|
|
|
|
|
}) if state.is_focused => {
|
2024-04-15 20:37:44 +01:00
|
|
|
for key_bind in self.key_binds.keys() {
|
2026-05-28 13:10:17 -06:00
|
|
|
if key_bind.matches(*modifiers, key, Some(physical_key)) {
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
|
|
|
|
|
|
|
|
|
return;
|
2024-02-14 08:42:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
let character = text
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|c| c.chars().next())
|
|
|
|
|
.unwrap_or_default();
|
2023-12-20 13:31:10 -07:00
|
|
|
match (
|
2024-02-05 14:18:19 -05:00
|
|
|
modifiers.logo(),
|
|
|
|
|
modifiers.control(),
|
|
|
|
|
modifiers.alt(),
|
|
|
|
|
modifiers.shift(),
|
2023-12-20 13:31:10 -07:00
|
|
|
) {
|
2023-12-21 22:13:17 -07:00
|
|
|
(true, _, _, _) => {
|
2023-12-20 13:31:10 -07:00
|
|
|
// Ignore super
|
|
|
|
|
}
|
2024-02-06 17:43:17 +01:00
|
|
|
(false, true, true, _) => {
|
2024-01-17 16:09:56 +01:00
|
|
|
// Handle ctrl-alt for non-control characters
|
2024-02-06 17:43:17 +01:00
|
|
|
// and control characters 0-32
|
|
|
|
|
if !character.is_control() || (character as u32) < 32 {
|
|
|
|
|
// Handle alt for non-control characters
|
|
|
|
|
let mut buf = [0x1B, 0, 0, 0, 0];
|
|
|
|
|
let len = {
|
|
|
|
|
let str = character.encode_utf8(&mut buf[1..]);
|
|
|
|
|
str.len() + 1
|
|
|
|
|
};
|
|
|
|
|
terminal.input_scroll(buf[..len].to_vec());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
(false, true, _, false) => {
|
2023-12-20 13:31:10 -07:00
|
|
|
// Handle ctrl for control characters (Ctrl-A to Ctrl-Z)
|
|
|
|
|
if character.is_control() {
|
|
|
|
|
let mut buf = [0, 0, 0, 0];
|
|
|
|
|
let str = character.encode_utf8(&mut buf);
|
2023-12-20 15:01:47 -07:00
|
|
|
terminal.input_scroll(str.as_bytes().to_vec());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
(false, true, _, true) => {
|
2026-05-23 15:32:13 +01:00
|
|
|
// Ctrl+Shift+<key>: send 0x1F (C-_) on '_', matching xterm/gnome-terminal.
|
|
|
|
|
// Use `modified_key` so this works regardless of which physical key
|
|
|
|
|
// produces '_' on the user's layout.
|
|
|
|
|
if *modified_key == Key::Character("_".into()) {
|
2024-02-05 14:18:19 -05:00
|
|
|
terminal.input_scroll(b"\x1F".as_slice());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2024-02-05 14:18:19 -05:00
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
}
|
|
|
|
|
(false, false, true, _) => {
|
2023-12-20 13:31:10 -07:00
|
|
|
if !character.is_control() {
|
|
|
|
|
// Handle alt for non-control characters
|
|
|
|
|
let mut buf = [0x1B, 0, 0, 0, 0];
|
2023-12-22 08:45:53 -07:00
|
|
|
let len = {
|
|
|
|
|
let str = character.encode_utf8(&mut buf[1..]);
|
|
|
|
|
str.len() + 1
|
|
|
|
|
};
|
|
|
|
|
terminal.input_scroll(buf[..len].to_vec());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
(false, false, false, _) => {
|
2023-12-20 13:31:10 -07:00
|
|
|
// Handle no modifiers for non-control characters
|
|
|
|
|
if !character.is_control() {
|
|
|
|
|
let mut buf = [0, 0, 0, 0];
|
|
|
|
|
let str = character.encode_utf8(&mut buf);
|
2023-12-20 15:01:47 -07:00
|
|
|
terminal.input_scroll(str.as_bytes().to_vec());
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-27 12:54:46 +09:00
|
|
|
Event::InputMethod(event) => match event {
|
|
|
|
|
input_method::Event::Opened | input_method::Event::Closed => {
|
2026-04-22 05:38:52 +09:00
|
|
|
state.preedit = matches!(event, input_method::Event::Opened)
|
|
|
|
|
.then(input_method::Preedit::new);
|
2025-04-27 12:54:46 +09:00
|
|
|
}
|
|
|
|
|
input_method::Event::Preedit(content, selection) => {
|
|
|
|
|
if state.is_focused {
|
|
|
|
|
let metrics = terminal.with_buffer(|buffer| buffer.metrics());
|
|
|
|
|
state.preedit = Some(input_method::Preedit {
|
|
|
|
|
content: content.to_owned(),
|
|
|
|
|
selection: selection.clone(),
|
|
|
|
|
text_size: Some(metrics.font_size.into()),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
input_method::Event::Commit(text) => {
|
|
|
|
|
if state.is_focused {
|
|
|
|
|
terminal.paste(text.to_string());
|
|
|
|
|
shell.capture_event();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-12-20 13:31:10 -07:00
|
|
|
Event::Mouse(MouseEvent::ButtonPressed(button)) => {
|
|
|
|
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
2024-01-30 08:59:19 +01:00
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
|
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
|
|
|
|
|
|
|
|
|
if is_mouse_mode {
|
2025-11-07 06:47:42 +02:00
|
|
|
state.autoscroll.stop();
|
2026-03-18 12:21:57 -04:00
|
|
|
terminal.report_mouse(
|
|
|
|
|
event.clone(),
|
|
|
|
|
&state.modifiers,
|
|
|
|
|
col as u32,
|
|
|
|
|
row as u32,
|
|
|
|
|
);
|
2024-01-30 08:59:19 +01:00
|
|
|
} else {
|
|
|
|
|
state.is_focused = true;
|
|
|
|
|
|
|
|
|
|
// Handle left click drag
|
2024-02-05 15:59:07 +01:00
|
|
|
#[allow(clippy::collapsible_if)]
|
2024-01-30 08:59:19 +01:00
|
|
|
if let Button::Left = button {
|
|
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
2024-06-12 09:50:07 -06:00
|
|
|
if x >= 0.0
|
|
|
|
|
&& x < buffer_size.0.unwrap_or(0.0)
|
|
|
|
|
&& y >= 0.0
|
|
|
|
|
&& y < buffer_size.1.unwrap_or(0.0)
|
|
|
|
|
{
|
2024-01-30 08:59:19 +01:00
|
|
|
let click_kind =
|
|
|
|
|
if let Some((click_kind, click_time)) = state.click.take() {
|
|
|
|
|
if click_time.elapsed() < self.click_timing {
|
|
|
|
|
match click_kind {
|
|
|
|
|
ClickKind::Single => ClickKind::Double,
|
|
|
|
|
ClickKind::Double => ClickKind::Triple,
|
|
|
|
|
ClickKind::Triple => ClickKind::Single,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ClickKind::Single
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ClickKind::Single
|
2024-01-30 08:59:19 +01:00
|
|
|
};
|
|
|
|
|
let location = terminal.viewport_to_point(TermPoint::new(
|
|
|
|
|
row as usize,
|
|
|
|
|
TermColumn(col as usize),
|
|
|
|
|
));
|
|
|
|
|
let side = if col.fract() < 0.5 {
|
|
|
|
|
TermSide::Left
|
2023-12-20 13:31:10 -07:00
|
|
|
} else {
|
2024-01-30 08:59:19 +01:00
|
|
|
TermSide::Right
|
2023-12-20 13:31:10 -07:00
|
|
|
};
|
2025-12-23 16:12:52 -08:00
|
|
|
// Check if shift is pressed and there's an existing selection to extend
|
|
|
|
|
if state.modifiers.shift() {
|
|
|
|
|
let mut term = terminal.term.lock();
|
|
|
|
|
if let Some(ref mut selection) = term.selection {
|
|
|
|
|
selection.update(location, side);
|
|
|
|
|
} else {
|
2026-01-12 11:04:12 -07:00
|
|
|
term.selection = Some(Selection::new(
|
|
|
|
|
SelectionType::Simple,
|
|
|
|
|
location,
|
|
|
|
|
side,
|
|
|
|
|
));
|
2024-01-30 08:59:19 +01:00
|
|
|
}
|
2025-12-23 16:12:52 -08:00
|
|
|
} else {
|
|
|
|
|
let selection = match click_kind {
|
|
|
|
|
ClickKind::Single => {
|
|
|
|
|
Selection::new(SelectionType::Simple, location, side)
|
|
|
|
|
}
|
|
|
|
|
ClickKind::Double => {
|
|
|
|
|
Selection::new(SelectionType::Semantic, location, side)
|
|
|
|
|
}
|
|
|
|
|
ClickKind::Triple => {
|
|
|
|
|
Selection::new(SelectionType::Lines, location, side)
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-30 08:59:19 +01:00
|
|
|
let mut term = terminal.term.lock();
|
|
|
|
|
term.selection = Some(selection);
|
2023-12-21 15:09:18 -07:00
|
|
|
}
|
2024-01-30 08:59:19 +01:00
|
|
|
terminal.needs_update = true;
|
|
|
|
|
state.click = Some((click_kind, Instant::now()));
|
2025-10-29 17:37:02 +02:00
|
|
|
state.dragging = Some(Dragging::Buffer {
|
|
|
|
|
edge_scroll_remainder: 0.0,
|
2025-11-07 06:47:42 +02:00
|
|
|
last_edge_direction: EdgeScrollDirection::None,
|
|
|
|
|
last_edge_overshoot: 0.0,
|
2025-10-29 17:37:02 +02:00
|
|
|
last_point: location,
|
|
|
|
|
last_side: side,
|
|
|
|
|
});
|
2024-01-30 08:59:19 +01:00
|
|
|
} else if scrollbar_rect.contains(Point::new(x, y)) {
|
2024-01-11 11:33:39 -07:00
|
|
|
if let Some(start_scroll) = terminal.scrollbar() {
|
|
|
|
|
state.dragging = Some(Dragging::Scrollbar {
|
|
|
|
|
start_y: y,
|
|
|
|
|
start_scroll,
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-01-30 08:59:19 +01:00
|
|
|
} else if x >= scrollbar_rect.x
|
|
|
|
|
&& x < (scrollbar_rect.x + scrollbar_rect.width)
|
|
|
|
|
{
|
|
|
|
|
if terminal.scrollbar().is_some() {
|
2024-06-12 09:50:07 -06:00
|
|
|
let scroll_ratio = terminal
|
|
|
|
|
.with_buffer(|buffer| y / buffer.size().1.unwrap_or(1.0));
|
2024-01-30 08:59:19 +01:00
|
|
|
terminal.scroll_to(scroll_ratio);
|
|
|
|
|
if let Some(start_scroll) = terminal.scrollbar() {
|
|
|
|
|
state.dragging = Some(Dragging::Scrollbar {
|
|
|
|
|
start_y: y,
|
|
|
|
|
start_scroll,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 10:26:17 -07:00
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
} else if *button == Button::Middle {
|
2024-02-02 10:35:45 +01:00
|
|
|
if let Some(on_middle_click) = &self.on_middle_click {
|
|
|
|
|
shell.publish(on_middle_click());
|
2023-12-21 10:26:17 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2024-01-30 08:59:19 +01:00
|
|
|
// Update context menu state
|
|
|
|
|
if let Some(on_context_menu) = &self.on_context_menu {
|
2026-09-08 13:49:17 -06:00
|
|
|
match self.context_menu_open {
|
|
|
|
|
true => {
|
2025-10-17 10:07:42 +02:00
|
|
|
shell.publish(on_context_menu(None));
|
|
|
|
|
}
|
2026-09-08 13:49:17 -06:00
|
|
|
false => {
|
2026-03-18 12:21:57 -04:00
|
|
|
if *button == Button::Right {
|
2025-10-16 09:27:00 +02:00
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
|
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
|
|
|
|
|
|
|
|
|
let location = terminal.viewport_to_point(TermPoint::new(
|
|
|
|
|
row as usize,
|
|
|
|
|
TermColumn(col as usize),
|
|
|
|
|
));
|
|
|
|
|
update_active_regex_match(
|
|
|
|
|
&mut terminal,
|
|
|
|
|
Some(location),
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
let link = get_hyperlink(&terminal, location);
|
2026-09-08 13:49:17 -06:00
|
|
|
shell.publish(on_context_menu(Some(MenuState { link })));
|
2025-10-16 09:27:00 +02:00
|
|
|
}
|
2025-10-17 10:07:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-20 14:13:04 +01:00
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => {
|
2025-11-07 06:47:42 +02:00
|
|
|
state.autoscroll.stop();
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(dragging) = state.dragging.take()
|
|
|
|
|
&& let Dragging::Buffer {
|
2025-10-29 17:37:02 +02:00
|
|
|
last_point,
|
|
|
|
|
last_side,
|
|
|
|
|
..
|
|
|
|
|
} = dragging
|
2026-04-10 18:40:26 +02:00
|
|
|
{
|
2025-10-29 17:37:02 +02:00
|
|
|
{
|
2026-04-10 18:40:26 +02:00
|
|
|
let mut term = terminal.term.lock();
|
|
|
|
|
if let Some(selection) = &mut term.selection {
|
|
|
|
|
selection.update(last_point, last_side);
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 18:40:26 +02:00
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
2024-01-30 08:59:19 +01:00
|
|
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
|
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
|
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
2024-11-17 01:47:50 +01:00
|
|
|
|
|
|
|
|
let location = terminal
|
|
|
|
|
.viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize)));
|
2026-04-10 18:09:25 +02:00
|
|
|
if state.modifiers.control()
|
|
|
|
|
&& let Some(on_open_hyperlink) = &self.on_open_hyperlink
|
2026-04-10 18:40:26 +02:00
|
|
|
&& let Some(hyperlink) = get_hyperlink(&terminal, location)
|
|
|
|
|
{
|
|
|
|
|
shell.publish(on_open_hyperlink(hyperlink));
|
|
|
|
|
shell.capture_event();
|
|
|
|
|
}
|
2024-11-17 01:47:50 +01:00
|
|
|
|
2024-01-30 08:59:19 +01:00
|
|
|
if is_mouse_mode {
|
2026-03-18 12:21:57 -04:00
|
|
|
terminal.report_mouse(
|
|
|
|
|
event.clone(),
|
|
|
|
|
&state.modifiers,
|
|
|
|
|
col as u32,
|
|
|
|
|
row as u32,
|
|
|
|
|
);
|
2024-01-30 08:59:19 +01:00
|
|
|
} else {
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2024-01-30 08:59:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Event::Mouse(MouseEvent::ButtonReleased(_button)) => {
|
2025-11-07 06:47:42 +02:00
|
|
|
state.autoscroll.stop();
|
2024-01-30 08:59:19 +01:00
|
|
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
|
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
|
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
|
|
|
|
if is_mouse_mode {
|
2026-03-18 12:21:57 -04:00
|
|
|
terminal.report_mouse(
|
|
|
|
|
event.clone(),
|
|
|
|
|
&state.modifiers,
|
|
|
|
|
col as u32,
|
|
|
|
|
row as u32,
|
|
|
|
|
);
|
2024-01-30 08:59:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
Event::Mouse(MouseEvent::CursorMoved { .. }) => {
|
2024-01-22 16:35:23 +01:00
|
|
|
if let Some(on_mouse_enter) = &self.on_mouse_enter {
|
|
|
|
|
let mouse_is_inside = cursor_position.position_in(layout.bounds()).is_some();
|
|
|
|
|
if let Some(known_is_inside) = self.mouse_inside_boundary {
|
|
|
|
|
if mouse_is_inside != known_is_inside {
|
|
|
|
|
if mouse_is_inside {
|
|
|
|
|
shell.publish(on_mouse_enter());
|
|
|
|
|
}
|
|
|
|
|
self.mouse_inside_boundary = Some(mouse_is_inside);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.mouse_inside_boundary = Some(mouse_is_inside);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 11:04:12 -07:00
|
|
|
if let Some(p_global) = cursor_position.position() {
|
2025-11-07 06:47:42 +02:00
|
|
|
let bounds = layout.bounds();
|
2026-01-12 11:04:12 -07:00
|
|
|
let col_row_opt = if let Some(p) = cursor_position.position_in(bounds) {
|
|
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
|
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
|
|
|
|
let location = terminal.viewport_to_point(TermPoint::new(
|
|
|
|
|
row as usize,
|
|
|
|
|
TermColumn(col as usize),
|
|
|
|
|
));
|
|
|
|
|
update_active_regex_match(
|
|
|
|
|
&mut terminal,
|
|
|
|
|
Some(location),
|
|
|
|
|
Some(&state.modifiers),
|
|
|
|
|
);
|
|
|
|
|
Some((col, row))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2024-11-17 01:47:50 +01:00
|
|
|
|
2024-01-30 08:59:19 +01:00
|
|
|
if is_mouse_mode {
|
2026-01-12 11:04:12 -07:00
|
|
|
if let Some((col, row)) = col_row_opt {
|
2026-03-18 12:21:57 -04:00
|
|
|
terminal.report_mouse(
|
|
|
|
|
event.clone(),
|
|
|
|
|
&state.modifiers,
|
|
|
|
|
col as u32,
|
|
|
|
|
row as u32,
|
|
|
|
|
);
|
2026-01-12 11:04:12 -07:00
|
|
|
}
|
2024-01-30 08:59:19 +01:00
|
|
|
} else {
|
2025-11-07 06:47:42 +02:00
|
|
|
let handled_buffer_drag = update_buffer_drag(
|
|
|
|
|
state,
|
|
|
|
|
&mut terminal,
|
|
|
|
|
buffer_size,
|
2026-01-12 11:04:12 -07:00
|
|
|
p_global,
|
2025-11-07 06:47:42 +02:00
|
|
|
bounds,
|
|
|
|
|
self.padding,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
|
|
|
|
if handled_buffer_drag {
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2025-11-07 06:47:42 +02:00
|
|
|
} else if let Some(Dragging::Scrollbar {
|
|
|
|
|
start_y,
|
|
|
|
|
start_scroll,
|
|
|
|
|
}) = state.dragging.as_mut()
|
|
|
|
|
{
|
2026-01-12 11:04:12 -07:00
|
|
|
let y = p_global.y - bounds.y - self.padding.top;
|
2025-11-07 06:47:42 +02:00
|
|
|
let start_y = *start_y;
|
|
|
|
|
let start_scroll = *start_scroll;
|
|
|
|
|
let scroll_offset = terminal.with_buffer(|buffer| {
|
|
|
|
|
(y - start_y) / buffer.size().1.unwrap_or(1.0)
|
|
|
|
|
});
|
|
|
|
|
terminal.scroll_to(start_scroll.0 + scroll_offset);
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2025-11-07 06:47:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if matches!(state.dragging, Some(Dragging::Buffer { .. })) {
|
|
|
|
|
if cursor_position.position_in(bounds).is_some() {
|
|
|
|
|
state.autoscroll.stop();
|
|
|
|
|
} else {
|
|
|
|
|
if state.autoscroll.is_active() {
|
2026-01-12 11:04:12 -07:00
|
|
|
state.autoscroll.update_pointer(p_global);
|
2025-11-07 06:47:42 +02:00
|
|
|
} else {
|
2026-01-12 11:04:12 -07:00
|
|
|
state.autoscroll.start(p_global);
|
2023-12-21 15:09:18 -07:00
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.request_redraw();
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2025-11-07 06:47:42 +02:00
|
|
|
} else {
|
|
|
|
|
state.autoscroll.stop();
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Event::Mouse(MouseEvent::WheelScrolled { delta }) => {
|
2024-01-31 15:43:20 +01:00
|
|
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
2024-01-30 08:59:19 +01:00
|
|
|
if is_mouse_mode {
|
2026-01-12 11:04:12 -07:00
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
2024-01-31 15:43:20 +01:00
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
2026-03-18 12:21:57 -04:00
|
|
|
terminal.scroll_mouse(*delta, &state.modifiers, col as u32, row as u32);
|
2025-10-29 17:37:02 +02:00
|
|
|
} else if terminal.term.lock().mode().contains(TermMode::ALT_SCREEN) {
|
2026-05-25 11:16:33 -06:00
|
|
|
terminal.scroll_as_arrows(*delta);
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2024-01-30 08:59:19 +01:00
|
|
|
} else {
|
2025-10-29 17:37:02 +02:00
|
|
|
match delta {
|
|
|
|
|
ScrollDelta::Lines { x: _, y } => {
|
2026-06-30 21:39:17 +02:00
|
|
|
// High-resolution wheels deliver a notch as many
|
|
|
|
|
// fractional deltas; accumulate them so nothing
|
|
|
|
|
// below one whole line is lost (see
|
|
|
|
|
// accumulate_wheel_lines).
|
2025-10-29 17:37:02 +02:00
|
|
|
state.scroll_pixels = 0.0;
|
2026-06-30 21:39:17 +02:00
|
|
|
let (lines, remainder) =
|
|
|
|
|
accumulate_wheel_lines(*y, state.scroll_lines);
|
|
|
|
|
state.scroll_lines = remainder;
|
2025-10-29 17:37:02 +02:00
|
|
|
if lines != 0 {
|
2026-06-30 21:39:17 +02:00
|
|
|
terminal.scroll(TerminalScroll::Delta(lines));
|
2024-01-30 08:59:19 +01:00
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
ScrollDelta::Pixels { x: _, y } => {
|
|
|
|
|
//TODO: this adjustment is just a guess!
|
2026-06-30 21:39:17 +02:00
|
|
|
state.scroll_lines = 0.0;
|
2025-10-29 17:37:02 +02:00
|
|
|
state.scroll_pixels -= y * 6.0;
|
|
|
|
|
let mut lines = 0;
|
|
|
|
|
let metrics = terminal.with_buffer(|buffer| buffer.metrics());
|
|
|
|
|
while state.scroll_pixels <= -metrics.line_height {
|
|
|
|
|
lines -= 1;
|
|
|
|
|
state.scroll_pixels += metrics.line_height;
|
|
|
|
|
}
|
|
|
|
|
while state.scroll_pixels >= metrics.line_height {
|
|
|
|
|
lines += 1;
|
|
|
|
|
state.scroll_pixels -= metrics.line_height;
|
2024-01-30 08:59:19 +01:00
|
|
|
}
|
2025-10-29 17:37:02 +02:00
|
|
|
if lines != 0 {
|
|
|
|
|
terminal.scroll(TerminalScroll::Delta(-lines));
|
|
|
|
|
}
|
2026-03-18 12:21:57 -04:00
|
|
|
shell.capture_event();
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-17 01:47:50 +01:00
|
|
|
{
|
|
|
|
|
let x = p.x - self.padding.left;
|
|
|
|
|
let y = p.y - self.padding.top;
|
|
|
|
|
//TODO: better calculation of position
|
|
|
|
|
let col = x / terminal.size().cell_width;
|
|
|
|
|
let row = y / terminal.size().cell_height;
|
|
|
|
|
|
|
|
|
|
let location = terminal.viewport_to_point(TermPoint::new(
|
|
|
|
|
row as usize,
|
|
|
|
|
TermColumn(col as usize),
|
|
|
|
|
));
|
2025-10-16 09:27:00 +02:00
|
|
|
update_active_regex_match(
|
|
|
|
|
&mut terminal,
|
|
|
|
|
Some(location),
|
|
|
|
|
Some(&state.modifiers),
|
|
|
|
|
);
|
2024-11-17 01:47:50 +01:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
2026-08-04 12:02:12 -06:00
|
|
|
|
|
|
|
|
if terminal.needs_update {
|
|
|
|
|
shell.request_redraw();
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 09:27:00 +02:00
|
|
|
fn get_hyperlink(
|
|
|
|
|
terminal: &std::sync::MutexGuard<'_, Terminal>,
|
|
|
|
|
location: TermPoint,
|
|
|
|
|
) -> Option<String> {
|
2025-12-30 13:51:27 -08:00
|
|
|
if let Some(link) = osc8_hyperlink_at(terminal, location) {
|
|
|
|
|
return Some(link.uri().to_string());
|
|
|
|
|
}
|
2025-10-16 09:27:00 +02:00
|
|
|
if let Some(match_) = terminal
|
|
|
|
|
.regex_matches
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|bounds| bounds.contains(&location))
|
|
|
|
|
{
|
|
|
|
|
let term = terminal.term.lock();
|
|
|
|
|
Some(term.bounds_to_string(*match_.start(), *match_.end()))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 13:51:27 -08:00
|
|
|
fn get_hyperlink_id(
|
|
|
|
|
terminal: &std::sync::MutexGuard<'_, Terminal>,
|
|
|
|
|
location: TermPoint,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
osc8_hyperlink_at(terminal, location).map(|link| link.id().to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn osc8_hyperlink_at(
|
|
|
|
|
terminal: &std::sync::MutexGuard<'_, Terminal>,
|
|
|
|
|
location: TermPoint,
|
|
|
|
|
) -> Option<alacritty_terminal::term::cell::Hyperlink> {
|
|
|
|
|
let term = terminal.term.lock();
|
2026-01-12 11:04:12 -07:00
|
|
|
if location.line >= term.screen_lines() || location.column.0 >= term.columns() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2025-12-30 13:51:27 -08:00
|
|
|
let grid = term.grid();
|
|
|
|
|
let cell = &grid[location];
|
|
|
|
|
if let Some(link) = cell.hyperlink() {
|
|
|
|
|
return Some(link);
|
|
|
|
|
}
|
2026-01-12 11:04:12 -07:00
|
|
|
if cell
|
|
|
|
|
.flags
|
|
|
|
|
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
|
2025-12-30 13:51:27 -08:00
|
|
|
&& location.column.0 > 0
|
|
|
|
|
{
|
|
|
|
|
let left = TermPoint::new(location.line, TermColumn(location.column.0 - 1));
|
|
|
|
|
return grid[left].hyperlink();
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-17 01:47:50 +01:00
|
|
|
fn update_active_regex_match(
|
|
|
|
|
terminal: &mut std::sync::MutexGuard<'_, Terminal>,
|
2025-10-10 12:56:56 +02:00
|
|
|
location: Option<TermPoint>,
|
2025-10-16 09:27:00 +02:00
|
|
|
modifiers: Option<&Modifiers>,
|
2024-11-17 01:47:50 +01:00
|
|
|
) {
|
2025-10-17 07:56:43 +02:00
|
|
|
//Do not update any highlights if
|
|
|
|
|
//there is a context_menu shown
|
|
|
|
|
//to the user
|
|
|
|
|
if terminal.context_menu.is_some() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 13:51:27 -08:00
|
|
|
let allow_hyperlink = modifiers
|
|
|
|
|
.map(|mods| mods.contains(Modifiers::CTRL))
|
|
|
|
|
.unwrap_or(false);
|
2025-10-17 07:56:43 +02:00
|
|
|
//Require CTRL for keyboard and mouse interaction
|
2025-10-16 09:27:00 +02:00
|
|
|
if let Some(modifiers) = modifiers {
|
|
|
|
|
if !modifiers.contains(Modifiers::CTRL) {
|
|
|
|
|
if terminal.active_regex_match.is_some() {
|
|
|
|
|
terminal.active_regex_match = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
2025-12-30 13:51:27 -08:00
|
|
|
if terminal.active_hyperlink_id.is_some() {
|
|
|
|
|
terminal.active_hyperlink_id = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
2025-10-16 09:27:00 +02:00
|
|
|
return;
|
2025-10-10 12:56:56 +02:00
|
|
|
}
|
2025-12-30 13:51:27 -08:00
|
|
|
} else if terminal.active_hyperlink_id.is_some() {
|
|
|
|
|
terminal.active_hyperlink_id = None;
|
|
|
|
|
terminal.needs_update = true;
|
2025-10-10 12:56:56 +02:00
|
|
|
}
|
|
|
|
|
let Some(location) = location else {
|
|
|
|
|
if terminal.active_regex_match.is_some() {
|
|
|
|
|
terminal.active_regex_match = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
2025-12-30 13:51:27 -08:00
|
|
|
if terminal.active_hyperlink_id.is_some() {
|
|
|
|
|
terminal.active_hyperlink_id = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
2025-10-10 12:56:56 +02:00
|
|
|
return;
|
|
|
|
|
};
|
2025-12-30 13:51:27 -08:00
|
|
|
if allow_hyperlink {
|
|
|
|
|
let next_hyperlink_id = get_hyperlink_id(terminal, location);
|
|
|
|
|
if terminal.active_hyperlink_id != next_hyperlink_id {
|
|
|
|
|
terminal.active_hyperlink_id = next_hyperlink_id;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
|
|
|
|
} else if terminal.active_hyperlink_id.is_some() {
|
|
|
|
|
terminal.active_hyperlink_id = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
2024-11-17 01:47:50 +01:00
|
|
|
if let Some(match_) = terminal
|
|
|
|
|
.regex_matches
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|bounds| bounds.contains(&location))
|
|
|
|
|
{
|
|
|
|
|
'update: {
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(active_match) = &terminal.active_regex_match
|
2026-04-10 18:40:26 +02:00
|
|
|
&& active_match == match_
|
|
|
|
|
{
|
|
|
|
|
break 'update;
|
|
|
|
|
}
|
2024-11-17 01:47:50 +01:00
|
|
|
terminal.active_regex_match = Some(match_.clone());
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
|
|
|
|
} else if terminal.active_regex_match.is_some() {
|
|
|
|
|
terminal.active_regex_match = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 21:39:17 +02:00
|
|
|
/// Whole lines scrolled per unit of wheel `Lines` delta.
|
|
|
|
|
//TODO: this adjustment is just a guess!
|
|
|
|
|
const SCROLL_LINE_MULTIPLIER: f32 = 6.0;
|
|
|
|
|
|
|
|
|
|
/// Convert a wheel `Lines` delta into whole lines to scroll, accumulating the
|
|
|
|
|
/// fractional remainder across events.
|
|
|
|
|
///
|
|
|
|
|
/// High-resolution wheels deliver a single physical notch as many fractional
|
|
|
|
|
/// deltas (e.g. `y = 0.125`). Scaling by [`SCROLL_LINE_MULTIPLIER`] and
|
|
|
|
|
/// truncating per event discards anything below one whole line, so most of
|
|
|
|
|
/// those events would scroll nothing. Carrying the remainder forward means the
|
|
|
|
|
/// fractions add up and a full notch scrolls the intended amount.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `(whole_lines, new_accumulator)`.
|
|
|
|
|
fn accumulate_wheel_lines(y: f32, accumulator: f32) -> (i32, f32) {
|
|
|
|
|
let total = accumulator + y * SCROLL_LINE_MULTIPLIER;
|
|
|
|
|
let lines = total.trunc() as i32;
|
|
|
|
|
(lines, total - lines as f32)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-05 14:18:19 -05:00
|
|
|
impl<'a, Message> From<TerminalBox<'a, Message>> for Element<'a, Message, cosmic::Theme, Renderer>
|
2023-12-20 13:31:10 -07:00
|
|
|
where
|
|
|
|
|
Message: Clone + 'a,
|
|
|
|
|
{
|
|
|
|
|
fn from(terminal_box: TerminalBox<'a, Message>) -> Self {
|
|
|
|
|
Self::new(terminal_box)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum ClickKind {
|
|
|
|
|
Single,
|
|
|
|
|
Double,
|
|
|
|
|
Triple,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum Dragging {
|
2025-10-29 17:37:02 +02:00
|
|
|
Buffer {
|
|
|
|
|
edge_scroll_remainder: f32,
|
2025-11-07 06:47:42 +02:00
|
|
|
last_edge_direction: EdgeScrollDirection,
|
|
|
|
|
last_edge_overshoot: f32,
|
2025-10-29 17:37:02 +02:00
|
|
|
last_point: TermPoint,
|
|
|
|
|
last_side: TermSide,
|
|
|
|
|
},
|
2023-12-20 14:57:44 -07:00
|
|
|
Scrollbar {
|
|
|
|
|
start_y: f32,
|
|
|
|
|
start_scroll: (f32, f32),
|
|
|
|
|
},
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
2025-11-07 06:47:42 +02:00
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
|
|
|
enum EdgeScrollDirection {
|
|
|
|
|
None,
|
|
|
|
|
Top,
|
|
|
|
|
Bottom,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 18:40:26 +02:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2025-10-29 17:37:02 +02:00
|
|
|
fn edge_scroll_adjustment(
|
|
|
|
|
y: f32,
|
|
|
|
|
buffer_height: f32,
|
|
|
|
|
cell_height: f32,
|
|
|
|
|
max_row_index: f32,
|
|
|
|
|
scroll_remainder: f32,
|
2025-11-07 06:47:42 +02:00
|
|
|
last_direction: EdgeScrollDirection,
|
|
|
|
|
last_overshoot: f32,
|
|
|
|
|
forced_increment: f32,
|
|
|
|
|
) -> (i32, f32, f32, EdgeScrollDirection, f32) {
|
2025-10-29 17:37:02 +02:00
|
|
|
if cell_height <= 0.0 {
|
2025-11-07 06:47:42 +02:00
|
|
|
return (0, 0.0, 0.0, EdgeScrollDirection::None, 0.0);
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut row = y / cell_height;
|
|
|
|
|
let mut delta = 0;
|
|
|
|
|
let mut remainder = scroll_remainder;
|
2025-11-07 06:47:42 +02:00
|
|
|
let mut direction = EdgeScrollDirection::None;
|
|
|
|
|
let mut overshoot = 0.0;
|
2025-10-29 17:37:02 +02:00
|
|
|
|
|
|
|
|
if y < 0.0 {
|
2025-11-07 06:47:42 +02:00
|
|
|
direction = EdgeScrollDirection::Top;
|
|
|
|
|
overshoot = (-y) / cell_height;
|
|
|
|
|
let previous_overshoot = if last_direction == direction {
|
|
|
|
|
last_overshoot
|
|
|
|
|
} else {
|
|
|
|
|
0.0
|
|
|
|
|
};
|
|
|
|
|
let overshoot_delta = overshoot - previous_overshoot;
|
|
|
|
|
let forced = if forced_increment > 0.0 {
|
|
|
|
|
forced_increment * overshoot.max(1.0)
|
|
|
|
|
} else {
|
|
|
|
|
0.0
|
|
|
|
|
};
|
|
|
|
|
if overshoot_delta > 0.0 || forced > 0.0 {
|
|
|
|
|
remainder = remainder.max(0.0) + overshoot_delta.max(0.0) + forced;
|
|
|
|
|
} else {
|
|
|
|
|
remainder = remainder.max(0.0).min(overshoot.fract());
|
|
|
|
|
}
|
2025-10-29 17:37:02 +02:00
|
|
|
delta = remainder.trunc() as i32;
|
|
|
|
|
remainder -= delta as f32;
|
|
|
|
|
row = 0.0;
|
|
|
|
|
} else if y > buffer_height {
|
2025-11-07 06:47:42 +02:00
|
|
|
direction = EdgeScrollDirection::Bottom;
|
|
|
|
|
overshoot = (y - buffer_height) / cell_height;
|
|
|
|
|
let previous_overshoot = if last_direction == direction {
|
|
|
|
|
last_overshoot
|
|
|
|
|
} else {
|
|
|
|
|
0.0
|
|
|
|
|
};
|
|
|
|
|
let overshoot_delta = overshoot - previous_overshoot;
|
|
|
|
|
let forced = if forced_increment > 0.0 {
|
|
|
|
|
forced_increment * overshoot.max(1.0)
|
|
|
|
|
} else {
|
|
|
|
|
0.0
|
|
|
|
|
};
|
|
|
|
|
if overshoot_delta > 0.0 || forced > 0.0 {
|
|
|
|
|
remainder = remainder.max(0.0) + overshoot_delta.max(0.0) + forced;
|
|
|
|
|
} else {
|
|
|
|
|
remainder = remainder.max(0.0).min(overshoot.fract());
|
|
|
|
|
}
|
|
|
|
|
let lines = remainder.trunc() as i32;
|
|
|
|
|
delta = -lines;
|
|
|
|
|
remainder -= lines as f32;
|
2025-10-29 17:37:02 +02:00
|
|
|
row = max_row_index;
|
|
|
|
|
} else {
|
|
|
|
|
remainder = 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row = row.clamp(0.0, max_row_index);
|
|
|
|
|
|
2025-11-07 06:47:42 +02:00
|
|
|
(delta, row, remainder, direction, overshoot)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_buffer_drag(
|
|
|
|
|
state: &mut State,
|
|
|
|
|
terminal: &mut Terminal,
|
|
|
|
|
buffer_size: (Option<f32>, Option<f32>),
|
|
|
|
|
pointer: Point,
|
|
|
|
|
bounds: Rectangle<f32>,
|
|
|
|
|
padding: Padding,
|
|
|
|
|
forced_increment: f32,
|
|
|
|
|
) -> bool {
|
|
|
|
|
let Some(dragging) = state.dragging.as_mut() else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Dragging::Buffer {
|
|
|
|
|
edge_scroll_remainder,
|
|
|
|
|
last_edge_direction,
|
|
|
|
|
last_edge_overshoot,
|
|
|
|
|
last_point,
|
|
|
|
|
last_side,
|
|
|
|
|
} = dragging
|
|
|
|
|
else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let x = (pointer.x - bounds.x) - padding.left;
|
|
|
|
|
let y = (pointer.y - bounds.y) - padding.top;
|
|
|
|
|
|
|
|
|
|
let size = terminal.size();
|
|
|
|
|
let buffer_height = buffer_size.1.unwrap_or(size.height as f32);
|
|
|
|
|
let max_row_index = (size.screen_lines().saturating_sub(1)) as f32;
|
|
|
|
|
let max_col_index = (size.columns().saturating_sub(1)) as f32;
|
|
|
|
|
let (scroll_delta, adjusted_row, new_remainder, new_direction, new_overshoot) =
|
|
|
|
|
edge_scroll_adjustment(
|
|
|
|
|
y,
|
|
|
|
|
buffer_height,
|
|
|
|
|
size.cell_height,
|
|
|
|
|
max_row_index,
|
|
|
|
|
*edge_scroll_remainder,
|
|
|
|
|
*last_edge_direction,
|
|
|
|
|
*last_edge_overshoot,
|
|
|
|
|
forced_increment,
|
|
|
|
|
);
|
|
|
|
|
*edge_scroll_remainder = new_remainder;
|
|
|
|
|
*last_edge_direction = new_direction;
|
|
|
|
|
*last_edge_overshoot = new_overshoot;
|
|
|
|
|
let col = (x / size.cell_width).clamp(0.0, max_col_index);
|
|
|
|
|
let row = adjusted_row.clamp(0.0, max_row_index);
|
|
|
|
|
if scroll_delta != 0 {
|
|
|
|
|
terminal.scroll(TerminalScroll::Delta(scroll_delta));
|
|
|
|
|
}
|
|
|
|
|
let location =
|
|
|
|
|
terminal.viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize)));
|
|
|
|
|
let side = if col.fract() < 0.5 {
|
|
|
|
|
TermSide::Left
|
|
|
|
|
} else {
|
|
|
|
|
TermSide::Right
|
|
|
|
|
};
|
|
|
|
|
{
|
|
|
|
|
let mut term = terminal.term.lock();
|
|
|
|
|
if let Some(selection) = &mut term.selection {
|
|
|
|
|
selection.update(location, side);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*last_point = location;
|
|
|
|
|
*last_side = side;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
|
|
|
|
|
true
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
pub struct State {
|
|
|
|
|
modifiers: Modifiers,
|
|
|
|
|
click: Option<(ClickKind, Instant)>,
|
|
|
|
|
dragging: Option<Dragging>,
|
2024-01-11 13:20:16 -07:00
|
|
|
is_focused: bool,
|
2023-12-20 13:31:10 -07:00
|
|
|
scroll_pixels: f32,
|
2026-06-30 21:39:17 +02:00
|
|
|
scroll_lines: f32,
|
2023-12-20 13:31:10 -07:00
|
|
|
scrollbar_rect: Cell<Rectangle<f32>>,
|
2025-11-07 06:47:42 +02:00
|
|
|
autoscroll: DragAutoscroll,
|
2025-04-27 12:54:46 +09:00
|
|
|
preedit: Option<input_method::Preedit>,
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl State {
|
|
|
|
|
/// Creates a new [`State`].
|
2024-04-15 21:05:30 +01:00
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
2023-12-20 13:31:10 -07:00
|
|
|
modifiers: Modifiers::empty(),
|
|
|
|
|
click: None,
|
|
|
|
|
dragging: None,
|
2024-01-11 13:20:16 -07:00
|
|
|
is_focused: false,
|
2023-12-20 13:31:10 -07:00
|
|
|
scroll_pixels: 0.0,
|
2026-06-30 21:39:17 +02:00
|
|
|
scroll_lines: 0.0,
|
2023-12-20 13:31:10 -07:00
|
|
|
scrollbar_rect: Cell::new(Rectangle::default()),
|
2025-11-07 06:47:42 +02:00
|
|
|
autoscroll: DragAutoscroll::new(AUTOSCROLL_INTERVAL),
|
2025-04-27 12:54:46 +09:00
|
|
|
preedit: None,
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-11 13:20:16 -07:00
|
|
|
|
|
|
|
|
impl operation::Focusable for State {
|
|
|
|
|
fn is_focused(&self) -> bool {
|
|
|
|
|
self.is_focused
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn focus(&mut self) {
|
|
|
|
|
self.is_focused = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unfocus(&mut self) {
|
|
|
|
|
self.is_focused = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-06 17:43:17 +01:00
|
|
|
|
|
|
|
|
/*
|
2024-02-06 20:55:15 +01:00
|
|
|
shift 0b1 (1)
|
|
|
|
|
alt 0b10 (2)
|
|
|
|
|
ctrl 0b100 (4)
|
|
|
|
|
super 0b1000 (8)
|
|
|
|
|
hyper 0b10000 (16)
|
|
|
|
|
meta 0b100000 (32)
|
|
|
|
|
caps_lock 0b1000000 (64)
|
|
|
|
|
num_lock 0b10000000 (128)
|
2024-02-06 17:43:17 +01:00
|
|
|
*/
|
2024-04-15 16:29:02 +01:00
|
|
|
fn calculate_modifier_number(state: &State) -> u8 {
|
2024-02-06 20:55:15 +01:00
|
|
|
let mut mod_no = 0;
|
|
|
|
|
if state.modifiers.shift() {
|
|
|
|
|
mod_no |= 1;
|
|
|
|
|
}
|
|
|
|
|
if state.modifiers.alt() {
|
|
|
|
|
mod_no |= 2;
|
|
|
|
|
}
|
|
|
|
|
if state.modifiers.control() {
|
|
|
|
|
mod_no |= 4;
|
|
|
|
|
}
|
|
|
|
|
if state.modifiers.logo() {
|
|
|
|
|
mod_no |= 8;
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
2024-02-06 20:55:15 +01:00
|
|
|
mod_no + 1
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[inline(always)]
|
2024-02-07 18:31:34 +01:00
|
|
|
fn csi(code: &str, suffix: &str, modifiers: u8) -> Option<Vec<u8>> {
|
2024-02-06 20:55:15 +01:00
|
|
|
if modifiers == 1 {
|
2024-07-27 14:37:38 +02:00
|
|
|
Some(format!("\x1B[{code}{suffix}").into_bytes())
|
2024-02-06 17:43:17 +01:00
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
Some(format!("\x1B[{code};{modifiers}{suffix}").into_bytes())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
|
|
|
|
|
// CSI 1 ; modifier {ABCDEFHPQS}
|
|
|
|
|
// code is ABCDEFHPQS
|
|
|
|
|
#[inline(always)]
|
|
|
|
|
fn csi2(code: &str, modifiers: u8) -> Option<Vec<u8>> {
|
|
|
|
|
if modifiers == 1 {
|
|
|
|
|
Some(format!("\x1B[{code}").into_bytes())
|
|
|
|
|
} else {
|
|
|
|
|
Some(format!("\x1B[1;{modifiers}{code}").into_bytes())
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[inline(always)]
|
|
|
|
|
fn ss3(code: &str, modifiers: u8) -> Option<Vec<u8>> {
|
2024-02-06 20:55:15 +01:00
|
|
|
if modifiers == 1 {
|
2024-07-27 14:37:38 +02:00
|
|
|
Some(format!("\x1B\x4F{code}").into_bytes())
|
2024-02-06 17:43:17 +01:00
|
|
|
} else {
|
2024-07-27 14:37:38 +02:00
|
|
|
Some(format!("\x1B[1;{modifiers}{code}").into_bytes())
|
2024-02-06 17:43:17 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-29 17:37:02 +02:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-06-30 21:39:17 +02:00
|
|
|
use super::{EdgeScrollDirection, accumulate_wheel_lines, edge_scroll_adjustment};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wheel_lines_single_fractional_event_keeps_remainder() {
|
|
|
|
|
// A high-res wheel notch arrives as fractional deltas. A lone 0.125
|
|
|
|
|
// event is less than one whole line, but must not be discarded.
|
|
|
|
|
let (lines, remainder) = accumulate_wheel_lines(0.125, 0.0);
|
|
|
|
|
assert_eq!(lines, 0);
|
|
|
|
|
assert!((remainder - 0.75).abs() < f32::EPSILON);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wheel_lines_accumulate_into_whole_lines() {
|
|
|
|
|
// Eight 0.125 deltas make up one physical notch and must scroll the
|
|
|
|
|
// full SCROLL_LINE_MULTIPLIER (6) lines, not be lost to truncation.
|
|
|
|
|
let mut accumulator = 0.0;
|
|
|
|
|
let mut total = 0;
|
|
|
|
|
for _ in 0..8 {
|
|
|
|
|
let (lines, remainder) = accumulate_wheel_lines(0.125, accumulator);
|
|
|
|
|
accumulator = remainder;
|
|
|
|
|
total += lines;
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(total, 6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wheel_lines_preserve_direction() {
|
|
|
|
|
let (down, _) = accumulate_wheel_lines(0.25, 0.0);
|
|
|
|
|
let (up, _) = accumulate_wheel_lines(-0.25, 0.0);
|
|
|
|
|
assert_eq!(down, 1);
|
|
|
|
|
assert_eq!(up, -1);
|
|
|
|
|
}
|
2025-10-29 17:37:02 +02:00
|
|
|
|
|
|
|
|
const BUFFER_HEIGHT: f32 = 200.0;
|
|
|
|
|
const CELL_HEIGHT: f32 = 20.0;
|
|
|
|
|
const MAX_ROW: f32 = 9.0;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn edge_scroll_small_top_overshoot_does_not_scroll_immediately() {
|
2025-11-07 06:47:42 +02:00
|
|
|
let (delta, row, remainder, direction, overshoot) = edge_scroll_adjustment(
|
|
|
|
|
-5.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
0.0,
|
|
|
|
|
EdgeScrollDirection::None,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
2025-10-29 17:37:02 +02:00
|
|
|
assert_eq!(delta, 0);
|
|
|
|
|
assert_eq!(row, 0.0);
|
|
|
|
|
assert!((remainder - 0.25).abs() < f32::EPSILON);
|
2025-11-07 06:47:42 +02:00
|
|
|
|
|
|
|
|
let (delta_repeat, row_repeat, remainder_repeat, _, _) = edge_scroll_adjustment(
|
|
|
|
|
-5.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
|
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(delta_repeat, 0);
|
|
|
|
|
assert_eq!(row_repeat, 0.0);
|
|
|
|
|
assert!((remainder_repeat - 0.25).abs() < f32::EPSILON);
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn edge_scroll_top_accumulates_into_scroll() {
|
2025-11-07 06:47:42 +02:00
|
|
|
let (_delta, _row, mut remainder, mut direction, mut overshoot) = edge_scroll_adjustment(
|
|
|
|
|
-5.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
0.0,
|
|
|
|
|
EdgeScrollDirection::None,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
|
|
|
|
let (delta, row, new_remainder, new_direction, new_overshoot) = edge_scroll_adjustment(
|
|
|
|
|
-45.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
|
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
2025-10-29 17:37:02 +02:00
|
|
|
assert_eq!(delta, 2);
|
|
|
|
|
assert_eq!(row, 0.0);
|
2025-11-07 06:47:42 +02:00
|
|
|
assert!((new_remainder - 0.25).abs() < f32::EPSILON);
|
|
|
|
|
remainder = new_remainder;
|
|
|
|
|
direction = new_direction;
|
|
|
|
|
overshoot = new_overshoot;
|
|
|
|
|
|
|
|
|
|
// repeated event with the same overshoot should not accumulate more scroll
|
|
|
|
|
let (delta, _, remainder, _, _) = edge_scroll_adjustment(
|
|
|
|
|
-45.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
|
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(delta, 0);
|
|
|
|
|
assert!((remainder - 0.25).abs() < f32::EPSILON);
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn edge_scroll_inside_viewport_resets_remainder() {
|
2025-11-07 06:47:42 +02:00
|
|
|
let (_delta, _row, remainder, direction, overshoot) = edge_scroll_adjustment(
|
|
|
|
|
-25.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
0.0,
|
|
|
|
|
EdgeScrollDirection::None,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
|
|
|
|
let (delta, row, remainder, direction, overshoot) = edge_scroll_adjustment(
|
|
|
|
|
60.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
|
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
2025-10-29 17:37:02 +02:00
|
|
|
assert_eq!(delta, 0);
|
|
|
|
|
assert!((row - 3.0).abs() < f32::EPSILON);
|
|
|
|
|
assert_eq!(remainder, 0.0);
|
2025-11-07 06:47:42 +02:00
|
|
|
assert_eq!(direction, EdgeScrollDirection::None);
|
|
|
|
|
assert_eq!(overshoot, 0.0);
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn edge_scroll_bottom_accumulates_scroll() {
|
2025-11-07 06:47:42 +02:00
|
|
|
let (delta, row, remainder, direction, overshoot) = edge_scroll_adjustment(
|
2025-10-29 17:37:02 +02:00
|
|
|
BUFFER_HEIGHT + 1.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
0.0,
|
2025-11-07 06:47:42 +02:00
|
|
|
EdgeScrollDirection::None,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
2025-10-29 17:37:02 +02:00
|
|
|
);
|
|
|
|
|
assert_eq!(delta, 0);
|
|
|
|
|
assert!((row - MAX_ROW).abs() < f32::EPSILON);
|
2025-11-07 06:47:42 +02:00
|
|
|
assert!((remainder - 0.05).abs() < f32::EPSILON);
|
2025-10-29 17:37:02 +02:00
|
|
|
|
2025-11-07 06:47:42 +02:00
|
|
|
let (delta, row, remainder, direction, overshoot) = edge_scroll_adjustment(
|
2025-10-29 17:37:02 +02:00
|
|
|
BUFFER_HEIGHT + 45.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
2025-11-07 06:47:42 +02:00
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
0.0,
|
2025-10-29 17:37:02 +02:00
|
|
|
);
|
|
|
|
|
assert_eq!(delta, -2);
|
|
|
|
|
assert!((row - MAX_ROW).abs() < f32::EPSILON);
|
2025-11-07 06:47:42 +02:00
|
|
|
assert!((remainder - 0.25).abs() < f32::EPSILON);
|
|
|
|
|
|
|
|
|
|
let (delta, _, remainder, _, _) = edge_scroll_adjustment(
|
|
|
|
|
BUFFER_HEIGHT + 45.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
|
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(delta, 0);
|
|
|
|
|
assert!((remainder - 0.25).abs() < f32::EPSILON);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn edge_scroll_forced_increment_continues_scrolling() {
|
|
|
|
|
let (delta, _, remainder, direction, overshoot) = edge_scroll_adjustment(
|
|
|
|
|
-5.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
0.0,
|
|
|
|
|
EdgeScrollDirection::None,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(delta, 0);
|
|
|
|
|
|
|
|
|
|
let (delta, _, remainder, direction, overshoot) = edge_scroll_adjustment(
|
|
|
|
|
-5.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
|
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
1.0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(delta, 1);
|
|
|
|
|
assert!((remainder - 0.25).abs() < f32::EPSILON);
|
|
|
|
|
|
|
|
|
|
let (delta, _, remainder, _, _) = edge_scroll_adjustment(
|
|
|
|
|
-5.0,
|
|
|
|
|
BUFFER_HEIGHT,
|
|
|
|
|
CELL_HEIGHT,
|
|
|
|
|
MAX_ROW,
|
|
|
|
|
remainder,
|
|
|
|
|
direction,
|
|
|
|
|
overshoot,
|
|
|
|
|
1.0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(delta, 1);
|
|
|
|
|
assert!((remainder - 0.25).abs() < f32::EPSILON);
|
2025-10-29 17:37:02 +02:00
|
|
|
}
|
|
|
|
|
}
|