use super::*;
use crate::widgets::ModalSheet;
use crate::{Event, EventResult, Label, Stack};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use crate::text::Font;
fn font() -> Arc<Font> {
Arc::new(Font::from_slice(TEST_FONT).unwrap())
}
fn counting_button(label: &str, count: &Rc<Cell<u32>>) -> Button {
let c = Rc::clone(count);
Button::new(label, font()).on_click(move || c.set(c.get() + 1))
}
fn press(app: &mut App, key: Key) {
app.on_key_down(key, Modifiers::default());
}
#[test]
fn root_enter_fires_the_default_button() {
let hits = Rc::new(Cell::new(0));
let other = Rc::new(Cell::new(0));
let mut col = FlexColumn::new();
col.push(Box::new(counting_button("Plain", &other)), 0.0);
col.push(
Box::new(counting_button("Go", &hits).with_default_action()),
0.0,
);
let mut app = App::new(Box::new(col));
app.layout(Size::new(300.0, 300.0));
assert!(app.focused_widget_type_name().is_none());
press(&mut app, Key::Enter);
assert_eq!(hits.get(), 1, "default button fires on Enter");
assert_eq!(other.get(), 0, "only the marked button fires");
press(&mut app, Key::Char('a'));
assert_eq!(hits.get(), 1);
}
#[test]
fn focused_text_field_keeps_enter() {
let hits = Rc::new(Cell::new(0));
let mut col = FlexColumn::new();
col.push(Box::new(TextField::new(font()).with_focus_id(7)), 0.0);
col.push(
Box::new(counting_button("Go", &hits).with_default_action()),
0.0,
);
let mut app = App::new(Box::new(col));
app.layout(Size::new(300.0, 300.0));
crate::focus::request_focus(7);
app.layout(Size::new(300.0, 300.0));
assert_eq!(app.focused_widget_type_name(), Some("TextField"));
press(&mut app, Key::Enter);
assert_eq!(hits.get(), 0, "text field owns Enter");
}
#[test]
fn disabled_default_button_does_not_fire() {
let hits = Rc::new(Cell::new(0));
let mut col = FlexColumn::new();
col.push(
Box::new(
counting_button("Go", &hits)
.with_default_action()
.with_enabled_fn(|| false),
),
0.0,
);
let mut app = App::new(Box::new(col));
app.layout(Size::new(300.0, 300.0));
press(&mut app, Key::Enter);
assert_eq!(hits.get(), 0);
}
struct Harness {
app: App,
root_hits: Rc<Cell<u32>>,
}
fn sheet_with_buttons(
visible: &Rc<Cell<bool>>,
ok: &Rc<Cell<u32>>,
cancel: Option<&Rc<Cell<u32>>>,
) -> ModalSheet {
let mut col = FlexColumn::new();
col.push(Box::new(TextField::new(font())), 0.0);
if let Some(cancel) = cancel {
col.push(
Box::new(counting_button("Cancel", cancel).with_cancel_action()),
0.0,
);
}
col.push(
Box::new(counting_button("OK", ok).with_default_action()),
0.0,
);
ModalSheet::new(Rc::clone(visible), Box::new(col))
}
fn harness(sheets: Vec<ModalSheet>) -> Harness {
let root_hits = Rc::new(Cell::new(0));
let mut col = FlexColumn::new();
col.push(
Box::new(counting_button("Root default", &root_hits).with_default_action()),
0.0,
);
let mut stack = Stack::new().add(Box::new(col));
for s in sheets {
stack = stack.add(Box::new(s));
}
let mut app = App::new(Box::new(stack));
app.layout(Size::new(400.0, 400.0));
Harness { app, root_hits }
}
#[test]
fn visible_sheet_routes_enter_to_its_own_default() {
let visible = Rc::new(Cell::new(true));
let ok = Rc::new(Cell::new(0));
let mut h = harness(vec![sheet_with_buttons(&visible, &ok, None)]);
press(&mut h.app, Key::Enter);
assert_eq!(ok.get(), 1, "sheet default fires");
assert_eq!(
h.root_hits.get(),
0,
"root default is shadowed by the modal"
);
assert!(visible.get(), "firing the default does not auto-close");
}
#[test]
fn hidden_sheet_does_not_shadow_the_root_default() {
let visible = Rc::new(Cell::new(false));
let ok = Rc::new(Cell::new(0));
let mut h = harness(vec![sheet_with_buttons(&visible, &ok, None)]);
press(&mut h.app, Key::Enter);
assert_eq!(ok.get(), 0);
assert_eq!(h.root_hits.get(), 1);
}
#[test]
fn only_the_topmost_sheet_default_fires() {
let vis_a = Rc::new(Cell::new(true));
let vis_b = Rc::new(Cell::new(true));
let ok_a = Rc::new(Cell::new(0));
let ok_b = Rc::new(Cell::new(0));
let mut h = harness(vec![
sheet_with_buttons(&vis_a, &ok_a, None),
sheet_with_buttons(&vis_b, &ok_b, None),
]);
press(&mut h.app, Key::Enter);
assert_eq!(ok_b.get(), 1, "topmost sheet");
assert_eq!(ok_a.get(), 0, "sheet beneath is shadowed");
assert_eq!(h.root_hits.get(), 0);
vis_b.set(false);
h.app.layout(Size::new(400.0, 400.0));
press(&mut h.app, Key::Enter);
assert_eq!(ok_a.get(), 1);
assert_eq!(ok_b.get(), 1);
}
#[test]
fn escape_fires_the_cancel_button_and_leaves_closing_to_it() {
let visible = Rc::new(Cell::new(true));
let ok = Rc::new(Cell::new(0));
let cancel = Rc::new(Cell::new(0));
let mut h = harness(vec![sheet_with_buttons(&visible, &ok, Some(&cancel))]);
press(&mut h.app, Key::Escape);
assert_eq!(cancel.get(), 1, "cancel button fires");
assert_eq!(ok.get(), 0);
assert!(
visible.get(),
"sheet did not close itself — the button decides"
);
}
#[test]
fn escape_without_cancel_action_closes_the_sheet() {
let visible = Rc::new(Cell::new(true));
let ok = Rc::new(Cell::new(0));
let mut h = harness(vec![sheet_with_buttons(&visible, &ok, None)]);
press(&mut h.app, Key::Escape);
assert!(!visible.get());
}
#[test]
fn sheet_closure_actions_run_on_enter_and_escape() {
let visible = Rc::new(Cell::new(true));
let entered = Rc::new(Cell::new(0));
let cancelled = Rc::new(Cell::new(0));
let e = Rc::clone(&entered);
let c = Rc::clone(&cancelled);
let sheet = ModalSheet::new(
Rc::clone(&visible),
Box::new(Label::new("plain content", font())),
)
.with_default_action(move || e.set(e.get() + 1))
.with_cancel_action(move || c.set(c.get() + 1));
let mut h = harness(vec![sheet]);
press(&mut h.app, Key::Enter);
assert_eq!(entered.get(), 1);
assert_eq!(h.root_hits.get(), 0);
press(&mut h.app, Key::Escape);
assert_eq!(cancelled.get(), 1);
assert!(
visible.get(),
"closure cancel action replaces the built-in close"
);
}
#[test]
fn focused_field_inside_sheet_keeps_enter() {
let visible = Rc::new(Cell::new(true));
let ok = Rc::new(Cell::new(0));
let mut col = FlexColumn::new();
col.push(Box::new(TextField::new(font()).with_focus_id(11)), 0.0);
col.push(
Box::new(counting_button("OK", &ok).with_default_action()),
0.0,
);
let sheet = ModalSheet::new(Rc::clone(&visible), Box::new(col));
let mut h = harness(vec![sheet]);
crate::focus::request_focus(11);
h.app.layout(Size::new(400.0, 400.0));
assert_eq!(h.app.focused_widget_type_name(), Some("TextField"));
press(&mut h.app, Key::Enter);
assert_eq!(ok.get(), 0);
}
#[test]
fn sheet_without_default_still_swallows_enter() {
let visible = Rc::new(Cell::new(true));
let mut sheet = ModalSheet::new(Rc::clone(&visible), Box::new(Label::new("plain", font())));
sheet.layout(Size::new(300.0, 300.0));
let r = sheet.on_event(&Event::KeyDown {
key: Key::Enter,
modifiers: Modifiers::default(),
});
assert_eq!(r, EventResult::Consumed);
assert!(visible.get());
}
fn passthrough_sheet(visible: &Rc<Cell<bool>>, content: Box<dyn Widget>) -> ModalSheet {
ModalSheet::new(Rc::clone(visible), content).with_key_passthrough(true)
}
#[test]
fn passthrough_sheet_without_default_shadows_the_root_default() {
let visible = Rc::new(Cell::new(true));
let sheet = passthrough_sheet(&visible, Box::new(Label::new("measuring…", font())));
let mut h = harness(vec![sheet]);
press(&mut h.app, Key::Enter);
assert_eq!(
h.root_hits.get(),
0,
"root default must not fire behind a passthrough modal"
);
assert!(visible.get(), "the sheet stays open");
}
#[test]
fn passthrough_sheet_with_default_fires_only_its_own() {
let visible = Rc::new(Cell::new(true));
let ok = Rc::new(Cell::new(0));
let mut col = FlexColumn::new();
col.push(
Box::new(counting_button("Done", &ok).with_default_action()),
0.0,
);
let mut h = harness(vec![passthrough_sheet(&visible, Box::new(col))]);
press(&mut h.app, Key::Enter);
assert_eq!(ok.get(), 1, "the sheet's own default fires");
assert_eq!(h.root_hits.get(), 0, "root default stays shadowed");
}
#[test]
fn passthrough_sheet_shadows_the_root_cancel_action() {
let visible = Rc::new(Cell::new(true));
let root_cancel = Rc::new(Cell::new(0));
let mut col = FlexColumn::new();
col.push(
Box::new(counting_button("Root cancel", &root_cancel).with_cancel_action()),
0.0,
);
let sheet = ModalSheet::new(
Rc::clone(&visible),
Box::new(Label::new("measuring…", font())),
)
.with_key_passthrough(true)
.with_escape_closes(false);
let mut stack = Stack::new().add(Box::new(col));
stack = stack.add(Box::new(sheet));
let mut app = App::new(Box::new(stack));
app.layout(Size::new(400.0, 400.0));
app.on_key_down(Key::Escape, Modifiers::default());
assert_eq!(
root_cancel.get(),
0,
"root cancel must not fire behind a passthrough modal"
);
}
#[test]
fn passthrough_sheet_still_forwards_other_keys() {
let visible = Rc::new(Cell::new(true));
let sheet = passthrough_sheet(&visible, Box::new(Label::new("measuring…", font())));
let mut h = harness(vec![sheet]);
let seen = Rc::new(Cell::new(0));
let s = Rc::clone(&seen);
h.app.set_global_key_handler(move |_key, _mods| {
s.set(s.get() + 1);
true
});
press(&mut h.app, Key::Char('c'));
assert_eq!(seen.get(), 1, "plain keys still pass through");
}