use std::cell::{Cell, RefCell};
use std::rc::Rc;
use teksilo_canvas::SizeProposal;
use crate::signal::Signal;
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()
}
}
fn size() -> SizeProposal {
SizeProposal::exact(400.0, 300.0)
}
struct Gate {
tree: WidgetTree,
go: WidgetId,
branch: WidgetId,
leaf: WidgetId,
other: WidgetId,
shown: Signal<bool>,
}
fn gate(show_on_tap: bool) -> Gate {
let shown = Signal::new(false);
let target: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let mut tree = WidgetTree::new();
let leaf = tree.add(FillWidget::new().focusable());
let branch = tree.add(StackWidget::new().child(leaf));
tree.visible_when(branch, shown.clone());
let other = tree.add(FillWidget::new().focusable());
let go = {
let (shown, target) = (shown.clone(), target.clone());
tree.add(FillWidget::new().on_tap(move |_, ctx| {
if show_on_tap {
shown.set(true);
}
if let Some(id) = target.get() {
ctx.request_focus(id);
}
}))
};
tree.add(StackWidget::new().child(branch).child(other).child(go));
tree.layout(size());
assert!(
!tree.is_active(leaf),
"the fixture starts with the branch hidden"
);
target.set(Some(leaf));
Gate {
tree,
go,
branch,
leaf,
other,
shown,
}
}
#[test]
fn a_request_into_a_branch_the_handler_shows_lands_after_the_layout() {
let mut g = gate(true);
g.tree.focus(g.other);
g.tree.click(g.go);
assert_eq!(
g.tree.focused(),
Some(g.other),
"a dormant node is never focused outright"
);
g.tree.layout(size());
assert!(g.tree.is_active(g.leaf));
assert_eq!(g.tree.focused(), Some(g.leaf));
}
#[test]
fn a_request_whose_target_stays_dormant_is_dropped() {
let mut g = gate(false);
g.tree.focus(g.other);
g.tree.click(g.go);
assert!(g.tree.has_pending_focus_request());
g.tree.layout(size());
assert!(!g.tree.has_pending_focus_request(), "dropped by the pass");
assert_eq!(g.tree.focused(), Some(g.other));
g.shown.set(true);
g.tree.layout(size());
assert!(g.tree.is_active(g.leaf));
assert_eq!(
g.tree.focused(),
Some(g.other),
"a stale request does not come back when its target finally wakes"
);
}
#[test]
fn a_later_focus_move_cancels_the_waiting_request() {
let mut g = gate(true);
g.tree.click(g.go);
assert!(g.tree.has_pending_focus_request());
g.tree.focus(g.other);
g.tree.layout(size());
assert!(g.tree.is_active(g.leaf));
assert_eq!(g.tree.focused(), Some(g.other));
}
#[test]
fn a_pointer_or_keyboard_focus_cancels_the_waiting_request() {
for origin in [
crate::focus::FocusOrigin::POINTER,
crate::focus::FocusOrigin::Keyboard,
] {
let mut g = gate(true);
g.tree.click(g.go);
assert!(g.tree.has_pending_focus_request());
g.tree.focus_with_origin(g.other, origin);
g.tree.layout(size());
assert!(g.tree.is_active(g.leaf));
assert_eq!(g.tree.focused(), Some(g.other), "{origin:?}");
}
}
#[test]
fn a_later_request_for_the_focused_node_still_cancels() {
let mut g = gate(true);
g.tree.focus(g.other);
g.tree.click(g.go);
g.tree.focus(g.other);
g.tree.layout(size());
assert_eq!(g.tree.focused(), Some(g.other));
}
#[test]
fn tree_focus_on_a_dormant_node_waits_for_it() {
let mut g = gate(false);
g.tree.focus(g.other);
g.tree.focus(g.leaf);
assert_eq!(g.tree.focused(), Some(g.other));
g.shown.set(true);
g.tree.layout(size());
assert_eq!(g.tree.focused(), Some(g.leaf));
}
#[test]
fn a_destroyed_target_drops_the_waiting_request() {
let mut g = gate(true);
g.tree.focus(g.other);
g.tree.click(g.go);
g.tree.destroy_subtree(g.branch);
g.tree.layout(size());
assert_eq!(g.tree.focused(), Some(g.other));
assert!(!g.tree.has_pending_focus_request());
}
#[test]
fn clear_focus_cancels_the_waiting_request() {
let mut g = gate(true);
g.tree.focus(g.other);
g.tree.click(g.go);
g.tree.clear_focus();
g.tree.layout(size());
assert!(g.tree.is_active(g.leaf));
assert_eq!(g.tree.focused(), None);
}
#[test]
fn clear_focus_is_a_move_to_nothing() {
let within = Signal::new(false);
let seen: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
let mut tree = WidgetTree::new();
let leaf = {
let seen = seen.clone();
tree.add(
FillWidget::new()
.focusable()
.on_focus(move |gained, _| seen.borrow_mut().push(gained)),
)
};
tree.add(StackWidget::new().child(leaf).focus_within(within.clone()));
tree.layout(size());
tree.focus_with_origin(leaf, crate::focus::FocusOrigin::Keyboard);
assert!(within.get());
assert!(
tree.focus_visible_signal().get(),
"a keyboard focus shows the ring"
);
tree.a11y_dirty = false;
tree.clear_focus();
assert_eq!(tree.focused(), None);
assert_eq!(*seen.borrow(), vec![true, false], "FocusLost was delivered");
assert!(!within.get(), "focus_within follows focus out");
assert!(!tree.focus_visible_signal().get(), "the ring goes with it");
assert!(tree.a11y_dirty, "the accessibility tree is told");
tree.clear_focus();
assert_eq!(seen.borrow().len(), 2);
}
#[test]
fn ctx_clear_focus_from_a_background_tap() {
let mut tree = WidgetTree::new();
let field = tree.add(FillWidget::new().focusable());
let background = tree.add(FillWidget::new().on_tap(|_, ctx| ctx.clear_focus()));
tree.add(Row(vec![background, field]));
tree.layout(size());
tree.focus(field);
tree.click(background);
assert_eq!(tree.focused(), None);
}
#[test]
fn ctx_clear_focus_orders_with_the_requests_around_it() {
let mut tree = WidgetTree::new();
let a = tree.add(FillWidget::new().focusable());
let b = tree.add(FillWidget::new().focusable());
let clear_then_request = tree.add(FillWidget::new().on_tap(move |_, ctx| {
ctx.clear_focus();
ctx.request_focus(b);
}));
let request_then_clear = tree.add(FillWidget::new().on_tap(move |_, ctx| {
ctx.request_focus(b);
ctx.clear_focus();
}));
tree.add(Row(vec![a, b, clear_then_request, request_then_clear]));
tree.layout(size());
tree.focus(a);
tree.click(clear_then_request);
assert_eq!(tree.focused(), Some(b));
tree.focus(a);
tree.click(request_then_clear);
assert_eq!(tree.focused(), None);
}
#[test]
fn a_focus_lost_handler_can_redirect_a_clear() {
let mut tree = WidgetTree::new();
let fallback = tree.add(FillWidget::new().focusable());
let leaf = tree.add(FillWidget::new().focusable().on_focus(move |gained, ctx| {
if !gained {
ctx.request_focus(fallback);
}
}));
tree.add(StackWidget::new().child(leaf).child(fallback));
tree.layout(size());
tree.focus(leaf);
tree.clear_focus();
assert_eq!(tree.focused(), Some(fallback));
}