aboutsummaryrefslogtreecommitdiff
path: root/src/kitty_graphics
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-14 03:02:14 +0300
committerGitHub <noreply@github.com>2026-09-14 03:02:14 +0300
commit21c1f8cf856affb875b5ab4302225a63f4563bdd (patch)
treefc3fa607c4af3d9233f3ba7d905a4744ad25c215 /src/kitty_graphics
parentadd3dd28f91db1a2b33e9582c60ac9c1809c1bf5 (diff)
fix: hide only kitty image placements overlapping overlays (#4081)
Diffstat (limited to 'src/kitty_graphics')
-rw-r--r--src/kitty_graphics/surface.rs263
1 files changed, 260 insertions, 3 deletions
diff --git a/src/kitty_graphics/surface.rs b/src/kitty_graphics/surface.rs
index e5f00ac2..4df05ffb 100644
--- a/src/kitty_graphics/surface.rs
+++ b/src/kitty_graphics/surface.rs
@@ -38,6 +38,61 @@ pub(crate) enum Visibility {
}
#[derive(Debug, Default)]
+pub(crate) struct Occlusion {
+ regions: Vec<Rect>,
+ popup_start: usize,
+}
+
+impl Occlusion {
+ pub(crate) fn cover(&mut self, rect: Rect) {
+ if !rect.is_empty() {
+ self.regions.push(rect);
+ }
+ }
+
+ pub(crate) fn start_popup(&mut self, rect: Rect) {
+ self.cover(rect);
+ self.popup_start = self.regions.len();
+ }
+
+ fn covers(
+ &self,
+ placement: &SurfaceGraphicsPlacement,
+ origin: (u16, u16),
+ cell: HostCellSize,
+ ) -> bool {
+ let regions = if matches!(
+ placement.asset.source,
+ SurfaceGraphicsSource::Terminal {
+ target: SurfaceGraphicsTarget::Popup { .. },
+ ..
+ }
+ ) {
+ &self.regions[self.popup_start..]
+ } else {
+ &self.regions
+ };
+ if regions.is_empty() {
+ return false;
+ }
+ let width = u64::from(cell.width_px);
+ let height = u64::from(cell.height_px);
+ let x =
+ u64::from(origin.0.saturating_add(placement.x)) * width + u64::from(placement.x_offset);
+ let y = u64::from(origin.1.saturating_add(placement.y)) * height
+ + u64::from(placement.y_offset);
+ let right = x + u64::from(placement.cols) * width;
+ let bottom = y + u64::from(placement.rows) * height;
+ regions.iter().any(|rect| {
+ x < u64::from(rect.right()) * width
+ && right > u64::from(rect.x) * width
+ && y < u64::from(rect.bottom()) * height
+ && bottom > u64::from(rect.y) * height
+ })
+ }
+}
+
+#[derive(Debug, Default)]
pub(crate) struct ClientState {
scope: String,
scene: SurfaceGraphicsScene,
@@ -166,6 +221,7 @@ impl ClientState {
main_origin: (u16, u16),
popup_origin: Option<(u16, u16)>,
cell_size: HostCellSize,
+ occlusion: &Occlusion,
) -> Vec<u8> {
let mut bytes = self.take_pending_cleanup();
self.stale_images.sort_unstable();
@@ -198,6 +254,7 @@ impl ClientState {
main_origin,
popup_origin,
cell_size,
+ occlusion,
)
})
.collect::<Vec<_>>();
@@ -550,6 +607,7 @@ fn client_host_placement(
main_origin: (u16, u16),
popup_origin: Option<(u16, u16)>,
cell_size: HostCellSize,
+ occlusion: &Occlusion,
) -> Option<HostPlacement> {
let origin = match (&placement.asset.source, visibility) {
(
@@ -571,6 +629,9 @@ fn client_host_placement(
}
_ => return None,
};
+ if occlusion.covers(placement, origin, cell_size) {
+ return None;
+ }
let source_key = HostSourceKey::ClientSurface {
scope: scope.to_owned(),
source: placement.asset.source.clone(),
@@ -692,6 +753,182 @@ mod tests {
}
#[test]
+ fn occlusion_uses_final_pixel_bounds_and_strict_overlap() {
+ let cell = HostCellSize {
+ width_px: 8,
+ height_px: 16,
+ };
+ let image = asset(
+ SurfaceGraphicsTarget::Pane {
+ pane_id: "pane".into(),
+ },
+ 1,
+ vec![1, 2, 3, 4],
+ );
+ let mut placement = scene(image, 1, 2).placements.remove(0);
+ let mut cover = Occlusion::default();
+ cover.cover(Rect::new(12, 8, 1, 1));
+ // The image at (11, 7) just touches the cover's top-left corner.
+ assert!(!cover.covers(&placement, (10, 5), cell));
+ placement.x_offset = 1;
+ assert!(!cover.covers(&placement, (10, 5), cell));
+ placement.y_offset = 1;
+ assert!(cover.covers(&placement, (10, 5), cell));
+ placement.x_offset = 0;
+ assert!(!cover.covers(&placement, (10, 5), cell));
+ placement.cols = 2;
+ assert!(cover.covers(&placement, (10, 5), cell));
+ assert!(!cover.covers(&placement, (0, 0), cell));
+ cover = Occlusion::default();
+ cover.cover(Rect::new(11, 7, 0, 1));
+ assert!(!cover.covers(&placement, (10, 5), cell));
+ }
+
+ #[test]
+ fn occlusion_keeps_shared_asset_placements_and_restores_without_upload() {
+ for direct in [false, true] {
+ let mut state = ClientState::default();
+ state.set_scope("occlusion");
+ let _ = state.take_pending_cleanup();
+ let image = asset(
+ SurfaceGraphicsTarget::Pane {
+ pane_id: "pane".into(),
+ },
+ 1,
+ vec![1, 2, 3, 4],
+ );
+ let id = host_image_id("occlusion", &image.key);
+ let mut graphics = scene(image.clone(), 0, 0);
+ let mut second = graphics.placements[0].clone();
+ second.logical_placement_id = 4;
+ second.x = 5;
+ graphics.placements.push(second);
+ if direct {
+ // Model an already uploaded asset, including on hosts without direct transport.
+ graphics.assets.clear();
+ state.host.images.insert(
+ id,
+ ImageSignature {
+ image_width: 1,
+ image_height: 1,
+ format_code: 32,
+ data_len: 4,
+ data_fingerprint: 1,
+ },
+ );
+ }
+ state.set_scene(graphics);
+ let cell = HostCellSize {
+ width_px: 8,
+ height_px: 16,
+ };
+ let _ = state.encode(Visibility::Main, (10, 5), None, cell, &Occlusion::default());
+ assert_eq!(state.host.placements.len(), 2);
+ let mut cover = Occlusion::default();
+ cover.cover(Rect::new(10, 5, 1, 1));
+ let hidden = state.encode(Visibility::Main, (10, 5), None, cell, &cover);
+ let hidden = String::from_utf8_lossy(&hidden);
+ assert!(hidden.contains("a=d,d=i"), "{hidden}");
+ assert!(!hidden.contains("a=d,d=I"), "{hidden}");
+ assert!(hidden.contains("\u{1b}[6;16H"), "{hidden}");
+ assert_eq!(state.host.placements.len(), 1);
+ cover.cover(Rect::new(15, 5, 1, 1));
+ let _ = state.encode(Visibility::Main, (10, 5), None, cell, &cover);
+ assert!(state.host.placements.is_empty());
+ assert!(state.host.images.contains_key(&id));
+ let restored =
+ state.encode(Visibility::Main, (10, 5), None, cell, &Occlusion::default());
+ let restored = String::from_utf8_lossy(&restored);
+ assert!(restored.contains("a=p"), "{restored}");
+ assert!(!restored.contains("a=t"), "{restored}");
+ assert_eq!(state.host.placements.len(), 2);
+ }
+ }
+
+ #[test]
+ fn popup_occlusion_respects_draw_order_and_its_own_images() {
+ let cell = HostCellSize {
+ width_px: 8,
+ height_px: 16,
+ };
+ let image = asset(
+ SurfaceGraphicsTarget::Pane {
+ pane_id: "pane".into(),
+ },
+ 1,
+ vec![1, 2, 3, 4],
+ );
+ let main = scene(image, 0, 0).placements.remove(0);
+ let image = asset(
+ SurfaceGraphicsTarget::Popup {
+ terminal_id: "popup".into(),
+ },
+ 2,
+ vec![4, 3, 2, 1],
+ );
+ let popup = scene(image, 0, 0).placements.remove(0);
+ let mut cover = Occlusion::default();
+ cover.cover(Rect::new(10, 5, 3, 3));
+ cover.start_popup(Rect::new(9, 4, 5, 5));
+ assert!(cover.covers(&main, (10, 5), cell));
+ assert!(!cover.covers(&main, (0, 0), cell));
+ assert!(!cover.covers(&popup, (10, 5), cell));
+ cover.cover(Rect::new(10, 5, 1, 1));
+ assert!(cover.covers(&popup, (10, 5), cell));
+ }
+
+ #[test]
+ #[ignore = "manual image placement overlap scaling profile"]
+ fn graphics_occlusion_render_scale_profile() {
+ let cell = HostCellSize {
+ width_px: 8,
+ height_px: 16,
+ };
+ for count in [1, 15] {
+ let mut graphics = SurfaceGraphicsScene::default();
+ for index in 0..count {
+ let image = asset(
+ SurfaceGraphicsTarget::Pane {
+ pane_id: format!("pane-{index}"),
+ },
+ index as u64,
+ vec![1, 2, 3, 4],
+ );
+ let pane = scene(image, index * 4, 0);
+ graphics.assets.extend(pane.assets);
+ graphics.placements.extend(pane.placements);
+ }
+ for covered in [false, true] {
+ let mut state = ClientState::default();
+ state.set_scope("profile");
+ state.set_scene(graphics.clone());
+ let mut cover = Occlusion::default();
+ if covered {
+ cover.cover(Rect::new(0, 10, 80, 3));
+ cover.cover(Rect::new(60, 0, 20, 5));
+ }
+ let _ = state.encode(Visibility::Main, (0, 0), None, cell, &cover);
+ let mut samples = Vec::new();
+ for _ in 0..101 {
+ let start = std::time::Instant::now();
+ for _ in 0..20 {
+ std::hint::black_box(state.encode(
+ Visibility::Main,
+ (0, 0),
+ None,
+ cell,
+ &cover,
+ ));
+ }
+ samples.push(start.elapsed().as_nanos() / 20);
+ }
+ samples.sort_unstable();
+ eprintln!("graphics occlusion panes={count} disjoint_overlays={covered} median_ns={} p95_ns={}", samples[50], samples[95]);
+ }
+ }
+ }
+
+ #[test]
fn client_encodes_final_main_origin_upload_once_and_replays_placement() {
let mut state = ClientState::default();
state.set_scope("endpoint-a:boot-1");
@@ -712,6 +949,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
assert!(String::from_utf8_lossy(&first).contains("a=t,t=d"));
assert!(String::from_utf8_lossy(&first).contains("\u{1b}[8;12H"));
@@ -724,6 +962,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
let second = String::from_utf8_lossy(&second);
assert!(!second.contains("a=t,t=d"));
@@ -747,12 +986,18 @@ mod tests {
width_px: 8,
height_px: 16,
};
- let _ = state.encode(Visibility::Main, (4, 2), None, cell);
+ let _ = state.encode(Visibility::Main, (4, 2), None, cell, &Occlusion::default());
- let hidden = state.encode(Visibility::Hidden, (4, 2), None, cell);
+ let hidden = state.encode(
+ Visibility::Hidden,
+ (4, 2),
+ None,
+ cell,
+ &Occlusion::default(),
+ );
assert!(String::from_utf8_lossy(&hidden).contains("a=d,d=i"));
- let restored = state.encode(Visibility::Main, (4, 2), None, cell);
+ let restored = state.encode(Visibility::Main, (4, 2), None, cell, &Occlusion::default());
let restored = String::from_utf8_lossy(&restored);
assert!(restored.contains("a=p"));
assert!(!restored.contains("a=t,t=d"));
@@ -779,6 +1024,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
assert!(String::from_utf8_lossy(&bytes).contains("\u{1b}[12;33H"));
}
@@ -796,6 +1042,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
let image = asset(
SurfaceGraphicsTarget::Pane {
@@ -818,6 +1065,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
let bytes = String::from_utf8_lossy(&bytes);
assert!(bytes.contains("a=p"));
@@ -851,6 +1099,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
let bytes = String::from_utf8_lossy(&bytes);
assert!(bytes.contains("a=p"), "{bytes}");
@@ -883,6 +1132,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
state.set_scene(SurfaceGraphicsScene {
@@ -897,6 +1147,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
))
.unwrap();
assert!(!hidden.contains(&format!("a=d,d=I,i={image_id}")));
@@ -911,6 +1162,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
))
.unwrap();
assert!(restored.contains("a=p"), "{restored}");
@@ -925,6 +1177,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
))
.unwrap();
assert!(removed.contains(&format!("a=d,d=I,i={image_id}")));
@@ -962,6 +1215,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
))
.unwrap();
assert!(bytes.contains("\u{1b}[2;3H"), "{bytes}");
@@ -1004,6 +1258,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
let bytes = String::from_utf8_lossy(&bytes);
assert!(bytes.contains(&format!("a=d,d=I,i={image_id}")), "{bytes}");
@@ -1029,6 +1284,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
state.set_scope("endpoint-a:boot-2");
@@ -1062,6 +1318,7 @@ mod tests {
width_px: 8,
height_px: 16,
},
+ &Occlusion::default(),
);
assert!(String::from_utf8_lossy(&bytes).contains("a=t,t=d"));
}