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/ui/widgets.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/ui/widgets.rs')
| -rw-r--r-- | src/ui/widgets.rs | 247 |
1 files changed, 39 insertions, 208 deletions
diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index ef416134..33fcc9a0 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -1,70 +1,30 @@ use ratatui::{ - layout::{Alignment, Constraint, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Clear, Paragraph, Wrap}, - Frame, + layout::{Constraint, Layout, Rect}, + style::Color, }; use crate::app::state::Palette; -pub(super) fn render_panel_shell( - frame: &mut Frame, - area: Rect, - border_color: Color, - bg: Color, -) -> Option<Rect> { - if area.width < 2 || area.height < 2 { - return None; - } - - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(border_color)) - .border_set(ratatui::symbols::border::PLAIN) - .style(Style::default().bg(bg)); - let inner = block.inner(area); - frame.render_widget(Clear, area); - frame.render_widget(block, area); - Some(inner) -} - -pub(super) fn panel_contrast_fg(p: &Palette) -> Color { - match p.panel_bg { - Color::Reset => p.surface_dim, +pub(super) fn panel_contrast_fg(palette: &Palette) -> Color { + match palette.panel_bg { + Color::Reset => palette.surface_dim, color => color, } } -pub(crate) fn centered_popup_rect(area: Rect, popup_w: u16, popup_h: u16) -> Option<Rect> { - let popup_w = popup_w.min(area.width.saturating_sub(4)); - let popup_h = popup_h.min(area.height.saturating_sub(2)); - if popup_w < 4 || popup_h < 4 { +pub(crate) fn centered_popup_rect(area: Rect, popup_width: u16, popup_height: u16) -> Option<Rect> { + let popup_width = popup_width.min(area.width.saturating_sub(4)); + let popup_height = popup_height.min(area.height.saturating_sub(2)); + if popup_width < 4 || popup_height < 4 { return None; } - let popup_x = area.x + (area.width.saturating_sub(popup_w)) / 2; - let popup_y = area.y + (area.height.saturating_sub(popup_h)) / 2; - Some(Rect::new(popup_x, popup_y, popup_w, popup_h)) -} - -pub(super) fn render_modal_shell( - frame: &mut Frame, - area: Rect, - popup_w: u16, - popup_h: u16, - p: &Palette, -) -> Option<Rect> { - let popup = centered_popup_rect(area, popup_w, popup_h)?; - render_panel_shell(frame, popup, p.accent, p.panel_bg) -} - -pub(super) fn render_modal_header(frame: &mut Frame, area: Rect, title: &str, p: &Palette) { - let line = Line::from(vec![Span::styled( - title, - Style::default().fg(p.text).add_modifier(Modifier::BOLD), - )]); - frame.render_widget(Paragraph::new(line), area); + Some(Rect::new( + area.x + area.width.saturating_sub(popup_width) / 2, + area.y + area.height.saturating_sub(popup_height) / 2, + popup_width, + popup_height, + )) } #[derive(Debug, Clone, Copy)] @@ -110,169 +70,40 @@ pub(crate) fn modal_stack_areas( } let areas = Layout::vertical(constraints).split(inner); - let mut header = Rect::default(); - let mut content = Rect::default(); - let mut footer = None; - let mut actions = None; - + let mut result = ModalStackAreas { + header: Rect::default(), + content: Rect::default(), + footer: None, + actions: None, + }; for (slot, area) in slots.into_iter().zip(areas.iter().step_by(2).copied()) { match slot { - Slot::Header => header = area, - Slot::Content => content = area, - Slot::Footer => footer = Some(area), - Slot::Actions => actions = Some(area), + Slot::Header => result.header = area, + Slot::Content => result.content = area, + Slot::Footer => result.footer = Some(area), + Slot::Actions => result.actions = Some(area), } } - - ModalStackAreas { - header, - content, - footer, - actions, - } + result } -pub(crate) fn action_button_text(hint: Option<&str>, label: &str) -> String { +fn action_button_width(hint: Option<&str>, label: &str) -> u16 { match hint { - Some(hint) => format!(" {hint} {label} "), - None => format!(" {label} "), + Some(hint) => format!(" {hint} {label} ").chars().count() as u16, + None => format!(" {label} ").chars().count() as u16, } } -pub(crate) fn action_button_width(hint: Option<&str>, label: &str) -> u16 { - action_button_text(hint, label).chars().count() as u16 +pub(crate) fn close_button_rect(area: Rect) -> Rect { + let width = action_button_width(Some("esc"), "close"); + Rect::new(area.x + area.width.saturating_sub(width), area.y, width, 1) } -pub(crate) struct ActionButtonSpec<'a> { - pub hint: Option<&'a str>, - pub label: &'a str, -} - -pub(crate) fn action_button_row_rects( - area: Rect, - buttons: &[ActionButtonSpec<'_>], - gap: u16, - row_offset: u16, -) -> Vec<Rect> { - let widths: Vec<u16> = buttons - .iter() - .map(|button| action_button_width(button.hint, button.label)) - .collect(); - centered_button_row(area, &widths, gap, row_offset) -} - -pub(super) fn render_action_button( - frame: &mut Frame, - rect: Rect, - hint: Option<&str>, - label: &str, - style: Style, -) { - frame.render_widget( - Paragraph::new(action_button_text(hint, label)) - .style(style) - .alignment(Alignment::Center), - rect, - ); -} - -pub(crate) fn render_modal_description(frame: &mut Frame, area: Rect, text: &str, style: Style) { - frame.render_widget( - Paragraph::new(format!(" {text}")) - .style(style) - .wrap(Wrap { trim: false }), - area, - ); -} - -pub(crate) fn modal_choice_rows(area: Rect, count: usize, row_height: u16) -> Vec<Rect> { - let mut rows = Vec::with_capacity(count); - let mut y = area.y; - for _ in 0..count { - if y >= area.y + area.height { - break; - } - let remaining = area.y + area.height - y; - let height = row_height.min(remaining); - rows.push(Rect::new(area.x, y, area.width, height)); - y = y.saturating_add(row_height); - } - rows -} - -pub(crate) fn render_modal_choice_list<T>( - frame: &mut Frame, - area: Rect, - title: &str, - description: &str, - options: &[(&str, T)], - current_value: T, - selected_idx: usize, - p: &Palette, - row_height: u16, -) where - T: Copy + PartialEq, -{ - let [desc_area, _, list_area] = Layout::vertical([ - Constraint::Length(2), - Constraint::Length(1), - Constraint::Min(2), - ]) - .areas::<3>(area); - - render_modal_description( - frame, - desc_area, - description, - Style::default().fg(p.overlay1), - ); - - let rows = modal_choice_rows(list_area, options.len(), row_height); - for (idx, ((label, value), row)) in options.iter().zip(rows.iter()).enumerate() { - let is_active = *value == current_value; - let is_selected = idx == selected_idx; - let marker = if is_active { " ✓" } else { "" }; - let style = if is_selected { - Style::default() - .bg(p.surface0) - .fg(p.text) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(p.subtext0) - }; - frame.render_widget( - Paragraph::new(format!(" {title}: {label}{marker}")) - .style(style) - .wrap(Wrap { trim: false }), - *row, - ); - } -} - -pub(super) fn centered_button_row( - inner: Rect, - widths: &[u16], - gap: u16, - row_offset: u16, -) -> Vec<Rect> { - let total_w = widths - .iter() - .copied() - .sum::<u16>() - .saturating_add(gap.saturating_mul(widths.len().saturating_sub(1) as u16)); - let mut x = inner.x + inner.width.saturating_sub(total_w) / 2; - let y = inner.y + row_offset.min(inner.height.saturating_sub(1)); - widths - .iter() - .map(|w| { - let rect = Rect::new( - x, - y, - (*w).min(inner.width.saturating_sub(x.saturating_sub(inner.x))), - 1, - ); - x = x.saturating_add(*w).saturating_add(gap); - rect - }) - .collect() +pub(crate) fn continue_button_rect(area: Rect) -> Rect { + Rect::new( + area.x, + area.y, + action_button_width(Some("↵"), "continue"), + 1, + ) } |
