From 207be3c771d281baae6e5fa0fb74be9a056e97a2 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Tue, 1 Sep 2026 15:48:22 +0300 Subject: refactor: render the shell in the client (#3487) * feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations --- src/input/mouse.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 15 deletions(-) (limited to 'src/input/mouse.rs') diff --git a/src/input/mouse.rs b/src/input/mouse.rs index 62b5f6c2..984537d1 100644 --- a/src/input/mouse.rs +++ b/src/input/mouse.rs @@ -47,10 +47,12 @@ impl HostGeometry { )) } + #[cfg(test)] fn column_boundary(self, column: u16) -> Option { boundary(column, self.cols, self.width_px) } + #[cfg(test)] fn row_boundary(self, row: u16) -> Option { boundary(row, self.rows, self.height_px) } @@ -63,28 +65,68 @@ impl HostPixels { child_width_px: u32, child_height_px: u32, ) -> Option { - let start_x = self.geometry.column_boundary(inner.x)?; - let start_y = self.geometry.row_boundary(inner.y)?; - let end_x = self - .geometry - .column_boundary(inner.x.checked_add(inner.width)?)?; - let end_y = self - .geometry - .row_boundary(inner.y.checked_add(inner.height)?)?; - let x = self.x.checked_sub(1)?.checked_sub(start_x)?; - let y = self.y.checked_sub(1)?.checked_sub(start_y)?; - let source_width = end_x.checked_sub(start_x)?; - let source_height = end_y.checked_sub(start_y)?; - if x >= source_width || y >= source_height || child_width_px == 0 || child_height_px == 0 { + let (host_column, host_row) = self.geometry.cell(self.x, self.y)?; + let end_column = inner.x.checked_add(inner.width)?; + let end_row = inner.y.checked_add(inner.height)?; + if host_column < inner.x + || host_column >= end_column + || host_row < inner.y + || host_row >= end_row + { return None; } Some(Position::Pixels { - x: scale(x, source_width, child_width_px).checked_add(1)?, - y: scale(y, source_height, child_height_px).checked_add(1)?, + x: map_axis_within_cell( + self.x, + host_column, + inner.x, + inner.width, + self.geometry.cols, + self.geometry.width_px, + child_width_px, + )?, + y: map_axis_within_cell( + self.y, + host_row, + inner.y, + inner.height, + self.geometry.rows, + self.geometry.height_px, + child_height_px, + )?, }) } } +fn map_axis_within_cell( + pixel: u32, + host_cell: u16, + pane_start: u16, + pane_cells: u16, + host_cells: u16, + host_extent: u32, + child_extent: u32, +) -> Option { + let local_cell = host_cell.checked_sub(pane_start)?; + if local_cell >= pane_cells { + return None; + } + let source_start = boundary(host_cell, host_cells, host_extent)?; + let source_end = boundary(host_cell.checked_add(1)?, host_cells, host_extent)?; + let target_start = boundary(local_cell, pane_cells, child_extent)?; + let target_end = boundary(local_cell.checked_add(1)?, pane_cells, child_extent)?; + let source_width = source_end.checked_sub(source_start)?; + let target_width = target_end.checked_sub(target_start)?; + let offset = pixel.checked_sub(1)?.checked_sub(source_start)?; + if source_width == 0 || target_width == 0 || offset >= source_width { + return None; + } + target_start + .checked_add(scale(offset, source_width, target_width))? + .checked_add(1) +} + +#[cfg(any(unix, test))] pub(crate) fn parse_report(data: &[u8]) -> Option<(u32, u32)> { let body = data.strip_prefix(b"\x1b[<")?; let body = body @@ -97,6 +139,7 @@ pub(crate) fn parse_report(data: &[u8]) -> Option<(u32, u32)> { fields.next().is_none().then_some((x, y)) } +#[cfg(any(unix, test))] pub(crate) fn report_at_cell(data: &[u8], column: u16, row: u16) -> Option> { let body = data.strip_prefix(b"\x1b[<")?; let suffix = if body.ends_with(b"M") { 'M' } else { 'm' }; @@ -114,6 +157,7 @@ pub(crate) fn report_at_cell(data: &[u8], column: u16, row: u16) -> Option Option { (!value.is_empty() && value.iter().all(u8::is_ascii_digit)) .then(|| std::str::from_utf8(value).ok()?.parse().ok()) @@ -183,6 +227,21 @@ mod tests { ); } + #[test] + fn fractional_scaling_preserves_the_canonical_child_cell() { + let geometry = HostGeometry::new(80, 1, 805, 20).unwrap(); + assert_eq!(geometry.cell(11, 1), Some((1, 0))); + assert_eq!( + HostPixels { + x: 11, + y: 1, + geometry, + } + .pane_position(ratatui::layout::Rect::new(0, 0, 80, 1), 800, 20), + Some(Position::Pixels { x: 11, y: 1 }) + ); + } + #[test] fn geometry_rejects_outside_pixels_and_maps_cells() { let geometry = HostGeometry::new(80, 24, 800, 480).unwrap(); -- cgit v1.2.3