mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## What changed - Remove the legacy selection-list appearance and use shared full-width selection, filled tabs, wrapped hints, and separate overflow rows throughout. - Give resume and fork pickers separate toolbar and search rows. Collapse spacing on short screens, keep the focused toolbar control visible on narrow screens, and prioritize labeled primary actions in the footer. - Apply shared selection styling and contrast-aware text colors to session rows and metadata. Clear inherited selection colors and reversal from the transcript loading overlay. - Preserve clipped-header notices alongside results in small approval dialogs. ## Testing Add rendering assertions and snapshots for session selection contrast, reversed selection, loading overlay styles, compact metadata and action labels, and keyboard-focused toolbar visibility. Extend approval clipping snapshots to shorter terminal heights. GitOrigin-RevId: 6e34b14f5528baf33e97ff6cf746107a1bbe69ab
113 lines
3.3 KiB
Rust
113 lines
3.3 KiB
Rust
//! Single-row filled tab headers for selection lists.
|
||
//!
|
||
//! Filled tabs always retain the active tab and reserve arrows for hidden neighbors.
|
||
|
||
use ratatui::buffer::Buffer;
|
||
use ratatui::layout::Rect;
|
||
use ratatui::style::Stylize;
|
||
use ratatui::text::Line;
|
||
use ratatui::widgets::Widget;
|
||
|
||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
|
||
use crate::render::renderable::Renderable;
|
||
|
||
use super::SelectionItem;
|
||
use super::picker_style::active_tab_style;
|
||
|
||
pub(crate) struct SelectionTab {
|
||
pub(crate) id: String,
|
||
pub(crate) label: String,
|
||
pub(crate) header: Box<dyn Renderable>,
|
||
pub(crate) items: Vec<SelectionItem>,
|
||
}
|
||
|
||
pub(super) fn tab_bar_height(tabs: &[SelectionTab], width: u16) -> u16 {
|
||
u16::from(!tabs.is_empty() && width > 0)
|
||
}
|
||
|
||
pub(super) fn render_tab_bar(
|
||
tabs: &[SelectionTab],
|
||
active_idx: usize,
|
||
area: Rect,
|
||
buf: &mut Buffer,
|
||
) {
|
||
let labels = tabs
|
||
.iter()
|
||
.map(|tab| tab.label.as_str())
|
||
.collect::<Vec<_>>();
|
||
render_filled_tab_bar(&labels, active_idx, area, buf);
|
||
}
|
||
|
||
/// Render a fixed-height tab strip and return the visible tab hit targets.
|
||
pub(crate) fn render_filled_tab_bar(
|
||
labels: &[&str],
|
||
active_idx: usize,
|
||
area: Rect,
|
||
buf: &mut Buffer,
|
||
) -> Vec<(usize, Rect)> {
|
||
if labels.is_empty() || area.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
let active_idx = active_idx.min(labels.len() - 1);
|
||
let widths = labels
|
||
.iter()
|
||
.map(|label| Line::from(*label).width() + 2)
|
||
.collect::<Vec<_>>();
|
||
let occupied = |start: usize, end: usize| {
|
||
widths[start..end].iter().sum::<usize>() + end - start - 1
|
||
+ usize::from(start > 0) * 2
|
||
+ usize::from(end < labels.len()) * 2
|
||
};
|
||
let mut start = active_idx;
|
||
let mut end = active_idx + 1;
|
||
while start > 0 && occupied(start - 1, end) <= usize::from(area.width) {
|
||
start -= 1;
|
||
}
|
||
while end < labels.len() && occupied(start, end + 1) <= usize::from(area.width) {
|
||
end += 1;
|
||
}
|
||
|
||
// Tiny strips prioritize a readable active label over navigation hints.
|
||
let show_left = start > 0 && area.width >= 5;
|
||
let show_right = end < labels.len() && area.width >= 7;
|
||
let mut x = area.x;
|
||
if show_left {
|
||
Line::from("‹")
|
||
.dim()
|
||
.render(Rect::new(x, area.y, /*width*/ 1, /*height*/ 1), buf);
|
||
x += 2;
|
||
}
|
||
let right = area.right().saturating_sub(u16::from(show_right) * 2);
|
||
let mut regions = Vec::new();
|
||
for idx in start..end {
|
||
let width = widths[idx].min(usize::from(right.saturating_sub(x))) as u16;
|
||
let line = Line::from(format!(" {} ", labels[idx]));
|
||
let line = truncate_line_with_ellipsis_if_overflow(line, usize::from(width));
|
||
let line = if idx == active_idx {
|
||
line.style(active_tab_style())
|
||
} else {
|
||
line.dim()
|
||
};
|
||
let tab = Rect::new(x, area.y, width, /*height*/ 1);
|
||
line.render(tab, buf);
|
||
regions.push((idx, tab));
|
||
x = x.saturating_add(width).saturating_add(/*rhs*/ 1);
|
||
}
|
||
if show_right {
|
||
Line::from("›").dim().render(
|
||
Rect::new(
|
||
area.right() - 1,
|
||
area.y,
|
||
/*width*/ 1,
|
||
/*height*/ 1,
|
||
),
|
||
buf,
|
||
);
|
||
}
|
||
regions
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "selection_tabs_tests.rs"]
|
||
mod tests;
|