aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/scroll.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/client/shell/scroll.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff)
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
Diffstat (limited to 'src/client/shell/scroll.rs')
-rw-r--r--src/client/shell/scroll.rs98
1 files changed, 98 insertions, 0 deletions
diff --git a/src/client/shell/scroll.rs b/src/client/shell/scroll.rs
new file mode 100644
index 00000000..a0496cb8
--- /dev/null
+++ b/src/client/shell/scroll.rs
@@ -0,0 +1,98 @@
+use ratatui::{buffer::Buffer, layout::Rect, style::Style};
+
+use super::Palette;
+
+pub(super) fn list_scroll_metrics(
+ row_heights: &[u16],
+ gaps_after: &[u16],
+ body_height: u16,
+ requested_start: usize,
+) -> crate::pane::ScrollMetrics {
+ if row_heights.is_empty() || body_height == 0 {
+ return crate::pane::ScrollMetrics {
+ offset_from_bottom: 0,
+ max_offset_from_bottom: 0,
+ viewport_rows: 0,
+ };
+ }
+
+ let mut used = 0u16;
+ let mut max_start = row_heights.len();
+ for index in (0..row_heights.len()).rev() {
+ let height = row_heights[index].max(1).min(body_height);
+ let gap = gaps_after.get(index).copied().unwrap_or(0);
+ if used.saturating_add(height).saturating_add(gap) > body_height {
+ break;
+ }
+ used = used.saturating_add(height).saturating_add(gap);
+ max_start = index;
+ }
+ max_start = max_start.min(row_heights.len().saturating_sub(1));
+ let start = requested_start.min(max_start);
+
+ let mut viewport_rows = 0usize;
+ let mut used = 0u16;
+ for (index, row_height) in row_heights.iter().enumerate().skip(start) {
+ let height = (*row_height).max(1).min(body_height);
+ if used.saturating_add(height) > body_height {
+ break;
+ }
+ used = used.saturating_add(height);
+ viewport_rows += 1;
+ let gap = gaps_after.get(index).copied().unwrap_or(0);
+ if used.saturating_add(gap) > body_height {
+ break;
+ }
+ used = used.saturating_add(gap);
+ }
+
+ crate::pane::ScrollMetrics {
+ offset_from_bottom: max_start.saturating_sub(start),
+ max_offset_from_bottom: max_start,
+ viewport_rows,
+ }
+}
+
+pub(super) fn render_list_scrollbar(
+ buffer: &mut Buffer,
+ track: Rect,
+ metrics: crate::pane::ScrollMetrics,
+ palette: &Palette,
+) {
+ let Some(thumb) = crate::ui::scrollbar_thumb(metrics, track) else {
+ return;
+ };
+ for row in track.y..track.bottom() {
+ if let Some(cell) = buffer.cell_mut((track.x, row)) {
+ cell.set_symbol("▕")
+ .set_style(Style::default().fg(palette.surface_dim));
+ }
+ }
+ for row in thumb.top..thumb.top.saturating_add(thumb.len) {
+ if let Some(cell) = buffer.cell_mut((track.x, row)) {
+ cell.set_symbol("▕")
+ .set_style(Style::default().fg(palette.overlay0));
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn list_metrics_preserve_variable_rows_and_caller_owned_gap_policy() {
+ let top = list_scroll_metrics(&[1, 3, 2], &[1, 1, 0], 5, 0);
+ assert_eq!(top.max_offset_from_bottom, 2);
+ assert_eq!(top.offset_from_bottom, 2);
+ assert_eq!(top.viewport_rows, 2);
+
+ let bottom = list_scroll_metrics(&[1, 3, 2], &[1, 1, 0], 5, usize::MAX);
+ assert_eq!(bottom.offset_from_bottom, 0);
+ assert_eq!(bottom.viewport_rows, 1);
+
+ let parent_child = list_scroll_metrics(&[2, 2, 2], &[0, 1, 0], 5, 0);
+ assert_eq!(parent_child.max_offset_from_bottom, 1);
+ assert_eq!(parent_child.viewport_rows, 2);
+ }
+}