mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Introduce a full codex-tui source snapshot under the new codex-tui2 crate so viewport work can be replayed in isolation. This change copies the entire codex-rs/tui/src tree into codex-rs/tui2/src in one atomic step, rather than piecemeal, to keep future diffs vs the original viewport bookmark easy to reason about. The goal is for codex-tui2 to render identically to the existing TUI behind the `features.tui2` flag while we gradually port the viewport/history commits from the joshka/viewport bookmark onto this forked tree. While on this baseline change, we also ran the codex-tui2 snapshot test suite and accepted all insta snapshots for the new crate, so the snapshot files now use the codex-tui2 naming scheme and encode the unmodified legacy TUI behavior. This keeps later viewport commits focused on intentional behavior changes (and their snapshots) rather than on mechanical snapshot renames.
116 lines
3.3 KiB
Rust
116 lines
3.3 KiB
Rust
/// Generic scroll/selection state for a vertical list menu.
|
|
///
|
|
/// Encapsulates the common behavior of a selectable list that supports:
|
|
/// - Optional selection (None when list is empty)
|
|
/// - Wrap-around navigation on Up/Down
|
|
/// - Maintaining a scroll window (`scroll_top`) so the selected row stays visible
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
pub(crate) struct ScrollState {
|
|
pub selected_idx: Option<usize>,
|
|
pub scroll_top: usize,
|
|
}
|
|
|
|
impl ScrollState {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
selected_idx: None,
|
|
scroll_top: 0,
|
|
}
|
|
}
|
|
|
|
/// Reset selection and scroll.
|
|
pub fn reset(&mut self) {
|
|
self.selected_idx = None;
|
|
self.scroll_top = 0;
|
|
}
|
|
|
|
/// Clamp selection to be within the [0, len-1] range, or None when empty.
|
|
pub fn clamp_selection(&mut self, len: usize) {
|
|
self.selected_idx = match len {
|
|
0 => None,
|
|
_ => Some(self.selected_idx.unwrap_or(0).min(len - 1)),
|
|
};
|
|
if len == 0 {
|
|
self.scroll_top = 0;
|
|
}
|
|
}
|
|
|
|
/// Move selection up by one, wrapping to the bottom when necessary.
|
|
pub fn move_up_wrap(&mut self, len: usize) {
|
|
if len == 0 {
|
|
self.selected_idx = None;
|
|
self.scroll_top = 0;
|
|
return;
|
|
}
|
|
self.selected_idx = Some(match self.selected_idx {
|
|
Some(idx) if idx > 0 => idx - 1,
|
|
Some(_) => len - 1,
|
|
None => 0,
|
|
});
|
|
}
|
|
|
|
/// Move selection down by one, wrapping to the top when necessary.
|
|
pub fn move_down_wrap(&mut self, len: usize) {
|
|
if len == 0 {
|
|
self.selected_idx = None;
|
|
self.scroll_top = 0;
|
|
return;
|
|
}
|
|
self.selected_idx = Some(match self.selected_idx {
|
|
Some(idx) if idx + 1 < len => idx + 1,
|
|
_ => 0,
|
|
});
|
|
}
|
|
|
|
/// Adjust `scroll_top` so that the current `selected_idx` is visible within
|
|
/// the window of `visible_rows`.
|
|
pub fn ensure_visible(&mut self, len: usize, visible_rows: usize) {
|
|
if len == 0 || visible_rows == 0 {
|
|
self.scroll_top = 0;
|
|
return;
|
|
}
|
|
if let Some(sel) = self.selected_idx {
|
|
if sel < self.scroll_top {
|
|
self.scroll_top = sel;
|
|
} else {
|
|
let bottom = self.scroll_top + visible_rows - 1;
|
|
if sel > bottom {
|
|
self.scroll_top = sel + 1 - visible_rows;
|
|
}
|
|
}
|
|
} else {
|
|
self.scroll_top = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::ScrollState;
|
|
|
|
#[test]
|
|
fn wrap_navigation_and_visibility() {
|
|
let mut s = ScrollState::new();
|
|
let len = 10;
|
|
let vis = 5;
|
|
|
|
s.clamp_selection(len);
|
|
assert_eq!(s.selected_idx, Some(0));
|
|
s.ensure_visible(len, vis);
|
|
assert_eq!(s.scroll_top, 0);
|
|
|
|
s.move_up_wrap(len);
|
|
s.ensure_visible(len, vis);
|
|
assert_eq!(s.selected_idx, Some(len - 1));
|
|
match s.selected_idx {
|
|
Some(sel) => assert!(s.scroll_top <= sel),
|
|
None => panic!("expected Some(selected_idx) after wrap"),
|
|
}
|
|
|
|
s.move_down_wrap(len);
|
|
s.ensure_visible(len, vis);
|
|
assert_eq!(s.selected_idx, Some(0));
|
|
assert_eq!(s.scroll_top, 0);
|
|
}
|
|
}
|