diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-01 15:48:22 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:48:22 +0300 |
| commit | 207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch) | |
| tree | d7b037ecdf629224a048c841d11659efdc23507d /src/client/shell/scroll.rs | |
| parent | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (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.rs | 98 |
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); + } +} |
