1513 lines
61 KiB
Rust
1513 lines
61 KiB
Rust
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
use alacritty_terminal::{
|
|
index::{Column as TermColumn, Point as TermPoint, Side as TermSide},
|
|
selection::{Selection, SelectionType},
|
|
term::{TermMode, cell::Flags},
|
|
vte::ansi::{CursorShape, NamedColor},
|
|
};
|
|
use cosmic::widget::menu::key_bind::KeyBind;
|
|
use cosmic::{
|
|
Renderer,
|
|
cosmic_theme::palette::{WithAlpha, blend::Compose},
|
|
iced::{
|
|
Color, Element, Length, Padding, Point, Rectangle, Size, Vector,
|
|
advanced::graphics::text::Raw,
|
|
event::{Event, Status},
|
|
keyboard::{Event as KeyEvent, Key, Modifiers},
|
|
mouse::{self, Button, Event as MouseEvent, ScrollDelta},
|
|
},
|
|
iced_core::{
|
|
Border, Shell,
|
|
clipboard::Clipboard,
|
|
keyboard::key::Named,
|
|
layout::{self, Layout},
|
|
renderer::{self, Quad, Renderer as _},
|
|
text::Renderer as _,
|
|
widget::{
|
|
self, Id, Widget,
|
|
operation::{self, Operation},
|
|
tree,
|
|
},
|
|
},
|
|
theme::Theme,
|
|
};
|
|
use cosmic_text::LayoutGlyph;
|
|
use indexmap::IndexSet;
|
|
use std::{
|
|
array,
|
|
cell::Cell,
|
|
cmp,
|
|
collections::HashMap,
|
|
sync::Mutex,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use crate::{
|
|
Action, Terminal, TerminalScroll, key_bind::key_binds, menu::MenuState,
|
|
mouse_reporter::MouseReporter, terminal::Metadata,
|
|
};
|
|
|
|
pub struct TerminalBox<'a, Message> {
|
|
terminal: &'a Mutex<Terminal>,
|
|
id: Option<Id>,
|
|
border: Border,
|
|
padding: Padding,
|
|
show_headerbar: bool,
|
|
click_timing: Duration,
|
|
context_menu: Option<Point>,
|
|
on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>,
|
|
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
|
|
opacity: Option<f32>,
|
|
mouse_inside_boundary: Option<bool>,
|
|
on_middle_click: Option<Box<dyn Fn() -> Message + 'a>>,
|
|
on_open_hyperlink: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
|
on_window_focused: Option<Box<dyn Fn() -> Message + 'a>>,
|
|
on_window_unfocused: Option<Box<dyn Fn() -> Message + 'a>>,
|
|
key_binds: HashMap<KeyBind, Action>,
|
|
sharp_corners: bool,
|
|
}
|
|
|
|
impl<'a, Message> TerminalBox<'a, Message>
|
|
where
|
|
Message: Clone,
|
|
{
|
|
pub fn new(terminal: &'a Mutex<Terminal>) -> Self {
|
|
Self {
|
|
terminal,
|
|
id: None,
|
|
border: Border::default(),
|
|
padding: Padding::new(0.0),
|
|
show_headerbar: true,
|
|
click_timing: Duration::from_millis(500),
|
|
context_menu: None,
|
|
on_context_menu: None,
|
|
on_mouse_enter: None,
|
|
opacity: None,
|
|
mouse_inside_boundary: None,
|
|
on_middle_click: None,
|
|
key_binds: key_binds(),
|
|
on_open_hyperlink: None,
|
|
on_window_focused: None,
|
|
on_window_unfocused: None,
|
|
sharp_corners: false,
|
|
}
|
|
}
|
|
|
|
pub fn id(mut self, id: Id) -> Self {
|
|
self.id = Some(id);
|
|
self
|
|
}
|
|
|
|
pub fn border<B: Into<Border>>(mut self, border: B) -> Self {
|
|
self.border = border.into();
|
|
self
|
|
}
|
|
|
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
|
self.padding = padding.into();
|
|
self
|
|
}
|
|
|
|
pub fn show_headerbar(mut self, show_headerbar: bool) -> Self {
|
|
self.show_headerbar = show_headerbar;
|
|
self
|
|
}
|
|
|
|
pub fn click_timing(mut self, click_timing: Duration) -> Self {
|
|
self.click_timing = click_timing;
|
|
self
|
|
}
|
|
|
|
pub fn context_menu(mut self, position: Point) -> Self {
|
|
self.context_menu = Some(position);
|
|
self
|
|
}
|
|
|
|
pub fn on_context_menu(
|
|
mut self,
|
|
on_context_menu: impl Fn(Option<MenuState>) -> Message + 'a,
|
|
) -> Self {
|
|
self.on_context_menu = Some(Box::new(on_context_menu));
|
|
self
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
pub fn opacity(mut self, opacity: f32) -> Self {
|
|
self.opacity = Some(opacity);
|
|
self
|
|
}
|
|
|
|
pub fn sharp_corners(mut self, sharp_corners: bool) -> Self {
|
|
self.sharp_corners = sharp_corners;
|
|
self
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
pub fn terminal_box<Message>(terminal: &Mutex<Terminal>) -> TerminalBox<'_, Message>
|
|
where
|
|
Message: Clone,
|
|
{
|
|
TerminalBox::new(terminal)
|
|
}
|
|
|
|
impl<'a, Message> Widget<Message, cosmic::Theme, Renderer> for TerminalBox<'a, Message>
|
|
where
|
|
Message: Clone,
|
|
{
|
|
fn tag(&self) -> tree::Tag {
|
|
tree::Tag::of::<State>()
|
|
}
|
|
|
|
fn state(&self) -> tree::State {
|
|
tree::State::new(State::new())
|
|
}
|
|
|
|
fn size(&self) -> Size<Length> {
|
|
Size::new(Length::Fill, Length::Fill)
|
|
}
|
|
|
|
fn layout(
|
|
&self,
|
|
_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();
|
|
|
|
//TODO: set size?
|
|
|
|
// Update if needed
|
|
if terminal.needs_update {
|
|
terminal.update();
|
|
terminal.needs_update = false;
|
|
}
|
|
|
|
// Calculate layout lines
|
|
terminal.with_buffer(|buffer| {
|
|
let mut layout_lines = 0;
|
|
for line in &buffer.lines {
|
|
if let Some(layout) = line.layout_opt() {
|
|
layout_lines += layout.len()
|
|
}
|
|
}
|
|
|
|
let height = layout_lines as f32 * buffer.metrics().line_height;
|
|
let size = Size::new(limits.max().width, height);
|
|
|
|
layout::Node::new(limits.resolve(Length::Fill, Length::Fill, size))
|
|
})
|
|
}
|
|
|
|
fn operate(
|
|
&self,
|
|
tree: &mut widget::Tree,
|
|
_layout: Layout<'_>,
|
|
_renderer: &Renderer,
|
|
operation: &mut dyn Operation,
|
|
) {
|
|
let state = tree.state.downcast_mut::<State>();
|
|
|
|
operation.focusable(state, self.id.as_ref());
|
|
}
|
|
|
|
fn mouse_interaction(
|
|
&self,
|
|
tree: &widget::Tree,
|
|
layout: Layout<'_>,
|
|
cursor_position: mouse::Cursor,
|
|
_viewport: &Rectangle,
|
|
_renderer: &Renderer,
|
|
) -> mouse::Interaction {
|
|
let state = tree.state.downcast_ref::<State>();
|
|
|
|
if let Some(Dragging::Scrollbar { .. }) = &state.dragging {
|
|
return mouse::Interaction::Idle;
|
|
}
|
|
|
|
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());
|
|
|
|
let x = p.x - self.padding.left;
|
|
let y = p.y - self.padding.top;
|
|
if x >= 0.0
|
|
&& x < buffer_size.0.unwrap_or(0.0)
|
|
&& y >= 0.0
|
|
&& y < buffer_size.1.unwrap_or(0.0)
|
|
{
|
|
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)));
|
|
if terminal
|
|
.regex_matches
|
|
.iter()
|
|
.any(|bounds| bounds.contains(&location))
|
|
{
|
|
return mouse::Interaction::Pointer;
|
|
}
|
|
}
|
|
|
|
return mouse::Interaction::Text;
|
|
}
|
|
}
|
|
|
|
mouse::Interaction::Idle
|
|
}
|
|
|
|
fn draw(
|
|
&self,
|
|
tree: &widget::Tree,
|
|
renderer: &mut Renderer,
|
|
theme: &Theme,
|
|
_style: &renderer::Style,
|
|
layout: Layout<'_>,
|
|
cursor_position: mouse::Cursor,
|
|
viewport: &Rectangle,
|
|
) {
|
|
let instant = Instant::now();
|
|
|
|
let state = tree.state.downcast_ref::<State>();
|
|
|
|
let cosmic_theme = theme.cosmic();
|
|
// matches the corners to the window border
|
|
let corner_radius = if self.sharp_corners {
|
|
cosmic_theme.radius_0()
|
|
} else {
|
|
cosmic_theme.radius_s()
|
|
}
|
|
.map(|x| if x < 4.0 { x - 1.0 } else { x + 3.0 });
|
|
let scrollbar_w = f32::from(cosmic_theme.spacing.space_xxs);
|
|
|
|
let view_position = layout.position() + [self.padding.left, self.padding.top].into();
|
|
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32)
|
|
- self.padding.horizontal() as i32
|
|
- scrollbar_w as i32;
|
|
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32)
|
|
- self.padding.vertical() as i32;
|
|
|
|
if view_w <= 0 || view_h <= 0 {
|
|
// Zero sized image
|
|
return;
|
|
}
|
|
|
|
let mut terminal = self.terminal.lock().unwrap();
|
|
|
|
// Ensure terminal is the right size
|
|
terminal.resize(view_w as u32, view_h as u32);
|
|
|
|
// Update if needed
|
|
if terminal.needs_update {
|
|
terminal.update();
|
|
terminal.needs_update = false;
|
|
}
|
|
|
|
// Render default background
|
|
{
|
|
let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
|
|
let background_color = shade(meta.bg, state.is_focused);
|
|
|
|
renderer.fill_quad(
|
|
Quad {
|
|
bounds: layout.bounds(),
|
|
border: Border {
|
|
radius: if self.show_headerbar {
|
|
[0.0, 0.0, corner_radius[2], corner_radius[3]].into()
|
|
} else {
|
|
corner_radius.into()
|
|
},
|
|
width: self.border.width,
|
|
color: self.border.color,
|
|
},
|
|
..Default::default()
|
|
},
|
|
Color::new(
|
|
f32::from(background_color.r()) / 255.0,
|
|
f32::from(background_color.g()) / 255.0,
|
|
f32::from(background_color.b()) / 255.0,
|
|
match self.opacity {
|
|
Some(opacity) => opacity,
|
|
None => f32::from(background_color.a()) / 255.0,
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Render cell backgrounds that do not match default
|
|
terminal.with_buffer(|buffer| {
|
|
for run in buffer.layout_runs() {
|
|
struct BgRect<'a> {
|
|
default_metadata: usize,
|
|
metadata: usize,
|
|
glyph_font_size: f32,
|
|
start_x: f32,
|
|
end_x: f32,
|
|
line_height: f32,
|
|
line_top: f32,
|
|
view_position: Point,
|
|
metadata_set: &'a IndexSet<Metadata>,
|
|
}
|
|
|
|
impl<'a> BgRect<'a> {
|
|
fn update<Renderer: renderer::Renderer>(
|
|
&mut self,
|
|
glyph: &LayoutGlyph,
|
|
renderer: &mut Renderer,
|
|
is_focused: bool,
|
|
) {
|
|
if glyph.metadata == self.metadata {
|
|
self.end_x = glyph.x + glyph.w;
|
|
} else {
|
|
self.fill(renderer, is_focused);
|
|
self.metadata = glyph.metadata;
|
|
self.glyph_font_size = glyph.font_size;
|
|
self.start_x = glyph.x;
|
|
self.end_x = glyph.x + glyph.w;
|
|
}
|
|
}
|
|
|
|
fn fill<Renderer: renderer::Renderer>(
|
|
&mut self,
|
|
renderer: &mut Renderer,
|
|
is_focused: bool,
|
|
) {
|
|
let cosmic_text_to_iced_color = |color: cosmic_text::Color| {
|
|
Color::new(
|
|
f32::from(color.r()) / 255.0,
|
|
f32::from(color.g()) / 255.0,
|
|
f32::from(color.b()) / 255.0,
|
|
f32::from(color.a()) / 255.0,
|
|
)
|
|
};
|
|
|
|
macro_rules! mk_pos_offset {
|
|
($x_offset:expr, $bottom_offset:expr) => {
|
|
Vector::new(
|
|
self.start_x + $x_offset,
|
|
self.line_top + self.line_height - $bottom_offset,
|
|
)
|
|
};
|
|
}
|
|
|
|
macro_rules! mk_quad {
|
|
($pos_offset:expr, $style_line_height:expr, $width:expr) => {
|
|
Quad {
|
|
bounds: Rectangle::new(
|
|
self.view_position + $pos_offset,
|
|
Size::new($width, $style_line_height),
|
|
),
|
|
..Default::default()
|
|
}
|
|
};
|
|
($pos_offset:expr, $style_line_height:expr) => {
|
|
mk_quad!($pos_offset, $style_line_height, self.end_x - self.start_x)
|
|
};
|
|
}
|
|
|
|
let metadata = &self.metadata_set[self.metadata];
|
|
if metadata.bg != self.metadata_set[self.default_metadata].bg {
|
|
let color = shade(metadata.bg, is_focused);
|
|
renderer.fill_quad(
|
|
mk_quad!(mk_pos_offset!(0.0, self.line_height), self.line_height),
|
|
cosmic_text_to_iced_color(color),
|
|
);
|
|
}
|
|
|
|
if !metadata.flags.is_empty() {
|
|
let style_line_height = (self.glyph_font_size / 10.0).clamp(1.0, 16.0);
|
|
|
|
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);
|
|
|
|
let pos_offset2 = mk_pos_offset!(0.0, bottom_offset / 2.0);
|
|
let underline2_quad = mk_quad!(pos_offset2, style_line_height);
|
|
|
|
renderer.fill_quad(underline1_quad, line_color);
|
|
renderer.fill_quad(underline2_quad, line_color);
|
|
}
|
|
|
|
// rects is a slice of (width, Option<y>), `None` means a gap.
|
|
let mut draw_repeated = |rects: &[(f32, Option<f32>)]| {
|
|
let full_width = self.end_x - self.start_x;
|
|
let pattern_len: f32 = rects.iter().map(|x| x.0).sum(); // total length of the pattern
|
|
let mut accu_width = 0.0;
|
|
let mut index = {
|
|
let in_pattern = self.start_x % pattern_len;
|
|
|
|
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 {
|
|
// draw first rect cropped to span
|
|
let pos_offset = mk_pos_offset!(accu_width, height);
|
|
let underline_quad =
|
|
mk_quad!(pos_offset, style_line_height, width);
|
|
renderer.fill_quad(underline_quad, line_color);
|
|
}
|
|
index = i + 1;
|
|
accu_width += width;
|
|
break;
|
|
}
|
|
}
|
|
index // index of first full rect
|
|
};
|
|
while accu_width < full_width {
|
|
let (width, x) = rects[index % rects.len()];
|
|
let cropped_width = width.min(full_width - accu_width);
|
|
if let Some(height) = x {
|
|
let pos_offset = mk_pos_offset!(accu_width, height);
|
|
let underline_quad =
|
|
mk_quad!(pos_offset, style_line_height, cropped_width);
|
|
renderer.fill_quad(underline_quad, line_color);
|
|
}
|
|
accu_width += cropped_width;
|
|
index += 1;
|
|
}
|
|
};
|
|
|
|
if metadata.flags.contains(Flags::DOTTED_UNDERLINE) {
|
|
let bottom_offset = style_line_height * 2.0;
|
|
let dot = (2.0, Some(bottom_offset));
|
|
let gap = (2.0, None);
|
|
draw_repeated(&[dot, gap]);
|
|
}
|
|
|
|
if metadata.flags.contains(Flags::DASHED_UNDERLINE) {
|
|
let bottom_offset = style_line_height * 2.0;
|
|
let dash = (6.0, Some(bottom_offset));
|
|
let gap = (3.0, None);
|
|
draw_repeated(&[dash, gap]);
|
|
}
|
|
|
|
if metadata.flags.contains(Flags::UNDERCURL) {
|
|
let style_line_height = style_line_height.floor();
|
|
let bottom_offset = style_line_height * 1.5;
|
|
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),
|
|
),
|
|
0 => (1.0, Some(bottom_offset)),
|
|
_ => unreachable!(),
|
|
});
|
|
draw_repeated(&pattern)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let default_metadata = terminal.default_attrs().metadata;
|
|
let metadata_set = &terminal.metadata_set;
|
|
let mut bg_rect = BgRect {
|
|
default_metadata,
|
|
metadata: default_metadata,
|
|
glyph_font_size: 0.0,
|
|
start_x: 0.0,
|
|
end_x: 0.0,
|
|
line_height: buffer.metrics().line_height,
|
|
line_top: run.line_top,
|
|
view_position,
|
|
metadata_set,
|
|
};
|
|
for glyph in run.glyphs {
|
|
bg_rect.update(glyph, renderer, state.is_focused);
|
|
}
|
|
bg_rect.fill(renderer, state.is_focused);
|
|
}
|
|
});
|
|
|
|
renderer.fill_raw(Raw {
|
|
buffer: terminal.buffer_weak(),
|
|
position: view_position,
|
|
color: Color::new(1.0, 1.0, 1.0, 1.0), // TODO
|
|
clip_bounds: Rectangle::new(view_position, Size::new(view_w as f32, view_h as f32)),
|
|
});
|
|
|
|
// Draw scrollbar
|
|
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),
|
|
);
|
|
|
|
let pressed = matches!(&state.dragging, Some(Dragging::Scrollbar { .. }));
|
|
|
|
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
|
|
.background
|
|
.component
|
|
.pressed
|
|
.without_alpha()
|
|
.with_alpha(0.5)
|
|
.over(base_color)
|
|
.into()
|
|
} else if hover {
|
|
// hover_state_color, 0.2
|
|
cosmic_theme
|
|
.background
|
|
.component
|
|
.hover
|
|
.without_alpha()
|
|
.with_alpha(0.2)
|
|
.over(base_color)
|
|
.into()
|
|
} else {
|
|
base_color.into()
|
|
};
|
|
|
|
renderer.fill_quad(
|
|
Quad {
|
|
bounds: scrollbar_draw,
|
|
border: Border {
|
|
radius: (scrollbar_draw.width / 2.0).into(),
|
|
width: 0.0,
|
|
color: Color::TRANSPARENT,
|
|
},
|
|
..Default::default()
|
|
},
|
|
scrollbar_color,
|
|
);
|
|
|
|
state.scrollbar_rect.set(scrollbar_rect);
|
|
} else {
|
|
state.scrollbar_rect.set(Rectangle::default())
|
|
}
|
|
|
|
// Draw cursor
|
|
{
|
|
let cursor = terminal.term.lock().renderable_content().cursor;
|
|
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)),
|
|
..Default::default()
|
|
};
|
|
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),
|
|
),
|
|
..Default::default()
|
|
};
|
|
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
|
|
}
|
|
}
|
|
|
|
let duration = instant.elapsed();
|
|
log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration);
|
|
}
|
|
|
|
fn on_event(
|
|
&mut self,
|
|
tree: &mut widget::Tree,
|
|
event: Event,
|
|
layout: Layout<'_>,
|
|
cursor_position: mouse::Cursor,
|
|
_renderer: &Renderer,
|
|
_clipboard: &mut dyn Clipboard,
|
|
shell: &mut Shell<'_, Message>,
|
|
_viewport: &Rectangle<f32>,
|
|
) -> Status {
|
|
let state = tree.state.downcast_mut::<State>();
|
|
let scrollbar_rect = state.scrollbar_rect.get();
|
|
let mut terminal = self.terminal.lock().unwrap();
|
|
let buffer_size = terminal.with_buffer(|buffer| buffer.size());
|
|
|
|
let is_app_cursor = terminal.term.lock().mode().contains(TermMode::APP_CURSOR);
|
|
let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE);
|
|
let mut status = Status::Ignored;
|
|
match event {
|
|
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());
|
|
}
|
|
}
|
|
cosmic::iced::window::Event::Unfocused => {
|
|
state.is_focused = false;
|
|
if let Some(on_window_unfocused) = &self.on_window_unfocused {
|
|
shell.publish(on_window_unfocused());
|
|
}
|
|
}
|
|
_ => {}
|
|
},
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
key: Key::Named(named),
|
|
modified_key: Key::Named(modified_named),
|
|
modifiers,
|
|
text,
|
|
..
|
|
}) if state.is_focused && named == modified_named => {
|
|
for key_bind in self.key_binds.keys() {
|
|
if key_bind.matches(modifiers, &Key::Named(named)) {
|
|
return Status::Captured;
|
|
}
|
|
}
|
|
|
|
let mod_no = calculate_modifier_number(state);
|
|
let escape_code = match named {
|
|
Named::Insert => csi("2", "~", mod_no),
|
|
Named::Delete => csi("3", "~", mod_no),
|
|
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)
|
|
}
|
|
}
|
|
Named::ArrowUp => {
|
|
if is_app_cursor {
|
|
ss3("A", mod_no)
|
|
} else {
|
|
csi2("A", mod_no)
|
|
}
|
|
}
|
|
Named::ArrowDown => {
|
|
if is_app_cursor {
|
|
ss3("B", mod_no)
|
|
} else {
|
|
csi2("B", mod_no)
|
|
}
|
|
}
|
|
Named::ArrowRight => {
|
|
if is_app_cursor {
|
|
ss3("C", mod_no)
|
|
} else {
|
|
csi2("C", mod_no)
|
|
}
|
|
}
|
|
Named::ArrowLeft => {
|
|
if is_app_cursor {
|
|
ss3("D", mod_no)
|
|
} else {
|
|
csi2("D", mod_no)
|
|
}
|
|
}
|
|
Named::End => {
|
|
if modifiers.shift() {
|
|
terminal.scroll(TerminalScroll::Bottom);
|
|
None
|
|
} else if is_app_cursor {
|
|
ss3("F", mod_no)
|
|
} else {
|
|
csi2("F", mod_no)
|
|
}
|
|
}
|
|
Named::Home => {
|
|
if modifiers.shift() {
|
|
terminal.scroll(TerminalScroll::Top);
|
|
None
|
|
} else if is_app_cursor {
|
|
ss3("H", mod_no)
|
|
} else {
|
|
csi2("H", mod_no)
|
|
}
|
|
}
|
|
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),
|
|
_ => None,
|
|
};
|
|
if let Some(escape_code) = escape_code {
|
|
terminal.input_scroll(escape_code);
|
|
return Status::Captured;
|
|
}
|
|
|
|
//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 { "" };
|
|
match named {
|
|
Named::Backspace => {
|
|
let code = if modifiers.control() { "\x08" } else { "\x7f" };
|
|
terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes());
|
|
status = Status::Captured;
|
|
}
|
|
Named::Enter => {
|
|
terminal.input_scroll(format!("{}{}", alt_prefix, "\x0D").into_bytes());
|
|
status = Status::Captured;
|
|
}
|
|
Named::Escape => {
|
|
//Escape with any modifier will cancel selection
|
|
let had_selection = {
|
|
let mut term = terminal.term.lock();
|
|
term.selection.take().is_some()
|
|
};
|
|
if had_selection {
|
|
terminal.update();
|
|
} else {
|
|
terminal.input_scroll(format!("{}{}", alt_prefix, "\x1B").into_bytes());
|
|
}
|
|
status = Status::Captured;
|
|
}
|
|
Named::Space => {
|
|
// Keep this instead of hardcoding the space to allow for dead keys
|
|
let character = text.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());
|
|
}
|
|
status = Status::Captured;
|
|
}
|
|
Named::Tab => {
|
|
let code = if modifiers.shift() { "\x1b[Z" } else { "\x09" };
|
|
terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes());
|
|
status = Status::Captured;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
|
state.modifiers = modifiers;
|
|
|
|
if modifiers.contains(Modifiers::CTRL) || terminal.active_regex_match.is_some() {
|
|
//Might need to update the url regex highlight,
|
|
//so we need to calculate the mouse position
|
|
let location = if let Some(p) = cursor_position.position() {
|
|
let x = (p.x - layout.bounds().x) - self.padding.left;
|
|
let y = (p.y - layout.bounds().y) - self.padding.top;
|
|
//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
|
|
};
|
|
update_active_regex_match(&mut terminal, location, Some(&state.modifiers));
|
|
}
|
|
}
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
text,
|
|
modifiers,
|
|
key,
|
|
..
|
|
}) if state.is_focused => {
|
|
for key_bind in self.key_binds.keys() {
|
|
if key_bind.matches(modifiers, &key) {
|
|
return Status::Captured;
|
|
}
|
|
}
|
|
let character = text.and_then(|c| c.chars().next()).unwrap_or_default();
|
|
match (
|
|
modifiers.logo(),
|
|
modifiers.control(),
|
|
modifiers.alt(),
|
|
modifiers.shift(),
|
|
) {
|
|
(true, _, _, _) => {
|
|
// Ignore super
|
|
}
|
|
(false, true, true, _) => {
|
|
// Handle ctrl-alt for non-control characters
|
|
// 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());
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
(false, true, _, false) => {
|
|
// 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);
|
|
terminal.input_scroll(str.as_bytes().to_vec());
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
(false, true, _, true) => {
|
|
//This is normally Ctrl+Minus, but since that
|
|
//is taken by zoom, we send that code for
|
|
//Ctrl+Underline instead, like xterm and
|
|
//gnome-terminal
|
|
if key == Key::Character("_".into()) {
|
|
terminal.input_scroll(b"\x1F".as_slice());
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
(false, false, true, _) => {
|
|
if !character.is_control() {
|
|
// 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());
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
(false, false, false, _) => {
|
|
// 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);
|
|
terminal.input_scroll(str.as_bytes().to_vec());
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Event::Mouse(MouseEvent::ButtonPressed(button)) => {
|
|
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 {
|
|
terminal.report_mouse(event, &state.modifiers, col as u32, row as u32);
|
|
} else {
|
|
state.is_focused = true;
|
|
|
|
// Handle left click drag
|
|
#[allow(clippy::collapsible_if)]
|
|
if let Button::Left = button {
|
|
let x = p.x - self.padding.left;
|
|
let y = p.y - self.padding.top;
|
|
if x >= 0.0
|
|
&& x < buffer_size.0.unwrap_or(0.0)
|
|
&& y >= 0.0
|
|
&& y < buffer_size.1.unwrap_or(0.0)
|
|
{
|
|
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
|
|
}
|
|
} else {
|
|
ClickKind::Single
|
|
};
|
|
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 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)
|
|
}
|
|
};
|
|
{
|
|
let mut term = terminal.term.lock();
|
|
term.selection = Some(selection);
|
|
}
|
|
terminal.needs_update = true;
|
|
state.click = Some((click_kind, Instant::now()));
|
|
state.dragging = Some(Dragging::Buffer);
|
|
} else if scrollbar_rect.contains(Point::new(x, y)) {
|
|
if let Some(start_scroll) = terminal.scrollbar() {
|
|
state.dragging = Some(Dragging::Scrollbar {
|
|
start_y: y,
|
|
start_scroll,
|
|
});
|
|
}
|
|
} else if x >= scrollbar_rect.x
|
|
&& x < (scrollbar_rect.x + scrollbar_rect.width)
|
|
{
|
|
if terminal.scrollbar().is_some() {
|
|
let scroll_ratio = terminal
|
|
.with_buffer(|buffer| y / buffer.size().1.unwrap_or(1.0));
|
|
terminal.scroll_to(scroll_ratio);
|
|
if let Some(start_scroll) = terminal.scrollbar() {
|
|
state.dragging = Some(Dragging::Scrollbar {
|
|
start_y: y,
|
|
start_scroll,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else if button == Button::Middle {
|
|
if let Some(on_middle_click) = &self.on_middle_click {
|
|
shell.publish(on_middle_click());
|
|
}
|
|
}
|
|
// Update context menu state
|
|
if let Some(on_context_menu) = &self.on_context_menu {
|
|
match self.context_menu {
|
|
Some(_) => {
|
|
shell.publish(on_context_menu(None));
|
|
}
|
|
None => {
|
|
if button == Button::Right {
|
|
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);
|
|
shell.publish(on_context_menu(Some(MenuState {
|
|
position: Some(p),
|
|
link,
|
|
})));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
}
|
|
Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => {
|
|
state.dragging = None;
|
|
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;
|
|
|
|
let location = terminal
|
|
.viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize)));
|
|
if state.modifiers.control() {
|
|
if let Some(on_open_hyperlink) = &self.on_open_hyperlink {
|
|
if let Some(hyperlink) = get_hyperlink(&terminal, location) {
|
|
shell.publish(on_open_hyperlink(hyperlink));
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
}
|
|
|
|
if is_mouse_mode {
|
|
terminal.report_mouse(event, &state.modifiers, col as u32, row as u32);
|
|
} else {
|
|
status = Status::Captured;
|
|
}
|
|
} else {
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
Event::Mouse(MouseEvent::ButtonReleased(_button)) => {
|
|
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 {
|
|
terminal.report_mouse(event, &state.modifiers, col as u32, row as u32);
|
|
}
|
|
}
|
|
}
|
|
Event::Mouse(MouseEvent::CursorMoved { .. }) => {
|
|
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);
|
|
}
|
|
}
|
|
if let Some(p) = cursor_position.position() {
|
|
let x = (p.x - layout.bounds().x) - self.padding.left;
|
|
let y = (p.y - layout.bounds().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),
|
|
);
|
|
|
|
if is_mouse_mode {
|
|
terminal.report_mouse(event, &state.modifiers, col as u32, row as u32);
|
|
} else {
|
|
if let Some(dragging) = &state.dragging {
|
|
match dragging {
|
|
Dragging::Buffer => {
|
|
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);
|
|
}
|
|
}
|
|
terminal.needs_update = true;
|
|
}
|
|
Dragging::Scrollbar {
|
|
start_y,
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
}
|
|
Event::Mouse(MouseEvent::WheelScrolled { delta }) => {
|
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
|
if is_mouse_mode {
|
|
let x = (p.x - layout.bounds().x) - self.padding.left;
|
|
let y = (p.y - layout.bounds().y) - self.padding.top;
|
|
//TODO: better calculation of position
|
|
let col = x / terminal.size().cell_width;
|
|
let row = y / terminal.size().cell_height;
|
|
terminal.scroll_mouse(delta, &state.modifiers, col as u32, row as u32);
|
|
} else {
|
|
if terminal.term.lock().mode().contains(TermMode::ALT_SCREEN) {
|
|
MouseReporter::report_mouse_wheel_as_arrows(
|
|
&terminal,
|
|
terminal.size().cell_width,
|
|
terminal.size().cell_height,
|
|
delta,
|
|
);
|
|
status = Status::Captured;
|
|
} else {
|
|
match delta {
|
|
ScrollDelta::Lines { x: _, y } => {
|
|
//TODO: this adjustment is just a guess!
|
|
state.scroll_pixels = 0.0;
|
|
let lines = (-y * 6.0) as i32;
|
|
if lines != 0 {
|
|
terminal.scroll(TerminalScroll::Delta(-lines));
|
|
}
|
|
status = Status::Captured;
|
|
}
|
|
ScrollDelta::Pixels { x: _, y } => {
|
|
//TODO: this adjustment is just a guess!
|
|
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;
|
|
}
|
|
if lines != 0 {
|
|
terminal.scroll(TerminalScroll::Delta(-lines));
|
|
}
|
|
status = Status::Captured;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
{
|
|
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),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
status
|
|
}
|
|
}
|
|
|
|
fn get_hyperlink(
|
|
terminal: &std::sync::MutexGuard<'_, Terminal>,
|
|
location: TermPoint,
|
|
) -> Option<String> {
|
|
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
|
|
}
|
|
}
|
|
|
|
fn update_active_regex_match(
|
|
terminal: &mut std::sync::MutexGuard<'_, Terminal>,
|
|
location: Option<TermPoint>,
|
|
modifiers: Option<&Modifiers>,
|
|
) {
|
|
//Do not update any highlights if
|
|
//there is a context_menu shown
|
|
//to the user
|
|
if terminal.context_menu.is_some() {
|
|
return;
|
|
}
|
|
|
|
//Require CTRL for keyboard and mouse interaction
|
|
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;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
let Some(location) = location else {
|
|
if terminal.active_regex_match.is_some() {
|
|
terminal.active_regex_match = None;
|
|
terminal.needs_update = true;
|
|
}
|
|
return;
|
|
};
|
|
if let Some(match_) = terminal
|
|
.regex_matches
|
|
.iter()
|
|
.find(|bounds| bounds.contains(&location))
|
|
{
|
|
'update: {
|
|
if let Some(active_match) = &terminal.active_regex_match {
|
|
if active_match == match_ {
|
|
break 'update;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
fn shade(color: cosmic_text::Color, is_focused: bool) -> cosmic_text::Color {
|
|
if is_focused {
|
|
color
|
|
} else {
|
|
let shade = 0.92;
|
|
cosmic_text::Color::rgba(
|
|
(f32::from(color.r()) * shade) as u8,
|
|
(f32::from(color.g()) * shade) as u8,
|
|
(f32::from(color.b()) * shade) as u8,
|
|
color.a(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl<'a, Message> From<TerminalBox<'a, Message>> for Element<'a, Message, cosmic::Theme, Renderer>
|
|
where
|
|
Message: Clone + 'a,
|
|
{
|
|
fn from(terminal_box: TerminalBox<'a, Message>) -> Self {
|
|
Self::new(terminal_box)
|
|
}
|
|
}
|
|
|
|
enum ClickKind {
|
|
Single,
|
|
Double,
|
|
Triple,
|
|
}
|
|
|
|
enum Dragging {
|
|
Buffer,
|
|
Scrollbar {
|
|
start_y: f32,
|
|
start_scroll: (f32, f32),
|
|
},
|
|
}
|
|
|
|
pub struct State {
|
|
modifiers: Modifiers,
|
|
click: Option<(ClickKind, Instant)>,
|
|
dragging: Option<Dragging>,
|
|
is_focused: bool,
|
|
scroll_pixels: f32,
|
|
scrollbar_rect: Cell<Rectangle<f32>>,
|
|
}
|
|
|
|
impl State {
|
|
/// Creates a new [`State`].
|
|
pub fn new() -> Self {
|
|
Self {
|
|
modifiers: Modifiers::empty(),
|
|
click: None,
|
|
dragging: None,
|
|
is_focused: false,
|
|
scroll_pixels: 0.0,
|
|
scrollbar_rect: Cell::new(Rectangle::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/*
|
|
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)
|
|
*/
|
|
fn calculate_modifier_number(state: &State) -> u8 {
|
|
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;
|
|
}
|
|
mod_no + 1
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn csi(code: &str, suffix: &str, modifiers: u8) -> Option<Vec<u8>> {
|
|
if modifiers == 1 {
|
|
Some(format!("\x1B[{code}{suffix}").into_bytes())
|
|
} else {
|
|
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())
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn ss3(code: &str, modifiers: u8) -> Option<Vec<u8>> {
|
|
if modifiers == 1 {
|
|
Some(format!("\x1B\x4F{code}").into_bytes())
|
|
} else {
|
|
Some(format!("\x1B[1;{modifiers}{code}").into_bytes())
|
|
}
|
|
}
|