#[path = "../examples/v020_named_focus.rs"]
#[allow(dead_code)]
mod named_focus_demo;
#[path = "../examples/v020_modal_trap.rs"]
#[allow(dead_code)]
mod modal_trap_demo;
use slt::{context::ModalOptions, Border, ButtonVariant, Context, EventBuilder, TestBackend};
use std::cell::RefCell;
use std::rc::Rc;
#[test]
fn named_focus_click_focuses_target_input() {
let state = Rc::new(RefCell::new(named_focus_demo::DemoState::default()));
let mut tb = TestBackend::new(80, 12);
{
let s = state.clone();
tb.render(move |ui| {
named_focus_demo::render(ui, &mut s.borrow_mut());
});
}
let (input_x, input_y) = locate_named_input(&tb, "Name");
let s1 = state.clone();
tb.sequence()
.events(
EventBuilder::new().click(input_x, input_y).key('h').build(),
move |ui| {
named_focus_demo::render(ui, &mut s1.borrow_mut());
},
)
.run();
let value = state.borrow().name.value.clone();
assert_eq!(
value,
"h",
"click + 'h' should land in state.name.value; got {value:?}.\nbuffer:\n{}",
tb.to_string_trimmed()
);
assert!(
state.borrow().email.value.is_empty(),
"email captured a keystroke meant for name"
);
assert!(
state.borrow().city.value.is_empty(),
"city captured a keystroke meant for name"
);
}
fn locate_named_input(tb: &TestBackend, label: &str) -> (u32, u32) {
let label_marker = format!("{label}:");
for y in 0..tb.height() {
let line = tb.line(y);
let Some(label_col) = char_index_of(&line, &label_marker) else {
continue;
};
let after_start = label_col + label_marker.chars().count();
let chars: Vec<char> = line.chars().collect();
let mut box_left = after_start;
for (i, ch) in chars.iter().enumerate().skip(after_start) {
if *ch == '╭' || *ch == '┌' {
box_left = i;
break;
}
}
let cursor_y = y + 1;
let cursor_x = (box_left + 2) as u32;
return (cursor_x, cursor_y);
}
panic!("could not locate input row for label {label:?}\nbuffer:\n{tb}");
}
#[test]
fn modal_trap_yes_click_persists_answer() {
let state = Rc::new(RefCell::new(modal_trap_demo::State {
show_modal: true,
answered: None,
}));
let mut tb = TestBackend::new(80, 16);
{
let s = state.clone();
tb.render(move |ui| {
render_modal_body(ui, &mut s.borrow_mut());
});
}
let (yes_x, yes_y) = locate_yes_button(&tb);
let s1 = state.clone();
tb.sequence()
.events(EventBuilder::new().click(yes_x, yes_y).build(), move |ui| {
render_modal_body(ui, &mut s1.borrow_mut());
})
.run();
assert_eq!(
state.borrow().answered,
Some(true),
"Yes click should set answered=Some(true).\nbuffer:\n{}",
tb.to_string_trimmed()
);
assert!(
!state.borrow().show_modal,
"Yes click should dismiss the modal"
);
let s2 = state.clone();
tb.sequence()
.tick(move |ui| {
render_modal_body(ui, &mut s2.borrow_mut());
})
.run();
assert_eq!(
state.borrow().answered,
Some(true),
"answered must persist across frames"
);
assert!(
!state.borrow().show_modal,
"show_modal must remain false across frames"
);
}
fn render_modal_body(ui: &mut Context, state: &mut modal_trap_demo::State) {
let sp = ui.spacing();
let _ = ui
.bordered(Border::Rounded)
.title("SLT v0.20: Modal focus trap")
.p(sp.sm())
.gap(sp.xs())
.grow(1)
.col(|ui| {
ui.text("test body").dim();
let _ = ui.container().gap(sp.sm()).row(|ui| {
let _ = ui.button("First bg button");
let _ = ui.button("Second bg button");
let _ = ui.button("Third bg button");
});
ui.text("");
if ui.button_with("Open modal", ButtonVariant::Primary).clicked {
state.show_modal = true;
state.answered = None;
}
});
if state.show_modal {
let _ = ui.modal_with(ModalOptions { tab_trap: true }, |ui| {
let _ = ui
.bordered(Border::Rounded)
.title("Confirm")
.p(sp.sm())
.gap(sp.xs())
.col(|ui| {
ui.text("Press Tab — focus stays inside the modal.").bold();
let _ = ui.container().gap(sp.sm()).row(|ui| {
if ui.button_with("Yes", ButtonVariant::Primary).clicked {
state.answered = Some(true);
state.show_modal = false;
}
if ui.button_with("No", ButtonVariant::Outline).clicked {
state.answered = Some(false);
state.show_modal = false;
}
});
ui.text("Esc to dismiss.").dim();
});
});
}
}
fn locate_yes_button(tb: &TestBackend) -> (u32, u32) {
for y in 0..tb.height() {
let line = tb.line(y);
if let Some(col) = char_index_of(&line, "Yes") {
return ((col as u32) + 1, y);
}
}
panic!("could not locate Yes button.\nbuffer:\n{tb}");
}
fn char_index_of(haystack: &str, needle: &str) -> Option<usize> {
let needle_chars: Vec<char> = needle.chars().collect();
let chars: Vec<char> = haystack.chars().collect();
if needle_chars.is_empty() || chars.len() < needle_chars.len() {
return None;
}
for start in 0..=chars.len() - needle_chars.len() {
if chars[start..start + needle_chars.len()] == needle_chars[..] {
return Some(start);
}
}
None
}
#[test]
fn code_block_lang_empty_lang_renders_tokens_inline() {
let mut tb = TestBackend::new(80, 8);
tb.render(|ui| {
let _ = ui.code_block_lang("fn main() { let x = 1; }", "");
});
let tokens = ["fn", "main", "let", "x", "=", "1"];
let buffer = tb.to_string_trimmed();
let mut shared_row: Option<u32> = None;
for y in 0..tb.height() {
let line = tb.line(y);
if tokens.iter().all(|t| line.contains(t)) {
shared_row = Some(y);
break;
}
}
assert!(
shared_row.is_some(),
"all tokens {tokens:?} must appear together on a single row.\nbuffer:\n{buffer}"
);
let row = shared_row.unwrap();
let line = tb.line(row);
for t in tokens {
assert!(
line.contains(t),
"token {t:?} missing from row {row}: {line:?}.\nbuffer:\n{buffer}"
);
}
}