summaryrefslogtreecommitdiff
path: root/src/ui/widgets.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/ui/widgets.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/ui/widgets.rs')
-rw-r--r--src/ui/widgets.rs247
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,
+ )
}