diff options
Diffstat (limited to 'src/client/shell')
| -rw-r--r-- | src/client/shell/composition.rs | 12 | ||||
| -rw-r--r-- | src/client/shell/graphics.rs | 34 | ||||
| -rw-r--r-- | src/client/shell/state.rs | 2 | ||||
| -rw-r--r-- | src/client/shell/tests/copy.rs | 2 | ||||
| -rw-r--r-- | src/client/shell/tests/graphics.rs | 148 | ||||
| -rw-r--r-- | src/client/shell/tests/input.rs | 14 | ||||
| -rw-r--r-- | src/client/shell/tests/text_editing.rs | 2 |
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}"); } } |
