better floating nac
This commit is contained in:
@@ -64,6 +64,7 @@ floating_max_height = 640
|
|||||||
"Alt+F" = "toggle_floats"
|
"Alt+F" = "toggle_floats"
|
||||||
"Alt+M" = "new_pane"
|
"Alt+M" = "new_pane"
|
||||||
"Alt+H" = "focus_left"
|
"Alt+H" = "focus_left"
|
||||||
|
# With floating panes shown, J/K cycle forward/backward through them.
|
||||||
"Alt+J" = "focus_down"
|
"Alt+J" = "focus_down"
|
||||||
"Alt+K" = "focus_up"
|
"Alt+K" = "focus_up"
|
||||||
"Alt+L" = "focus_right"
|
"Alt+L" = "focus_right"
|
||||||
|
|||||||
+93
-5
@@ -152,6 +152,7 @@ pub struct Tab {
|
|||||||
pub floats: Vec<PaneId>,
|
pub floats: Vec<PaneId>,
|
||||||
pub active: PaneId,
|
pub active: PaneId,
|
||||||
pub floats_visible: bool,
|
pub floats_visible: bool,
|
||||||
|
last_active_float: Option<PaneId>,
|
||||||
pub zoomed: bool,
|
pub zoomed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +164,7 @@ impl Tab {
|
|||||||
floats: vec![],
|
floats: vec![],
|
||||||
active: pane,
|
active: pane,
|
||||||
floats_visible: false,
|
floats_visible: false,
|
||||||
|
last_active_float: None,
|
||||||
zoomed: false,
|
zoomed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,6 +179,9 @@ impl Tab {
|
|||||||
pub fn detach(&mut self, id: PaneId) {
|
pub fn detach(&mut self, id: PaneId) {
|
||||||
self.root = self.root.take().and_then(|r| r.remove(id));
|
self.root = self.root.take().and_then(|r| r.remove(id));
|
||||||
self.floats.retain(|p| *p != id);
|
self.floats.retain(|p| *p != id);
|
||||||
|
if self.last_active_float == Some(id) {
|
||||||
|
self.last_active_float = None;
|
||||||
|
}
|
||||||
if self.floats.is_empty() {
|
if self.floats.is_empty() {
|
||||||
self.floats_visible = false;
|
self.floats_visible = false;
|
||||||
}
|
}
|
||||||
@@ -201,17 +206,29 @@ impl Tab {
|
|||||||
self.floats_visible = !self.floats_visible;
|
self.floats_visible = !self.floats_visible;
|
||||||
self.zoomed = false;
|
self.zoomed = false;
|
||||||
if self.floats_visible {
|
if self.floats_visible {
|
||||||
if let Some(id) = self.floats.last() {
|
if let Some(id) = self
|
||||||
self.active = *id;
|
.last_active_float
|
||||||
}
|
.filter(|id| self.floats.contains(id))
|
||||||
} else if self.floats.contains(&self.active)
|
.or_else(|| self.floats.last().copied())
|
||||||
&& let Some(root) = &self.root
|
|
||||||
{
|
{
|
||||||
|
self.active = id;
|
||||||
|
}
|
||||||
|
} else if self.floats.contains(&self.active) {
|
||||||
|
self.last_active_float = Some(self.active);
|
||||||
|
if let Some(root) = &self.root {
|
||||||
self.active = root.first();
|
self.active = root.first();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn focus_direction(&mut self, direction: Direction, rects: &[(PaneId, Rect)]) {
|
pub fn focus_direction(&mut self, direction: Direction, rects: &[(PaneId, Rect)]) {
|
||||||
|
if self.floats_visible
|
||||||
|
&& !self.floats.is_empty()
|
||||||
|
&& matches!(direction, Direction::Down | Direction::Up)
|
||||||
|
{
|
||||||
|
self.cycle(matches!(direction, Direction::Up));
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some((_, source)) = rects
|
let Some((_, source)) = rects
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(id, _)| *id == self.active && self.is_visible(*id))
|
.find(|(id, _)| *id == self.active && self.is_visible(*id))
|
||||||
@@ -322,6 +339,51 @@ mod tests {
|
|||||||
assert!(tab.is_visible(2));
|
assert!(tab.is_visible(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn showing_floats_restores_the_pane_active_when_hidden() {
|
||||||
|
let mut tab = Tab::new(1, 1);
|
||||||
|
tab.root.as_mut().unwrap().split(1, 2, Axis::Horizontal);
|
||||||
|
tab.floats = vec![3, 4, 5];
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 5);
|
||||||
|
tab.cycle(true);
|
||||||
|
assert_eq!(tab.active, 4);
|
||||||
|
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 1);
|
||||||
|
tab.cycle(false);
|
||||||
|
assert_eq!(tab.active, 2);
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 4);
|
||||||
|
|
||||||
|
tab.cycle(true);
|
||||||
|
tab.toggle_floats();
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 3);
|
||||||
|
|
||||||
|
tab.root = None;
|
||||||
|
tab.toggle_floats();
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn showing_floats_falls_back_when_remembered_pane_is_detached() {
|
||||||
|
for dock in [false, true] {
|
||||||
|
let mut tab = Tab::new(1, 1);
|
||||||
|
tab.floats = vec![2, 3];
|
||||||
|
tab.floats_visible = true;
|
||||||
|
tab.active = 2;
|
||||||
|
tab.toggle_floats();
|
||||||
|
tab.detach(2);
|
||||||
|
if dock {
|
||||||
|
tab.root.as_mut().unwrap().split(1, 2, Axis::Horizontal);
|
||||||
|
}
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn directional_focus_uses_geometry_and_ignores_hidden_or_foreign_panes() {
|
fn directional_focus_uses_geometry_and_ignores_hidden_or_foreign_panes() {
|
||||||
let mut tab = Tab::new(1, 1);
|
let mut tab = Tab::new(1, 1);
|
||||||
@@ -375,6 +437,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn up_and_down_cycle_visible_floats_without_geometry() {
|
||||||
|
let mut tab = Tab::new(1, 1);
|
||||||
|
tab.floats = vec![2, 3, 4];
|
||||||
|
tab.floats_visible = true;
|
||||||
|
for (direction, expected) in [
|
||||||
|
(Direction::Down, 2),
|
||||||
|
(Direction::Down, 3),
|
||||||
|
(Direction::Down, 4),
|
||||||
|
(Direction::Down, 2),
|
||||||
|
(Direction::Up, 4),
|
||||||
|
(Direction::Up, 3),
|
||||||
|
(Direction::Up, 2),
|
||||||
|
] {
|
||||||
|
tab.focus_direction(direction, &[]);
|
||||||
|
assert_eq!(tab.active, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 1);
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn visible_floats_constrain_directional_and_cyclic_navigation() {
|
fn visible_floats_constrain_directional_and_cyclic_navigation() {
|
||||||
let mut tab = Tab::new(1, 1);
|
let mut tab = Tab::new(1, 1);
|
||||||
@@ -395,6 +481,8 @@ mod tests {
|
|||||||
(Direction::Right, 4),
|
(Direction::Right, 4),
|
||||||
(Direction::Down, 5),
|
(Direction::Down, 5),
|
||||||
(Direction::Left, 6),
|
(Direction::Left, 6),
|
||||||
|
(Direction::Up, 5),
|
||||||
|
(Direction::Up, 4),
|
||||||
(Direction::Up, 3),
|
(Direction::Up, 3),
|
||||||
] {
|
] {
|
||||||
tab.focus_direction(direction, &rects);
|
tab.focus_direction(direction, &rects);
|
||||||
|
|||||||
+187
-18
@@ -9,12 +9,14 @@ use libghostty_vt::{
|
|||||||
kitty::graphics::{DecodePng, DecodedImage, ImageFormat, Layer, PlacementIterator},
|
kitty::graphics::{DecodePng, DecodedImage, ImageFormat, Layer, PlacementIterator},
|
||||||
render::{CellIterator, Colors, CursorVisualStyle, Dirty, RowIterator},
|
render::{CellIterator, Colors, CursorVisualStyle, Dirty, RowIterator},
|
||||||
screen::CellWide,
|
screen::CellWide,
|
||||||
style::{RgbColor, Underline},
|
style::{RgbColor, Style, Underline},
|
||||||
|
terminal::Mode,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
io::Cursor,
|
io::Cursor,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Where a pane's terminal is drawn this frame, and the colors that are slerm's
|
/// Where a pane's terminal is drawn this frame, and the colors that are slerm's
|
||||||
@@ -143,6 +145,23 @@ struct CachedRow {
|
|||||||
backgrounds: Vec<egui::Shape>,
|
backgrounds: Vec<egui::Shape>,
|
||||||
foregrounds: Vec<egui::Shape>,
|
foregrounds: Vec<egui::Shape>,
|
||||||
selection: Option<(u16, u16)>,
|
selection: Option<(u16, u16)>,
|
||||||
|
cells: Vec<CachedCell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct CellAppearance {
|
||||||
|
text: String,
|
||||||
|
style: Style,
|
||||||
|
fg: Color32,
|
||||||
|
bg: Color32,
|
||||||
|
background: bool,
|
||||||
|
wide: CellWide,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CachedCell {
|
||||||
|
appearance: CellAppearance,
|
||||||
|
backgrounds: std::ops::Range<usize>,
|
||||||
|
foregrounds: std::ops::Range<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_glyph(painter: &Painter, text: String, font: &FontId, height: f32) -> Arc<egui::Galley> {
|
fn layout_glyph(painter: &Painter, text: String, font: &FontId, height: f32) -> Arc<egui::Galley> {
|
||||||
@@ -205,8 +224,12 @@ pub struct Renderer {
|
|||||||
cached_selection: (Color32, Color32),
|
cached_selection: (Color32, Color32),
|
||||||
cached_cell: egui::Vec2,
|
cached_cell: egui::Vec2,
|
||||||
cached_cols: u16,
|
cached_cols: u16,
|
||||||
|
frame: Vec<egui::epaint::ClippedShape>,
|
||||||
|
sync_started: Option<Instant>,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
rebuilt_rows: usize,
|
rebuilt_rows: usize,
|
||||||
|
#[cfg(test)]
|
||||||
|
rebuilt_cells: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
@@ -225,8 +248,12 @@ impl Renderer {
|
|||||||
cached_selection: (Color32::PLACEHOLDER, Color32::PLACEHOLDER),
|
cached_selection: (Color32::PLACEHOLDER, Color32::PLACEHOLDER),
|
||||||
cached_cell: egui::Vec2::ZERO,
|
cached_cell: egui::Vec2::ZERO,
|
||||||
cached_cols: 0,
|
cached_cols: 0,
|
||||||
|
frame: Vec::new(),
|
||||||
|
sync_started: None,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
rebuilt_rows: 0,
|
rebuilt_rows: 0,
|
||||||
|
#[cfg(test)]
|
||||||
|
rebuilt_cells: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,9 +283,26 @@ impl Renderer {
|
|||||||
if fonts_changed {
|
if fonts_changed {
|
||||||
self.row_cache.clear();
|
self.row_cache.clear();
|
||||||
self.glyphs.clear();
|
self.glyphs.clear();
|
||||||
|
self.frame.clear();
|
||||||
}
|
}
|
||||||
self.font_marker = Some(marker);
|
self.font_marker = Some(marker);
|
||||||
self.cached_cell = cell;
|
self.cached_cell = cell;
|
||||||
|
// Editors may clear and redraw in separate PTY reads. Keep presenting
|
||||||
|
// the last complete frame until DEC synchronized output ends. Bound
|
||||||
|
// the wait so a crashed application cannot leave the display frozen.
|
||||||
|
if term.mode(Mode::SYNC_OUTPUT)? {
|
||||||
|
let elapsed = self.sync_started.get_or_insert_with(Instant::now).elapsed();
|
||||||
|
let timeout = Duration::from_secs(1);
|
||||||
|
if elapsed < timeout && !self.frame.is_empty() {
|
||||||
|
painter.ctx().request_repaint_after(timeout - elapsed);
|
||||||
|
self.present(&painter, rect);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.sync_started = None;
|
||||||
|
}
|
||||||
|
let screen_rect = rect;
|
||||||
|
let rect = Rect::from_min_size(egui::Pos2::ZERO, rect.size());
|
||||||
let style_fonts = painter.ctx().fonts(|fonts| {
|
let style_fonts = painter.ctx().fonts(|fonts| {
|
||||||
[(false, false), (true, false), (false, true), (true, true)].map(|(bold, italic)| {
|
[(false, false), (true, false), (false, true), (true, true)].map(|(bold, italic)| {
|
||||||
let family = crate::fonts::family(bold, italic);
|
let family = crate::fonts::family(bold, italic);
|
||||||
@@ -304,7 +348,8 @@ impl Renderer {
|
|||||||
self.cached_colors = Some(colors.clone());
|
self.cached_colors = Some(colors.clone());
|
||||||
self.cached_selection = selected_colors;
|
self.cached_selection = selected_colors;
|
||||||
self.cached_cols = cols;
|
self.cached_cols = cols;
|
||||||
painter.rect_filled(rect, 0, color(colors.background));
|
let mut shapes = vec![egui::Shape::rect_filled(rect, 0, color(colors.background))];
|
||||||
|
let mut cursor_shapes = Vec::new();
|
||||||
let mut rows = self.rows.update(&snapshot)?;
|
let mut rows = self.rows.update(&snapshot)?;
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
@@ -343,18 +388,43 @@ impl Renderer {
|
|||||||
if selected {
|
if selected {
|
||||||
(bg, fg) = selected_colors;
|
(bg, fg) = selected_colors;
|
||||||
}
|
}
|
||||||
if explicit_bg.is_some() || style.inverse || selected {
|
|
||||||
backgrounds.push(egui::Shape::rect_filled(cell_rect, 0, bg));
|
|
||||||
}
|
|
||||||
if style.faint {
|
if style.faint {
|
||||||
fg = fg.gamma_multiply(0.6);
|
fg = fg.gamma_multiply(0.6);
|
||||||
}
|
}
|
||||||
let wide = c.raw_cell()?.wide()?;
|
let wide = c.raw_cell()?.wide()?;
|
||||||
|
text.clear();
|
||||||
|
c.graphemes_utf8(&mut text)?;
|
||||||
|
let appearance = CellAppearance {
|
||||||
|
text: text.clone(),
|
||||||
|
style,
|
||||||
|
fg,
|
||||||
|
bg,
|
||||||
|
background: explicit_bg.is_some() || style.inverse || selected,
|
||||||
|
wide,
|
||||||
|
};
|
||||||
|
// Dirty flags are row-granular. Compare cells so edits in one
|
||||||
|
// split don't rebuild its unchanged neighbor on the same row.
|
||||||
|
let background_start = backgrounds.len();
|
||||||
|
let foreground_start = foregrounds.len();
|
||||||
|
if let Some(old) = self.row_cache[y].cells.get(x)
|
||||||
|
&& old.appearance == appearance
|
||||||
|
{
|
||||||
|
backgrounds
|
||||||
|
.extend_from_slice(&self.row_cache[y].backgrounds[old.backgrounds.clone()]);
|
||||||
|
foregrounds
|
||||||
|
.extend_from_slice(&self.row_cache[y].foregrounds[old.foregrounds.clone()]);
|
||||||
|
} else {
|
||||||
|
#[cfg(test)]
|
||||||
|
{
|
||||||
|
self.rebuilt_cells += 1;
|
||||||
|
}
|
||||||
|
if appearance.background {
|
||||||
|
backgrounds.push(egui::Shape::rect_filled(cell_rect, 0, bg));
|
||||||
|
}
|
||||||
if !style.invisible
|
if !style.invisible
|
||||||
&& !matches!(wide, CellWide::SpacerHead | CellWide::SpacerTail)
|
&& !matches!(wide, CellWide::SpacerHead | CellWide::SpacerTail)
|
||||||
&& c.graphemes_len()? > 0
|
&& c.graphemes_len()? > 0
|
||||||
{
|
{
|
||||||
c.graphemes_utf8(&mut text)?;
|
|
||||||
// Placeholder cells are reserved by the Kitty protocol, not printable text.
|
// Placeholder cells are reserved by the Kitty protocol, not printable text.
|
||||||
if let Some(separator) = powerline_shape(&text, cell_rect, fg) {
|
if let Some(separator) = powerline_shape(&text, cell_rect, fg) {
|
||||||
foregrounds.push(separator);
|
foregrounds.push(separator);
|
||||||
@@ -409,6 +479,12 @@ impl Renderer {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
cached.cells.push(CachedCell {
|
||||||
|
appearance,
|
||||||
|
backgrounds: background_start..backgrounds.len(),
|
||||||
|
foregrounds: foreground_start..foregrounds.len(),
|
||||||
|
});
|
||||||
x += 1;
|
x += 1;
|
||||||
}
|
}
|
||||||
self.row_cache[y] = cached;
|
self.row_cache[y] = cached;
|
||||||
@@ -419,7 +495,7 @@ impl Renderer {
|
|||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
for layer in [Layer::BelowBg, Layer::BelowText, Layer::AboveText] {
|
for layer in [Layer::BelowBg, Layer::BelowText, Layer::AboveText] {
|
||||||
if layer == Layer::BelowText {
|
if layer == Layer::BelowText {
|
||||||
painter.extend(
|
shapes.extend(
|
||||||
self.row_cache
|
self.row_cache
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|row| row.backgrounds.iter())
|
.flat_map(|row| row.backgrounds.iter())
|
||||||
@@ -431,7 +507,7 @@ impl Renderer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if layer == Layer::AboveText {
|
if layer == Layer::AboveText {
|
||||||
painter.extend(
|
shapes.extend(
|
||||||
self.row_cache
|
self.row_cache
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|row| row.foregrounds.iter())
|
.flat_map(|row| row.foregrounds.iter())
|
||||||
@@ -451,6 +527,7 @@ impl Renderer {
|
|||||||
&mut self.placements,
|
&mut self.placements,
|
||||||
&mut self.textures,
|
&mut self.textures,
|
||||||
&mut seen,
|
&mut seen,
|
||||||
|
&mut shapes,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
self.textures.retain(|id, _| seen.contains(id));
|
self.textures.retain(|id, _| seen.contains(id));
|
||||||
@@ -465,20 +542,18 @@ impl Renderer {
|
|||||||
match snapshot.cursor_visual_style()? {
|
match snapshot.cursor_visual_style()? {
|
||||||
CursorVisualStyle::Bar if active => {
|
CursorVisualStyle::Bar if active => {
|
||||||
cursor_rect.max.x = cursor_rect.min.x + 2.0;
|
cursor_rect.max.x = cursor_rect.min.x + 2.0;
|
||||||
painter.rect_filled(cursor_rect, 0, c);
|
shapes.push(egui::Shape::rect_filled(cursor_rect, 0, c));
|
||||||
}
|
}
|
||||||
CursorVisualStyle::Underline if active => {
|
CursorVisualStyle::Underline if active => {
|
||||||
cursor_rect.min.y = cursor_rect.max.y - 2.0;
|
cursor_rect.min.y = cursor_rect.max.y - 2.0;
|
||||||
painter.rect_filled(cursor_rect, 0, c);
|
shapes.push(egui::Shape::rect_filled(cursor_rect, 0, c));
|
||||||
}
|
}
|
||||||
CursorVisualStyle::Block if active => {
|
CursorVisualStyle::Block if active => {
|
||||||
painter.rect_filled(cursor_rect, 0, c);
|
shapes.push(egui::Shape::rect_filled(cursor_rect, 0, c));
|
||||||
// Redraw the covered glyph in the background color so an
|
// Redraw the covered glyph in the background color so an
|
||||||
// application's solid block cursor leaves text readable.
|
// application's solid block cursor leaves text readable.
|
||||||
let ink = color(colors.background);
|
let ink = color(colors.background);
|
||||||
let cursor_painter =
|
cursor_shapes.extend(
|
||||||
painter.with_clip_rect(cursor_rect.intersect(painter.clip_rect()));
|
|
||||||
cursor_painter.extend(
|
|
||||||
self.row_cache[cursor.y as usize]
|
self.row_cache[cursor.y as usize]
|
||||||
.foregrounds
|
.foregrounds
|
||||||
.iter()
|
.iter()
|
||||||
@@ -493,23 +568,48 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
shape
|
egui::epaint::ClippedShape {
|
||||||
|
clip_rect: cursor_rect.intersect(rect),
|
||||||
|
shape,
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
painter.rect_stroke(
|
shapes.push(egui::Shape::rect_stroke(
|
||||||
cursor_rect,
|
cursor_rect,
|
||||||
theme::CURSOR_CORNER_RADIUS,
|
theme::CURSOR_CORNER_RADIUS,
|
||||||
Stroke::new(theme::BORDER_WIDTH, c),
|
Stroke::new(theme::BORDER_WIDTH, c),
|
||||||
egui::StrokeKind::Inside,
|
egui::StrokeKind::Inside,
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snapshot.set_dirty(Dirty::Clean)?;
|
snapshot.set_dirty(Dirty::Clean)?;
|
||||||
|
self.frame = shapes
|
||||||
|
.into_iter()
|
||||||
|
.map(|shape| egui::epaint::ClippedShape {
|
||||||
|
clip_rect: rect,
|
||||||
|
shape,
|
||||||
|
})
|
||||||
|
.chain(cursor_shapes)
|
||||||
|
.collect();
|
||||||
|
self.present(&painter, screen_rect);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn present(&self, painter: &Painter, rect: Rect) {
|
||||||
|
for cached in &self.frame {
|
||||||
|
let mut shape = cached.shape.clone();
|
||||||
|
shape.translate(rect.min.to_vec2());
|
||||||
|
let clip = cached
|
||||||
|
.clip_rect
|
||||||
|
.translate(rect.min.to_vec2())
|
||||||
|
.intersect(rect)
|
||||||
|
.intersect(painter.clip_rect());
|
||||||
|
painter.with_clip_rect(clip).add(shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -522,6 +622,7 @@ fn paint_images(
|
|||||||
iterator: &mut PlacementIterator<'_>,
|
iterator: &mut PlacementIterator<'_>,
|
||||||
textures: &mut HashMap<u32, (u64, TextureHandle)>,
|
textures: &mut HashMap<u32, (u64, TextureHandle)>,
|
||||||
seen: &mut HashSet<u32>,
|
seen: &mut HashSet<u32>,
|
||||||
|
shapes: &mut Vec<egui::Shape>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let graphics = term.kitty_graphics()?;
|
let graphics = term.kitty_graphics()?;
|
||||||
let mut placements = iterator.update(&graphics)?;
|
let mut placements = iterator.update(&graphics)?;
|
||||||
@@ -596,7 +697,12 @@ fn paint_images(
|
|||||||
}
|
}
|
||||||
images.sort_by_key(|(z, id, pid, _, _)| (*z, *id, *pid));
|
images.sort_by_key(|(z, id, pid, _, _)| (*z, *id, *pid));
|
||||||
for (_, id, _, dest, uv) in images {
|
for (_, id, _, dest, uv) in images {
|
||||||
painter.image(textures[&id].1.id(), dest, uv, Color32::WHITE);
|
shapes.push(egui::Shape::image(
|
||||||
|
textures[&id].1.id(),
|
||||||
|
dest,
|
||||||
|
uv,
|
||||||
|
Color32::WHITE,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -707,6 +813,69 @@ mod tests {
|
|||||||
.shapes
|
.shapes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrolling_one_editor_split_reuses_unchanged_cells() {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let mut term =
|
||||||
|
crate::pane::terminal(&crate::config::Config::default(), &theme::DEFAULT).unwrap();
|
||||||
|
term.resize(20, 5, 8, 16).unwrap();
|
||||||
|
term.vt_write(b"\x1b[?25l");
|
||||||
|
for row in 1..=5 {
|
||||||
|
term.vt_write(format!("\x1b[{row};1Hleft {row} right {row}").as_bytes());
|
||||||
|
}
|
||||||
|
let rect = Rect::from_min_size(pos2(12.0, 24.0), vec2(200.0, 120.0));
|
||||||
|
let font = FontId::monospace(16.0);
|
||||||
|
let mut renderer = Renderer::new().unwrap();
|
||||||
|
frame(&ctx, &mut renderer, &term, rect, &font);
|
||||||
|
assert_eq!(renderer.rebuilt_cells, 100);
|
||||||
|
|
||||||
|
// Simulate an editor repainting entire rows while only its left split changes.
|
||||||
|
for row in 1..=5 {
|
||||||
|
term.vt_write(format!("\x1b[{row};1Hleft {} right {row}", row + 1).as_bytes());
|
||||||
|
}
|
||||||
|
let actual = frame(&ctx, &mut renderer, &term, rect, &font);
|
||||||
|
assert_eq!(renderer.rebuilt_cells, 105, "only five digits changed");
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
frame(&ctx, &mut Renderer::new().unwrap(), &term, rect, &font)
|
||||||
|
);
|
||||||
|
// Rewriting identical text should reuse every cell, even on dirty rows.
|
||||||
|
term.vt_write(b"\x1b[1;1Hleft 2 right 1");
|
||||||
|
frame(&ctx, &mut renderer, &term, rect, &font);
|
||||||
|
assert_eq!(renderer.rebuilt_cells, 105);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synchronized_updates_keep_the_last_complete_frame() {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let mut term =
|
||||||
|
crate::pane::terminal(&crate::config::Config::default(), &theme::DEFAULT).unwrap();
|
||||||
|
term.resize(20, 5, 8, 16).unwrap();
|
||||||
|
term.vt_write(b"\x1b[?25lleft buffer\r\nright buffer");
|
||||||
|
let rect = Rect::from_min_size(pos2(12.0, 24.0), vec2(200.0, 120.0));
|
||||||
|
let font = FontId::monospace(16.0);
|
||||||
|
let mut renderer = Renderer::new().unwrap();
|
||||||
|
let initial = frame(&ctx, &mut renderer, &term, rect, &font);
|
||||||
|
let cells = renderer.rebuilt_cells;
|
||||||
|
term.vt_write(b"\x1b[?2026h\x1b[2J\x1b[Hpartial");
|
||||||
|
assert!(term.mode(Mode::SYNC_OUTPUT).unwrap());
|
||||||
|
assert_eq!(initial, frame(&ctx, &mut renderer, &term, rect, &font));
|
||||||
|
assert_eq!(renderer.rebuilt_cells, cells);
|
||||||
|
term.vt_write(b"\rcomplete\r\nright buffer\x1b[?2026l");
|
||||||
|
let completed = frame(&ctx, &mut renderer, &term, rect, &font);
|
||||||
|
assert_ne!(initial, completed);
|
||||||
|
assert_eq!(
|
||||||
|
completed,
|
||||||
|
frame(&ctx, &mut Renderer::new().unwrap(), &term, rect, &font)
|
||||||
|
);
|
||||||
|
|
||||||
|
// A missing end marker must not freeze the terminal indefinitely.
|
||||||
|
term.vt_write(b"\x1b[?2026h\x1b[Htimeout");
|
||||||
|
assert_eq!(completed, frame(&ctx, &mut renderer, &term, rect, &font));
|
||||||
|
renderer.sync_started = Some(Instant::now() - Duration::from_secs(2));
|
||||||
|
assert_ne!(completed, frame(&ctx, &mut renderer, &term, rect, &font));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cached_rows_match_fresh_rendering_after_terminal_changes() {
|
fn cached_rows_match_fresh_rendering_after_terminal_changes() {
|
||||||
use libghostty_vt::{
|
use libghostty_vt::{
|
||||||
|
|||||||
Reference in New Issue
Block a user