aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/shell')
-rw-r--r--src/client/shell/composition.rs12
-rw-r--r--src/client/shell/graphics.rs34
-rw-r--r--src/client/shell/state.rs2
-rw-r--r--src/client/shell/tests/copy.rs2
-rw-r--r--src/client/shell/tests/graphics.rs148
-rw-r--r--src/client/shell/tests/input.rs14
-rw-r--r--src/client/shell/tests/text_editing.rs2
7 files changed, 174 insertions, 40 deletions
diff --git a/src/client/shell/composition.rs b/src/client/shell/composition.rs
index 9b710dd5..b7ab4a96 100644
--- a/src/client/shell/composition.rs
+++ b/src/client/shell/composition.rs
@@ -147,7 +147,11 @@ impl ClientShellState {
FrameData::from_ratatui_buffer_with_hyperlinks(&buffer, None, &[])
}
- pub(crate) fn compose(&mut self, cols: u16, rows: u16) -> Option<FrameData> {
+ pub(crate) fn compose(
+ &mut self,
+ cols: u16,
+ rows: u16,
+ ) -> Option<crate::client::frame_output::ComposedFrame> {
self.last_composed_at = Some(std::time::Instant::now());
self.selection_repaint_deadline = None;
if self.last_composed_size != Some((cols, rows)) && self.mode == ClientShellMode::Navigate {
@@ -167,7 +171,7 @@ impl ClientShellState {
&& self.navigation_target_valid(&pending.target)
});
if self.snapshot.is_none() || self.pane_surface.is_none() {
- return Some(self.compose_unavailable(cols, rows));
+ return Some(self.compose_unavailable(cols, rows).into());
}
let snapshot = self.snapshot.as_deref()?;
// Do not compose a retained surface while waiting for its matching snapshot or
@@ -716,8 +720,8 @@ impl ClientShellState {
self.hits.pane_splits.clear();
self.hits.popup = None;
}
- self.compose_graphics(&mut frame, layout, &occlusion);
- Some(frame)
+ let graphics = self.compose_graphics(layout, &occlusion);
+ Some(crate::client::frame_output::ComposedFrame { frame, graphics })
}
}
diff --git a/src/client/shell/graphics.rs b/src/client/shell/graphics.rs
index a7ae81e3..35e61dbb 100644
--- a/src/client/shell/graphics.rs
+++ b/src/client/shell/graphics.rs
@@ -1,12 +1,37 @@
use super::*;
+#[cfg(unix)]
+pub(crate) struct ClientGraphicsCheckpoint(crate::kitty_graphics::surface::ClientState);
+
impl ClientShellState {
- #[cfg(unix)]
+ #[cfg(all(test, unix))]
pub(crate) fn graphics_scope(&self) -> &str {
self.graphics.scope()
}
#[cfg(unix)]
+ pub(crate) fn accepts_direct_graphics_asset(
+ &self,
+ key: &crate::protocol::SurfaceGraphicsAssetKey,
+ image_id: u32,
+ ) -> bool {
+ self.graphics.accepts_direct_asset(key, image_id)
+ }
+
+ #[cfg(unix)]
+ pub(crate) fn direct_graphics_checkpoint(&self) -> ClientGraphicsCheckpoint {
+ ClientGraphicsCheckpoint(self.graphics.clone())
+ }
+
+ #[cfg(unix)]
+ pub(crate) fn restore_direct_graphics_checkpoint(
+ &mut self,
+ checkpoint: ClientGraphicsCheckpoint,
+ ) {
+ self.graphics = checkpoint.0;
+ }
+
+ #[cfg(unix)]
pub(crate) fn trust_direct_graphics_asset(
&mut self,
key: &crate::protocol::SurfaceGraphicsAssetKey,
@@ -33,10 +58,9 @@ impl ClientShellState {
pub(super) fn compose_graphics(
&mut self,
- frame: &mut FrameData,
layout: ClientShellLayout,
occlusion: &crate::kitty_graphics::surface::Occlusion,
- ) {
+ ) -> crate::kitty_graphics::GraphicsOutput {
let visibility = if self.endpoint_error.is_some() {
crate::kitty_graphics::surface::Visibility::Hidden
} else if self.hits.popup.is_some() {
@@ -49,12 +73,12 @@ impl ClientShellState {
.popup
.as_ref()
.map(|popup| (popup.inner_rect.x, popup.inner_rect.y));
- frame.graphics = self.graphics.encode(
+ self.graphics.encode_output(
visibility,
(layout.pane_surface.x, layout.pane_surface.y),
popup_origin,
self.graphics_cell_size,
occlusion,
- );
+ )
}
}
diff --git a/src/client/shell/state.rs b/src/client/shell/state.rs
index 0d6937e0..c170c9b0 100644
--- a/src/client/shell/state.rs
+++ b/src/client/shell/state.rs
@@ -1015,7 +1015,7 @@ impl ClientShellState {
pane_surface_generation: None,
pane_surface: None,
pending_pane_surface: None,
- graphics: crate::kitty_graphics::surface::ClientState::default(),
+ graphics: crate::kitty_graphics::surface::ClientState::new(),
graphics_cell_size: crate::kitty_graphics::HostCellSize {
width_px: 1,
height_px: 1,
diff --git a/src/client/shell/tests/copy.rs b/src/client/shell/tests/copy.rs
index 112417fc..67e36f23 100644
--- a/src/client/shell/tests/copy.rs
+++ b/src/client/shell/tests/copy.rs
@@ -1563,7 +1563,7 @@ fn navigator_narrow_layout_and_long_search_stay_inside_the_popup() {
navigator.query = "界".repeat(100).as_str().into();
let frame = state.compose(width, height).expect("navigator frame");
let popup = state.hits.navigator_popup;
- let cursor = frame.cursor.expect("search cursor");
+ let cursor = frame.cursor.as_ref().expect("search cursor");
assert!(super::super::contains(popup, (cursor.x, cursor.y)));
assert!(state.hits.navigator_rows.is_empty());
assert!(popup.right() <= width && popup.bottom() <= height);
diff --git a/src/client/shell/tests/graphics.rs b/src/client/shell/tests/graphics.rs
index 240e031c..e54301a4 100644
--- a/src/client/shell/tests/graphics.rs
+++ b/src/client/shell/tests/graphics.rs
@@ -63,6 +63,48 @@ fn add_main_image(
surface.graphics.placements.push(placement);
}
+#[cfg(unix)]
+#[test]
+fn failed_direct_ack_composition_restores_graphics_for_inline_retry() {
+ use crate::kitty_graphics::surface::host_image_id;
+
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ let mut pane = surface();
+ let (mut asset, mut placement) = image(
+ SurfaceGraphicsTarget::Pane {
+ pane_id: "pane_1".into(),
+ },
+ 0,
+ 0,
+ 71,
+ );
+ asset.key.source = SurfaceGraphicsSource::PaneLayer {
+ pane_id: "pane_1".into(),
+ layer_id: "direct-rollback".into(),
+ };
+ placement.asset = asset.key.clone();
+ let key = asset.key.clone();
+ pane.graphics.assets.push(asset);
+ pane.graphics.placements.push(placement);
+ state.set_pane_surface(pane);
+
+ // Model an ACK arriving while projection pairing makes composition unavailable.
+ state.pending_pane_surface = Some(surface());
+ let image_id = host_image_id(state.graphics_scope(), &key);
+ let graphics_before = format!("{:?}", state.graphics);
+ let checkpoint = state.direct_graphics_checkpoint();
+ assert!(state.trust_direct_graphics_asset(&key, image_id));
+ assert!(state.compose(106, 20).is_none());
+ state.restore_direct_graphics_checkpoint(checkpoint);
+ assert_eq!(format!("{:?}", state.graphics), graphics_before);
+
+ // Once pairing is available, fallback still owns the asset and uploads it normally.
+ state.pending_pane_surface = None;
+ let retry = state.compose(106, 20).expect("fallback frame");
+ assert!(String::from_utf8_lossy(&retry.graphics.into_inline_bytes()).contains("a=t"));
+}
+
fn is_placed(bytes: &[u8], point: (u16, u16)) -> bool {
String::from_utf8_lossy(bytes).contains(&format!("\x1b[{};{}H", point.1 + 1, point.0 + 1))
}
@@ -85,11 +127,11 @@ fn assert_graphics_cover(state: &mut ClientShellState, covered: Rect, cols: u16,
state.set_pane_surface(surface);
let frame = state.compose(cols, rows).unwrap();
assert!(
- is_placed(&frame.graphics, outside),
+ is_placed(&frame.graphics.clone().into_inline_bytes(), outside),
"outside={outside:?} cover={covered:?}"
);
assert!(
- !is_placed(&frame.graphics, inside),
+ !is_placed(&frame.graphics.clone().into_inline_bytes(), inside),
"inside={inside:?} cover={covered:?}"
);
}
@@ -327,18 +369,38 @@ fn every_dialog_and_menu_occludes_its_panel_not_the_whole_screen() {
state.compose(106, 40).unwrap();
state.overlay = Some(overlay);
let frame = state.compose(106, 40).unwrap();
- assert!(is_placed(&frame.graphics, outside), "{:?}", state.overlay);
+ assert!(
+ is_placed(&frame.graphics.clone().into_inline_bytes(), outside),
+ "{:?}",
+ state.overlay
+ );
if let Some(border) = border {
- assert!(!is_placed(&frame.graphics, border), "{:?}", state.overlay);
- assert!(String::from_utf8_lossy(&frame.graphics).contains("a=d,d=i"));
+ assert!(
+ !is_placed(&frame.graphics.clone().into_inline_bytes(), border),
+ "{:?}",
+ state.overlay
+ );
+ assert!(
+ String::from_utf8_lossy(&frame.graphics.clone().into_inline_bytes())
+ .contains("a=d,d=i")
+ );
}
state.overlay = None;
let restored = state.compose(106, 40).unwrap();
- assert!(is_placed(&restored.graphics, outside));
+ assert!(is_placed(
+ &restored.graphics.clone().into_inline_bytes(),
+ outside
+ ));
if let Some(border) = border {
- assert!(is_placed(&restored.graphics, border));
+ assert!(is_placed(
+ &restored.graphics.clone().into_inline_bytes(),
+ border
+ ));
}
- assert!(!String::from_utf8_lossy(&restored.graphics).contains("a=t"));
+ assert!(
+ !String::from_utf8_lossy(&restored.graphics.clone().into_inline_bytes())
+ .contains("a=t")
+ );
}
}
@@ -377,17 +439,26 @@ fn selection_copy_cursor_and_search_hide_only_the_highlighted_images() {
});
let frame = state.compose(106, 20).unwrap();
for point in &points[..3] {
- assert!(!is_placed(&frame.graphics, *point), "{point:?}");
+ assert!(
+ !is_placed(&frame.graphics.clone().into_inline_bytes(), *point),
+ "{point:?}"
+ );
}
- assert!(is_placed(&frame.graphics, points[3]));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ points[3]
+ ));
state.mode = ClientShellMode::Terminal;
state.copy_mode = None;
state.selection = None;
let frame = state.compose(106, 20).unwrap();
for point in points {
- assert!(is_placed(&frame.graphics, point));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ point
+ ));
}
- assert!(!String::from_utf8_lossy(&frame.graphics).contains("a=t"));
+ assert!(!String::from_utf8_lossy(&frame.graphics.clone().into_inline_bytes()).contains("a=t"));
}
#[test]
@@ -400,15 +471,26 @@ fn mobile_switcher_still_hides_the_entire_underlying_surface() {
add_main_image(&mut surface, layout, point, 1);
state.set_pane_surface(surface);
let frame = state.compose(40, 24).unwrap();
- assert!(is_placed(&frame.graphics, point));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ point
+ ));
state.mode = ClientShellMode::Navigate;
let frame = state.compose(40, 24).unwrap();
- assert!(!is_placed(&frame.graphics, point));
- assert!(String::from_utf8_lossy(&frame.graphics).contains("a=d,d=i"));
+ assert!(!is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ point
+ ));
+ assert!(
+ String::from_utf8_lossy(&frame.graphics.clone().into_inline_bytes()).contains("a=d,d=i")
+ );
state.mode = ClientShellMode::Terminal;
let frame = state.compose(40, 24).unwrap();
- assert!(is_placed(&frame.graphics, point));
- assert!(!String::from_utf8_lossy(&frame.graphics).contains("a=t"));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ point
+ ));
+ assert!(!String::from_utf8_lossy(&frame.graphics.clone().into_inline_bytes()).contains("a=t"));
}
#[test]
@@ -437,15 +519,33 @@ fn popup_terminal_keeps_own_graphics_and_hides_only_background_overlap() {
surface.graphics.placements.push(placement);
state.set_pane_surface(surface);
let frame = state.compose(106, 20).unwrap();
- assert!(is_placed(&frame.graphics, outside));
- assert!(!is_placed(&frame.graphics, border));
- assert!(is_placed(&frame.graphics, inside));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ outside
+ ));
+ assert!(!is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ border
+ ));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ inside
+ ));
state.overlay = Some(ClientShellOverlay::Onboarding);
let frame = state.compose(106, 20).unwrap();
- assert!(is_placed(&frame.graphics, outside));
- assert!(!is_placed(&frame.graphics, inside));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ outside
+ ));
+ assert!(!is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ inside
+ ));
state.overlay = None;
let frame = state.compose(106, 20).unwrap();
- assert!(is_placed(&frame.graphics, inside));
- assert!(!String::from_utf8_lossy(&frame.graphics).contains("a=t"));
+ assert!(is_placed(
+ &frame.graphics.clone().into_inline_bytes(),
+ inside
+ ));
+ assert!(!String::from_utf8_lossy(&frame.graphics.clone().into_inline_bytes()).contains("a=t"));
}
diff --git a/src/client/shell/tests/input.rs b/src/client/shell/tests/input.rs
index b55cf9a2..b54548f2 100644
--- a/src/client/shell/tests/input.rs
+++ b/src/client/shell/tests/input.rs
@@ -216,18 +216,24 @@ fn client_shell_graphics_follow_final_shell_origin_and_local_overlay_visibility(
state.set_pane_surface(pane_surface);
let visible = state.compose(106, 20).expect("visible graphics frame");
- let visible = String::from_utf8_lossy(&visible.graphics);
+ let visible = visible.graphics.clone().into_inline_bytes();
+ let visible = String::from_utf8_lossy(&visible);
assert!(visible.contains("a=t,t=d"));
assert!(visible.contains("\u{1b}[2;27H"));
state.overlay = Some(ClientShellOverlay::Onboarding);
let uncovered = state.compose(106, 20).expect("overlay frame");
- assert!(!String::from_utf8_lossy(&uncovered.graphics).contains("a=d"));
- assert!(String::from_utf8_lossy(&uncovered.graphics).contains("a=p"));
+ assert!(
+ !String::from_utf8_lossy(&uncovered.graphics.clone().into_inline_bytes()).contains("a=d")
+ );
+ assert!(
+ String::from_utf8_lossy(&uncovered.graphics.clone().into_inline_bytes()).contains("a=p")
+ );
state.overlay = None;
let restored = state.compose(106, 20).expect("restored graphics frame");
- let restored = String::from_utf8_lossy(&restored.graphics);
+ let restored = restored.graphics.clone().into_inline_bytes();
+ let restored = String::from_utf8_lossy(&restored);
assert!(restored.contains("a=p"));
assert!(!restored.contains("a=t,t=d"));
}
diff --git a/src/client/shell/tests/text_editing.rs b/src/client/shell/tests/text_editing.rs
index b63ce9f6..118a606e 100644
--- a/src/client/shell/tests/text_editing.rs
+++ b/src/client/shell/tests/text_editing.rs
@@ -384,7 +384,7 @@ fn every_field_renders_long_unicode_across_resize_without_mutation() {
for (width, height) in [(120, 40), (60, 20), (12, 6), (1, 1), (120, 40)] {
let before = editor(&mut state).clone();
if let Some(frame) = state.compose(width, height) {
- if let Some(cursor) = frame.cursor.filter(|cursor| cursor.visible) {
+ if let Some(cursor) = frame.cursor.as_ref().filter(|cursor| cursor.visible) {
assert!(cursor.x < width && cursor.y < height, "field {field}");
}
}