Add spacing between dropdown headers and items (#3472)
<img width="927" height="194" alt="image" src="https://github.com/user-attachments/assets/f4cb999b-16c3-448a-aed4-060bed8b96dd" /> <img width="1246" height="205" alt="image" src="https://github.com/user-attachments/assets/5d9ba5bd-0c02-46da-a809-b583a176528a" />
This commit is contained in:
@@ -137,11 +137,12 @@ impl BottomPaneView for ListSelectionView {
|
|||||||
|
|
||||||
fn desired_height(&self, _width: u16) -> u16 {
|
fn desired_height(&self, _width: u16) -> u16 {
|
||||||
let rows = (self.items.len()).clamp(1, MAX_POPUP_ROWS);
|
let rows = (self.items.len()).clamp(1, MAX_POPUP_ROWS);
|
||||||
// +1 for the title row, +1 for optional subtitle, +1 for optional footer
|
// +1 for the title row, +1 for a spacer line beneath the header,
|
||||||
let mut height = rows as u16 + 1;
|
// +1 for optional subtitle, +1 for optional footer
|
||||||
|
let mut height = rows as u16 + 2;
|
||||||
if self.subtitle.is_some() {
|
if self.subtitle.is_some() {
|
||||||
// +1 for subtitle, +1 for a blank spacer line beneath it
|
// +1 for subtitle (the spacer is accounted for above)
|
||||||
height = height.saturating_add(2);
|
height = height.saturating_add(1);
|
||||||
}
|
}
|
||||||
if self.footer_hint.is_some() {
|
if self.footer_hint.is_some() {
|
||||||
height = height.saturating_add(2);
|
height = height.saturating_add(2);
|
||||||
@@ -178,17 +179,18 @@ impl BottomPaneView for ListSelectionView {
|
|||||||
vec![Self::dim_prefix_span(), sub.clone().dim()];
|
vec![Self::dim_prefix_span(), sub.clone().dim()];
|
||||||
let subtitle_para = Paragraph::new(Line::from(subtitle_spans));
|
let subtitle_para = Paragraph::new(Line::from(subtitle_spans));
|
||||||
subtitle_para.render(subtitle_area, buf);
|
subtitle_para.render(subtitle_area, buf);
|
||||||
// Render the extra spacer line with the dimmed prefix to align with title/subtitle
|
next_y = next_y.saturating_add(1);
|
||||||
let spacer_area = Rect {
|
|
||||||
x: area.x,
|
|
||||||
y: next_y.saturating_add(1),
|
|
||||||
width: area.width,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
Self::render_dim_prefix_line(spacer_area, buf);
|
|
||||||
next_y = next_y.saturating_add(2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let spacer_area = Rect {
|
||||||
|
x: area.x,
|
||||||
|
y: next_y,
|
||||||
|
width: area.width,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
Self::render_dim_prefix_line(spacer_area, buf);
|
||||||
|
next_y = next_y.saturating_add(1);
|
||||||
|
|
||||||
let footer_reserved = if self.footer_hint.is_some() { 2 } else { 0 };
|
let footer_reserved = if self.footer_hint.is_some() { 2 } else { 0 };
|
||||||
let rows_area = Rect {
|
let rows_area = Rect {
|
||||||
x: area.x,
|
x: area.x,
|
||||||
@@ -245,3 +247,78 @@ impl BottomPaneView for ListSelectionView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::BottomPaneView;
|
||||||
|
use super::*;
|
||||||
|
use crate::app_event::AppEvent;
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
|
||||||
|
fn make_selection_view(subtitle: Option<&str>) -> ListSelectionView {
|
||||||
|
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||||
|
let tx = AppEventSender::new(tx_raw);
|
||||||
|
let items = vec![
|
||||||
|
SelectionItem {
|
||||||
|
name: "Read Only".to_string(),
|
||||||
|
description: Some("Codex can read files".to_string()),
|
||||||
|
is_current: true,
|
||||||
|
actions: vec![],
|
||||||
|
},
|
||||||
|
SelectionItem {
|
||||||
|
name: "Full Access".to_string(),
|
||||||
|
description: Some("Codex can edit files".to_string()),
|
||||||
|
is_current: false,
|
||||||
|
actions: vec![],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
ListSelectionView::new(
|
||||||
|
"Select Approval Mode".to_string(),
|
||||||
|
subtitle.map(str::to_string),
|
||||||
|
Some("Press Enter to confirm or Esc to go back".to_string()),
|
||||||
|
items,
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_lines(view: &ListSelectionView) -> String {
|
||||||
|
let width = 48;
|
||||||
|
let height = BottomPaneView::desired_height(view, width);
|
||||||
|
let area = Rect::new(0, 0, width, height);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
view.render(area, &mut buf);
|
||||||
|
|
||||||
|
let lines: Vec<String> = (0..area.height)
|
||||||
|
.map(|row| {
|
||||||
|
let mut line = String::new();
|
||||||
|
for col in 0..area.width {
|
||||||
|
let symbol = buf[(area.x + col, area.y + row)].symbol();
|
||||||
|
if symbol.is_empty() {
|
||||||
|
line.push(' ');
|
||||||
|
} else {
|
||||||
|
line.push_str(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_blank_line_between_title_and_items_without_subtitle() {
|
||||||
|
let view = make_selection_view(None);
|
||||||
|
assert_snapshot!(
|
||||||
|
"list_selection_spacing_without_subtitle",
|
||||||
|
render_lines(&view)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_blank_line_between_subtitle_and_items() {
|
||||||
|
let view = make_selection_view(Some("Switch between Codex approval presets"));
|
||||||
|
assert_snapshot!("list_selection_spacing_with_subtitle", render_lines(&view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/bottom_pane/list_selection_view.rs
|
||||||
|
expression: render_lines(&view)
|
||||||
|
---
|
||||||
|
▌ Select Approval Mode
|
||||||
|
▌ Switch between Codex approval presets
|
||||||
|
▌
|
||||||
|
▌> 1. Read Only (current) Codex can read files
|
||||||
|
▌ 2. Full Access Codex can edit files
|
||||||
|
|
||||||
|
Press Enter to confirm or Esc to go back
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/bottom_pane/list_selection_view.rs
|
||||||
|
expression: render_lines(&view)
|
||||||
|
---
|
||||||
|
▌ Select Approval Mode
|
||||||
|
▌
|
||||||
|
▌> 1. Read Only (current) Codex can read files
|
||||||
|
▌ 2. Full Access Codex can edit files
|
||||||
|
|
||||||
|
Press Enter to confirm or Esc to go back
|
||||||
Reference in New Issue
Block a user