use std::cell::RefCell;
use std::rc::Rc;
use abstracttui::app::{App, Driver, RunConfig};
use abstracttui::base::{Rect, Size};
use abstracttui::prelude::*;
use abstracttui::term::Capabilities;
use abstracttui::testing::CaptureTerm;
use abstracttui::ui::{text, Phase, UiEvent};
const W: i32 = 44;
const H: i32 = 14;
fn config() -> RunConfig {
RunConfig {
caps: Some(Capabilities::with(|c| {
c.truecolor = true;
c.colors_256 = true;
})),
enter: None,
probe: false,
}
}
fn settle(driver: &mut Driver, app: &mut App, term: &mut CaptureTerm) {
for _ in 0..64 {
let turn = driver.turn(app, term).expect("turn");
if turn.idle {
return;
}
}
panic!("loop failed to settle within 64 turns");
}
fn screen_lines(term: &CaptureTerm) -> Vec<String> {
term.screen()
.to_text()
.lines()
.map(str::to_string)
.collect()
}
fn sgr_click_bytes(x: i32, y: i32) -> (Vec<u8>, Vec<u8>) {
(
format!("\x1b[<0;{};{}M", x + 1, y + 1).into_bytes(),
format!("\x1b[<0;{};{}m", x + 1, y + 1).into_bytes(),
)
}
fn col_of(line: &str, needle: &str) -> i32 {
let byte = line
.find(needle)
.unwrap_or_else(|| panic!("{needle:?} in {line:?}"));
line[..byte].chars().count() as i32
}
fn channel_options() -> Vec<SelectOption> {
vec![
SelectOption::new("stable").hint("lts"),
SelectOption::new("beta"),
SelectOption::new("nightly"),
SelectOption::new("archive").disabled(true),
SelectOption::new("custom"),
]
}
type SelectAppState = (Signal<usize>, Rc<RefCell<Vec<usize>>>, Rc<RefCell<u32>>);
fn select_app(app: &mut App) -> SelectAppState {
let changes: Rc<RefCell<Vec<usize>>> = Default::default();
let hits: Rc<RefCell<u32>> = Default::default();
let holder: Rc<RefCell<Option<Signal<usize>>>> = Default::default();
let (c2, h2, v2) = (changes.clone(), hits.clone(), holder.clone());
app.mount(move |cx| {
let value = cx.signal(0usize);
*v2.borrow_mut() = Some(value);
let c3 = c2.clone();
let h3 = h2.clone();
Element::new()
.style(LayoutStyle::column())
.child(text("== settings chrome =="))
.child(
Element::new()
.style(LayoutStyle::row().h(1).gap(2))
.child(
Select::new(channel_options())
.value(value)
.layout(LayoutStyle::default().w(20).h(1).shrink(0.0))
.on_change(move |i| c3.borrow_mut().push(i))
.view(cx),
)
.child(
abstracttui::widgets::Button::new("danger")
.on_click(move || *h3.borrow_mut() += 1)
.view(cx),
)
.build(),
)
.child(dyn_view(LayoutStyle::line(1), move || {
text(format!("value = {}", value.get()))
}))
.child(
Element::new()
.style(LayoutStyle::default().grow(1.0))
.build(),
)
.child(text(" status: ready"))
.build()
})
.expect("mount");
let value = holder.borrow().expect("value signal");
(value, changes, hits)
}
#[test]
fn select_full_keyboard_round_trip_with_damage_containment() {
let mut app = App::new(Size::new(W, H));
let (value, changes, _hits) = select_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
let before = screen_lines(&term);
assert!(before[1].contains("stable"), "trigger shows the value");
assert!(before[1].contains("▾"), "chevron affordance");
assert!(before[2].contains("value = 0"));
term.push_input(b"\t");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\r");
settle(&mut driver, &mut app, &mut term);
let open_bytes = term.take_bytes().len();
let with_popup = screen_lines(&term);
assert!(
with_popup[2].contains("stable") && with_popup[2].contains("lts"),
"popup row 0 under the trigger (hint renders): {with_popup:?}"
);
assert!(with_popup[3].contains("beta"));
assert!(
with_popup[6].contains("custom"),
"all five options: {with_popup:?}"
);
let popup_rows: Vec<usize> = vec![2, 3, 4, 5, 6];
term.push_input(b"\x1b[B"); settle(&mut driver, &mut app, &mut term);
let nav_bytes = term.take_bytes();
let after_nav = screen_lines(&term);
for (i, (a, b)) in with_popup.iter().zip(&after_nav).enumerate() {
if !popup_rows.contains(&i) {
assert_eq!(a, b, "row {i} outside the popup changed");
}
}
let nav_text = String::from_utf8_lossy(&nav_bytes);
assert!(
!nav_text.contains("chrome") && !nav_text.contains("status"),
"static chrome must not re-emit: {nav_text:?}"
);
assert!(
nav_bytes.len() < open_bytes,
"highlight flip ({} bytes) cheaper than popup open ({} bytes)",
nav_bytes.len(),
open_bytes
);
println!(
"measured: select popup-open frame {} bytes; highlight-move frame {} bytes",
open_bytes,
nav_bytes.len()
);
assert!(
after_nav[2].contains("stable") || after_nav[2].contains("lts"),
"popup covers the value line while open"
);
assert_eq!(value.get_untracked(), 0, "movement is not activation");
assert!(changes.borrow().is_empty());
term.push_input(b"\r");
settle(&mut driver, &mut app, &mut term);
let after_commit = screen_lines(&term);
assert_eq!(value.get_untracked(), 1, "beta committed");
assert_eq!(changes.borrow().as_slice(), [1], "exactly one on_change");
assert!(
after_commit[1].contains("beta"),
"trigger re-renders the value"
);
assert!(
after_commit[2].contains("value = 1"),
"vacated region repainted from below: {after_commit:?}"
);
assert!(!after_commit[3].contains("beta"), "popup gone");
term.push_input(b"\r");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1b[B");
term.push_input(b"\x1b[27u"); settle(&mut driver, &mut app, &mut term);
assert_eq!(value.get_untracked(), 1, "Escape restored nothing new");
assert_eq!(changes.borrow().as_slice(), [1]);
assert!(
screen_lines(&term)[2].contains("value = 1"),
"popup closed on Escape"
);
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0, "all bytes modeled");
}
#[test]
fn sgr_click_opens_picks_and_outside_press_never_acts_below() {
let mut app = App::new(Size::new(W, H));
let (value, _changes, hits) = select_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
let first = screen_lines(&term);
let button_row = first
.iter()
.position(|l| l.contains("danger"))
.expect("button visible") as i32;
let button_x = col_of(&first[button_row as usize], "danger");
let (press, release) = sgr_click_bytes(3, 1);
term.push_input(&press);
term.push_input(&release);
settle(&mut driver, &mut app, &mut term);
let lines = screen_lines(&term);
let nightly_row = lines
.iter()
.position(|l| l.contains("nightly"))
.expect("popup open") as i32;
let (press, release) = sgr_click_bytes(button_x + 2, button_row);
term.push_input(&press);
term.push_input(&release);
settle(&mut driver, &mut app, &mut term);
assert_eq!(*hits.borrow(), 0, "outside press must not reach the button");
assert!(
!screen_lines(&term).iter().any(|l| l.contains("nightly")),
"popup dismissed"
);
let (press, release) = sgr_click_bytes(button_x + 2, button_row);
term.push_input(&press);
term.push_input(&release);
settle(&mut driver, &mut app, &mut term);
assert_eq!(*hits.borrow(), 1, "closed popup: clicks act normally");
let (press, release) = sgr_click_bytes(3, 1);
term.push_input(&press);
term.push_input(&release);
settle(&mut driver, &mut app, &mut term);
let (press, release) = sgr_click_bytes(4, nightly_row);
term.push_input(&press);
term.push_input(&release);
settle(&mut driver, &mut app, &mut term);
assert_eq!(value.get_untracked(), 2, "click committed nightly");
assert!(
screen_lines(&term)[2].contains("value = 2"),
"value line live"
);
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0);
}
#[test]
fn select_popup_inside_stacked_modals_receives_keys_and_returns_them() {
let mut app = App::new(Size::new(W, H));
let overlays = app.overlays();
let modal2_keys: Rc<RefCell<Vec<char>>> = Default::default();
let holder: Rc<RefCell<Option<Signal<usize>>>> = Default::default();
let (mk, vh) = (modal2_keys.clone(), holder.clone());
app.mount(move |cx| {
let value = cx.signal(0usize);
*vh.borrow_mut() = Some(value);
let overlays = overlays.clone();
let mk = mk.clone();
Element::new()
.style(LayoutStyle::column())
.child(text("root world"))
.shortcut(KeyChord::plain(Key::Char('m')), move |_| {
let overlays2 = overlays.clone();
let mk2 = mk.clone();
let m1cx = cx.child();
overlays.layer_tree(
1000,
Rect::new(2, 2, 40, 10),
true,
m1cx,
Element::new()
.style(LayoutStyle::column())
.on(Phase::Bubble, move |ctx, ev| {
if let UiEvent::Key(k) = ev {
if k.key == Key::Char('n') {
let m2cx = m1cx.child();
let mk3 = mk2.clone();
overlays2.layer_tree(
1100,
Rect::new(4, 3, 34, 8),
true,
m2cx,
Element::new()
.style(LayoutStyle::column())
.on(Phase::Bubble, move |_ctx, ev| {
if let UiEvent::Key(k) = ev {
if let Key::Char(c) = k.key {
mk3.borrow_mut().push(c);
}
}
})
.child(text("modal two"))
.child(
Select::new(channel_options())
.value(value)
.layout(
LayoutStyle::default()
.w(18)
.h(1)
.shrink(0.0),
)
.view(m2cx),
)
.build(),
);
ctx.stop_propagation();
}
}
})
.child(text("modal one"))
.build(),
);
})
.build()
})
.expect("mount");
let value = holder.borrow().expect("value");
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"m"); settle(&mut driver, &mut app, &mut term);
assert!(screen_lines(&term).iter().any(|l| l.contains("modal one")));
term.push_input(b"n"); settle(&mut driver, &mut app, &mut term);
assert!(screen_lines(&term).iter().any(|l| l.contains("modal two")));
term.push_input(b"\r");
settle(&mut driver, &mut app, &mut term);
let lines = screen_lines(&term);
assert!(
lines.iter().any(|l| l.contains("nightly")),
"popup visible above the modal stack: {lines:?}"
);
term.push_input(b"\x1b[B\x1b[B\r");
settle(&mut driver, &mut app, &mut term);
assert_eq!(value.get_untracked(), 2, "popup owned the keys");
assert!(modal2_keys.borrow().is_empty(), "modal two heard nothing");
assert!(
!screen_lines(&term).iter().any(|l| l.contains("beta")),
"popup closed"
);
term.push_input(b"z");
settle(&mut driver, &mut app, &mut term);
assert_eq!(
modal2_keys.borrow().as_slice(),
['z'],
"second modal owns keys again"
);
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0);
}