use std::cell::Cell;
use std::rc::Rc;
use teksilo_canvas::SizeProposal;
use teksilo_tokens::PointerKind;
use crate::event::{Key, Modifiers};
use crate::test_widgets::{FillWidget, StackWidget};
use crate::widget_builder::WidgetBuilder;
use crate::widget_id::WidgetId;
use crate::widget_tree::WidgetTree;
#[derive(Debug)]
struct Row(Vec<WidgetId>);
impl crate::widget::Widget for Row {
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &crate::widget::LayoutContext,
) -> crate::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
fn place_children(
&self,
bounds: teksilo_canvas::Rect,
_proposal: SizeProposal,
children: &mut [crate::widget::WidgetPlacement],
_ctx: &crate::widget::LayoutContext,
) {
let width = bounds.width / children.len().max(1) as f32;
for (i, child) in children.iter_mut().enumerate() {
child.origin = teksilo_canvas::Point::new(bounds.x + width * i as f32, bounds.y);
child.size = teksilo_canvas::Size::new(width, bounds.height);
}
}
fn children(&self) -> Vec<WidgetId> {
self.0.clone()
}
}
struct Fixture {
tree: WidgetTree,
row: WidgetId,
field: WidgetId,
button: WidgetId,
other: WidgetId,
taps: Rc<Cell<u32>>,
}
fn fixture(focus_on_click: bool) -> Fixture {
let taps = Rc::new(Cell::new(0));
let mut tree = WidgetTree::new();
let field = tree.add(FillWidget::new().focusable());
let button = {
let taps = taps.clone();
tree.add(
FillWidget::new()
.focusable()
.on_tap(move |_, _| taps.set(taps.get() + 1))
.focus_on_click(focus_on_click),
)
};
let other = tree.add(FillWidget::new().focusable());
let row = tree.add(Row(vec![field, button, other]));
tree.layout(SizeProposal::exact(300.0, 100.0));
Fixture {
tree,
row,
field,
button,
other,
taps,
}
}
impl Fixture {
fn centre(&self, id: WidgetId) -> teksilo_canvas::Point {
let b = self.tree.bounds(id);
teksilo_canvas::Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0)
}
}
#[test]
fn a_mouse_click_activates_without_taking_focus() {
let mut f = fixture(false);
f.tree.focus(f.field);
let at = f.centre(f.button);
f.tree.tap_with(PointerKind::Mouse, at);
assert_eq!(f.taps.get(), 1, "the click activates the button");
assert_eq!(f.tree.focused(), Some(f.field), "focus stays in the field");
}
#[test]
fn a_tap_activates_without_taking_focus() {
let mut f = fixture(false);
f.tree.focus(f.field);
let at = f.centre(f.button);
f.tree.tap_with(PointerKind::Touch, at);
assert_eq!(f.taps.get(), 1, "the tap activates the button");
assert_eq!(f.tree.focused(), Some(f.field), "focus stays in the field");
f.tree.assert_no_leaked_pointer_state();
}
#[test]
fn by_default_a_click_and_a_tap_focus_the_button() {
for kind in [PointerKind::Mouse, PointerKind::Touch] {
let mut f = fixture(true);
f.tree.focus(f.field);
let at = f.centre(f.button);
f.tree.tap_with(kind, at);
assert_eq!(f.tree.focused(), Some(f.button), "{kind:?}");
}
}
#[test]
fn it_stays_reachable_from_the_keyboard() {
let mut f = fixture(false);
assert_eq!(
f.tree.tab_stops_within(f.row),
vec![f.field, f.button, f.other]
);
f.tree.focus(f.field);
f.tree.press_key(Key::Tab, Modifiers::NONE);
assert_eq!(f.tree.focused(), Some(f.button), "Tab lands on it");
f.tree.focus(f.other);
f.tree.focus(f.button);
assert_eq!(f.tree.focused(), Some(f.button), "and so does a request");
}
#[test]
fn a_click_inside_it_does_not_focus_an_ancestor_instead() {
let mut tree = WidgetTree::new();
let field = tree.add(FillWidget::new().focusable());
let label = tree.add(FillWidget::new());
let button = tree.add(
StackWidget::new()
.child(label)
.focus_on_click(false)
.focusable(true),
);
let outer = tree.add(StackWidget::new().child(button).focusable(true));
tree.add(Row(vec![field, outer]));
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.focus(field);
let b = tree.bounds(label);
let at = teksilo_canvas::Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
for kind in [PointerKind::Mouse, PointerKind::Touch] {
tree.tap_with(kind, at);
assert_eq!(tree.focused(), Some(field), "{kind:?}");
}
}
#[test]
fn a_field_inside_a_container_built_with_it_still_takes_focus() {
let mut tree = WidgetTree::new();
let field = tree.add(FillWidget::new().focusable());
let inner = tree.add(FillWidget::new().focusable());
let container = tree.add(StackWidget::new().child(inner).focus_on_click(false));
tree.add(Row(vec![field, container]));
tree.layout(SizeProposal::exact(200.0, 100.0));
let b = tree.bounds(inner);
let at = teksilo_canvas::Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
for kind in [PointerKind::Mouse, PointerKind::Touch] {
tree.focus(field);
tree.tap_with(kind, at);
assert_eq!(tree.focused(), Some(inner), "{kind:?}");
}
}
#[test]
fn a_click_that_takes_no_focus_has_the_focused_field_commit_first() {
use crate::widget_builder::HandlerSet;
for (focus_on_click, kind, expected) in [
(false, PointerKind::Mouse, &["commit", "tap"][..]),
(false, PointerKind::Touch, &["commit", "tap"][..]),
(true, PointerKind::Mouse, &["tap"][..]),
] {
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut tree = WidgetTree::new();
let field = tree.add(FillWidget::new().focusable());
{
let log = log.clone();
tree.apply_external_handler_set(
field,
HandlerSet::new().on_commit_edit(move |_| log.borrow_mut().push("commit")),
);
}
let button = {
let log = log.clone();
tree.add(
FillWidget::new()
.focusable()
.on_tap(move |_, _| log.borrow_mut().push("tap"))
.focus_on_click(focus_on_click),
)
};
tree.add(Row(vec![field, button]));
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.focus(field);
let b = tree.bounds(button);
let at = teksilo_canvas::Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
tree.tap_with(kind, at);
assert_eq!(*log.borrow(), expected, "{focus_on_click} {kind:?}");
}
}
#[test]
fn an_assistive_click_that_takes_no_focus_has_the_focused_field_commit_first() {
use crate::event::{EventResponse, WidgetEvent};
use crate::widget_builder::HandlerSet;
for (focus_on_click, expected) in [(false, &["commit", "click"][..]), (true, &["click"][..])] {
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut tree = WidgetTree::new();
let field = tree.add(FillWidget::new().focusable());
{
let log = log.clone();
tree.apply_external_handler_set(
field,
HandlerSet::new().on_commit_edit(move |_| log.borrow_mut().push("commit")),
);
}
let button = {
let log = log.clone();
tree.add(
FillWidget::new()
.focusable()
.on_access_action(move |action, _| {
if action == accesskit::Action::Click {
log.borrow_mut().push("click");
EventResponse::Handled
} else {
EventResponse::Ignored
}
})
.focus_on_click(focus_on_click),
)
};
tree.add(Row(vec![field, button]));
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.focus(field);
tree.dispatch_event(WidgetEvent::AccessAction {
action: accesskit::Action::Click,
target: Some(button),
target_node: crate::accessibility::widget_id_to_node_id(button),
data: None,
});
assert_eq!(*log.borrow(), expected, "{focus_on_click}");
}
}
#[test]
fn a_press_that_does_not_complete_a_click_commits_nothing() {
use crate::event::{PointerButton, WidgetEvent};
use crate::pointer::{CancelReason, PointerId};
use crate::widget_builder::HandlerSet;
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
let f = fixture(false);
let mut tree = f.tree;
{
let log = log.clone();
tree.apply_external_handler_set(
f.field,
HandlerSet::new().on_commit_edit(move |_| log.borrow_mut().push("commit")),
);
}
let centre = |tree: &WidgetTree, id: WidgetId| {
let b = tree.bounds(id);
teksilo_canvas::Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0)
};
let (on, off) = (centre(&tree, f.button), centre(&tree, f.other));
tree.focus(f.field);
tree.pointer_down_button(on, PointerButton::Primary);
assert!(log.borrow().is_empty(), "nothing is committed on the press");
tree.pointer_move(off);
tree.pointer_up_button(off, PointerButton::Primary);
assert!(log.borrow().is_empty(), "a drag-off commits nothing");
tree.pointer_down_button(on, PointerButton::Primary);
tree.cancel_pointer(
PointerId::MOUSE,
CancelReason::WindowDeactivated,
&mut crate::window::NoopWindowOps,
);
tree.pointer_up_button(on, PointerButton::Primary);
assert!(log.borrow().is_empty(), "a cancelled press commits nothing");
assert_eq!(f.taps.get(), 0, "and neither activated the button");
tree.dispatch_event(WidgetEvent::pointer_down(
on,
PointerButton::Primary,
Modifiers::NONE,
));
tree.pointer_up_button(on, PointerButton::Primary);
assert_eq!(*log.borrow(), ["commit"]);
assert_eq!(f.taps.get(), 1);
assert_eq!(tree.focused(), Some(f.field));
}
#[test]
fn the_commit_crosses_from_a_popover_to_its_anchor_and_stops_at_a_modal() {
use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
use crate::widget_builder::HandlerSet;
for (modal, expected) in [(false, &["field", "page"][..]), (true, &["field"][..])] {
let placement = if modal {
OverlayPlacement::Centered
} else {
OverlayPlacement::Below
};
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut tree = WidgetTree::new();
let anchor = tree.add(FillWidget::new());
let page = tree.add(StackWidget::new().child(anchor));
let field = tree.add(FillWidget::new().focusable());
for (id, name) in [(field, "field"), (page, "page")] {
let log = log.clone();
tree.apply_external_handler_set(
id,
HandlerSet::new().on_commit_edit(move |_| log.borrow_mut().push(name)),
);
}
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.show_overlay(OverlayRequest::new(
field,
anchor,
placement,
DismissBehavior::Manual,
));
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.focus(field);
tree.commit_pending_edit(&mut crate::window::NoopWindowOps);
assert_eq!(*log.borrow(), expected, "modal: {modal}");
}
}