use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use frust_core::{
AnyView, BuildCtx, ChangeFlags, EventCtx, EventResult, InputEvent, PointerButton, PointerEvent,
PointerPhase, View, Widget,
};
use kurbo::Point;
use crate::ChildKey;
pub use crate::image::ImageSource;
pub use crate::text::{ThemeTextColor, ThemeTextType};
pub use crate::overlay::{
OverlayAlign, OverlayAnchor, OverlayPlacement, OverlaySide, OverlaySlot, place,
};
pub use frust_core::{OutsideTap, OverlayBand, OverlayInput, OverlayKey};
pub const PRESSED_OPACITY: f32 = 0.10;
pub type ErasedCallback = Box<dyn FnMut(&mut EventCtx)>;
pub type ErasedArgCallback<A> = Box<dyn FnMut(&mut EventCtx, A)>;
pub type TypedArgCallback<State, A> = std::rc::Rc<dyn Fn(&mut State, A)>;
pub fn erase_callback<State: 'static>(callback: &Rc<dyn Fn(&mut State)>) -> ErasedCallback {
let callback = callback.clone();
Box::new(move |ctx: &mut EventCtx| {
let state = ctx.state_mut::<State>();
callback(state);
})
}
pub fn erase_callback_arg<State: 'static, A: 'static>(
callback: &TypedArgCallback<State, A>,
) -> ErasedArgCallback<A> {
let callback = callback.clone();
Box::new(move |ctx: &mut EventCtx, arg: A| {
let state = ctx.state_mut::<State>();
callback(state, arg);
})
}
pub fn build_child<State: 'static>(view: &AnyView<State>, ctx: &mut BuildCtx<'_>) -> ChildPod {
let element: Box<dyn Widget> = ctx.with_focus_link(false, |ctx| view.build(ctx));
ChildPod::new(Box::new(element))
}
pub fn rebuild_child<State: 'static>(
prev: &AnyView<State>,
next: &AnyView<State>,
pod: &mut ChildPod,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let link_focused = pod.is_focused();
let (flags, swapped) = rebuild_child_tracked(prev, next, pod, ctx);
if swapped {
if pod.is_active() {
pod.set_active(false);
}
if link_focused {
pod.set_focused(false);
}
mark_orphan_if_live(ctx, link_focused);
}
flags
}
fn rebuild_child_tracked<State: 'static>(
prev: &AnyView<State>,
next: &AnyView<State>,
pod: &mut ChildPod,
ctx: &mut BuildCtx<'_>,
) -> (ChangeFlags, bool) {
let link_focused = pod.is_focused();
let element = pod
.widget_mut()
.downcast_mut::<Box<dyn Widget>>()
.expect("layout-container child element is a boxed AnyView widget");
let before = {
let any: &dyn Any = &**element;
any.type_id()
};
let flags = ctx.with_focus_link(link_focused, |ctx| next.rebuild(prev, element, ctx));
let after = {
let any: &dyn Any = &**element;
any.type_id()
};
(flags, before != after)
}
pub fn teardown_child<State: 'static>(
view: &AnyView<State>,
pod: &mut ChildPod,
ctx: &mut BuildCtx<'_>,
) {
if pod.is_active() {
cancel_pod(pod);
pod.set_active(false);
}
let link_focused = pod.is_focused();
if link_focused {
pod.set_focused(false);
}
mark_orphan_if_live(ctx, link_focused);
if let Some(element) = pod.widget_mut().downcast_mut::<Box<dyn Widget>>() {
ctx.with_focus_link(link_focused, |ctx| view.teardown(element, ctx));
}
}
fn mark_orphan_if_live(ctx: &BuildCtx<'_>, link_focused: bool) {
if link_focused && ctx.has_focus() {
frust_core::mark_focus_orphaned();
}
}
pub(crate) fn cancel_pod(pod: &mut ChildPod) {
let mut dummy_state = ();
let mut ctx = EventCtx::new(&mut dummy_state, pod.origin(), pod.size());
let cancel = InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Cancel,
position: Point::ZERO,
button: PointerButton::Primary,
});
pod.event_child(&mut ctx, &cancel);
}
fn cancel_active_children(pods: &mut [ChildPod], ctx: &BuildCtx<'_>) {
for pod in pods.iter_mut() {
if pod.is_active() {
cancel_pod(pod);
pod.set_active(false);
}
let link_focused = pod.is_focused();
if link_focused {
pod.set_focused(false);
}
mark_orphan_if_live(ctx, link_focused);
}
}
pub fn rebuild_children<State: 'static, C>(
prev: &[C],
next: &[C],
pods: &mut Vec<ChildPod>,
ctx: &mut BuildCtx<'_>,
view_of: impl Fn(&C) -> &AnyView<State>,
key_of: impl Fn(&C) -> Option<ChildKey>,
) -> ChangeFlags {
let any_keyed = prev.iter().chain(next.iter()).any(|c| key_of(c).is_some());
if !any_keyed {
return rebuild_children_positional(prev, next, pods, ctx, view_of);
}
let all_keyed = prev.iter().chain(next.iter()).all(|c| key_of(c).is_some());
if !all_keyed {
debug_assert!(
false,
"keyed child list mixes keyed and unkeyed children; \
falling back to positional reconciliation"
);
return rebuild_children_positional(prev, next, pods, ctx, view_of);
}
rebuild_children_keyed(prev, next, pods, ctx, view_of, key_of)
}
fn rebuild_children_positional<State: 'static, C>(
prev: &[C],
next: &[C],
pods: &mut Vec<ChildPod>,
ctx: &mut BuildCtx<'_>,
view_of: impl Fn(&C) -> &AnyView<State>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
let common = prev.len().min(next.len());
let mut first_swap: Option<usize> = None;
for i in 0..common {
let (child_flags, child_swapped) =
rebuild_child_tracked(view_of(&prev[i]), view_of(&next[i]), &mut pods[i], ctx);
flags |= child_flags;
if child_swapped {
pods[i].set_active(false);
if first_swap.is_none() {
first_swap = Some(i);
}
}
}
if next.len() > prev.len() {
for child in &next[prev.len()..] {
pods.push(build_child(view_of(child), ctx));
}
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
} else if next.len() < prev.len() {
for (offset, child) in prev[next.len()..].iter().enumerate() {
teardown_child(view_of(child), &mut pods[next.len() + offset], ctx);
}
pods.truncate(next.len());
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
if prev.len() != next.len() || first_swap.is_some() {
let k = first_swap.unwrap_or(common);
cancel_active_children(&mut pods[k..], ctx);
}
flags
}
fn rebuild_children_keyed<State: 'static, C>(
prev: &[C],
next: &[C],
pods: &mut Vec<ChildPod>,
ctx: &mut BuildCtx<'_>,
view_of: impl Fn(&C) -> &AnyView<State>,
key_of: impl Fn(&C) -> Option<ChildKey>,
) -> ChangeFlags {
let mut old_by_key: HashMap<ChildKey, usize> = HashMap::with_capacity(prev.len());
let mut duplicate = false;
for (i, child) in prev.iter().enumerate() {
let key = key_of(child).expect("all-keyed list checked by caller");
if old_by_key.insert(key, i).is_some() {
duplicate = true;
}
}
let mut seen_new: HashSet<ChildKey> = HashSet::with_capacity(next.len());
for child in next {
let key = key_of(child).expect("all-keyed list checked by caller");
if !seen_new.insert(key) {
duplicate = true;
}
}
if duplicate {
debug_assert!(
false,
"keyed child list has duplicate keys; falling back to positional reconciliation"
);
return rebuild_children_positional(prev, next, pods, ctx, view_of);
}
let mut old_pods: Vec<Option<ChildPod>> = pods.drain(..).map(Some).collect();
let mut new_pods: Vec<ChildPod> = Vec::with_capacity(next.len());
let mut flags = ChangeFlags::NONE;
for child in next {
let key = key_of(child).expect("all-keyed list checked by caller");
if let Some(&old_index) = old_by_key.get(&key) {
let mut pod = old_pods[old_index]
.take()
.expect("each old key matches at most one new child (no duplicates)");
let (child_flags, child_swapped) =
rebuild_child_tracked(view_of(&prev[old_index]), view_of(child), &mut pod, ctx);
flags |= child_flags;
if child_swapped {
pod.set_active(false);
let link_focused = pod.is_focused();
if link_focused {
pod.set_focused(false);
}
mark_orphan_if_live(ctx, link_focused);
}
new_pods.push(pod);
} else {
new_pods.push(build_child(view_of(child), ctx));
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
}
for (old_index, slot) in old_pods.iter_mut().enumerate() {
if let Some(mut pod) = slot.take() {
teardown_child(view_of(&prev[old_index]), &mut pod, ctx);
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
}
*pods = new_pods;
flags
}
pub trait VisitPods {
fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod));
}
impl VisitPods for ChildPod {
fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
visitor(self);
}
}
impl<T: VisitPods> VisitPods for Option<T> {
fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
if let Some(inner) = self {
inner.visit_pods(visitor);
}
}
}
impl<T: VisitPods> VisitPods for [T] {
fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
for item in self {
item.visit_pods(visitor);
}
}
}
impl<T: VisitPods> VisitPods for Vec<T> {
fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
self.as_slice().visit_pods(visitor);
}
}
#[macro_export]
macro_rules! visit_children {
($($field:ident),* $(,)?) => {
fn visit_children(&self, visitor: &mut dyn FnMut(&$crate::authoring::ChildPod)) {
$( $crate::authoring::VisitPods::visit_pods(&self.$field, visitor); )*
}
};
}
pub use crate::visit_children;
pub use frust_core::ChildPod;
pub(crate) fn releases_capture(event: &InputEvent) -> bool {
matches!(
event,
InputEvent::Pointer(p)
if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
)
}
pub(crate) fn presses(p: &PointerEvent) -> bool {
p.button == PointerButton::Primary
}
fn is_pointer_down(event: &InputEvent) -> bool {
matches!(
event,
InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
)
}
pub fn route_event(
children: &mut [ChildPod],
ctx: &mut EventCtx<'_>,
event: &InputEvent,
) -> EventResult {
if event.is_broadcast() {
for pod in children.iter_mut() {
pod.event_child(ctx, event);
}
return EventResult::Ignored;
}
if event.is_focus_routed() {
if let Some(pod) = children.iter_mut().find(|p| p.holds_live_focus()) {
return pod.event_child(ctx, event);
}
return EventResult::Ignored;
}
if let Some(pod) = children.iter_mut().find(|p| p.is_active()) {
return route_event_single(pod, ctx, event);
}
let position = event.position();
let mut handled = EventResult::Ignored;
let mut kept_focus: Option<usize> = None;
let n = children.len();
for i in (0..n).rev() {
if children[i].contains(position)
&& children[i].event_child(ctx, event) == EventResult::Handled
{
if children[i].holds_live_focus() {
kept_focus = Some(i);
}
handled = EventResult::Handled;
break;
}
}
if is_pointer_down(event) {
for (i, pod) in children.iter_mut().enumerate() {
if Some(i) != kept_focus && pod.is_focused() {
pod.set_focused(false);
}
}
}
handled
}
pub fn route_event_single(
pod: &mut ChildPod,
ctx: &mut EventCtx<'_>,
event: &InputEvent,
) -> EventResult {
if event.is_broadcast() {
pod.event_child(ctx, event);
return EventResult::Ignored;
}
if event.is_focus_routed() {
if pod.holds_live_focus() {
return pod.event_child(ctx, event);
}
return EventResult::Ignored;
}
if pod.is_active() {
let result = pod.event_child(ctx, event);
if releases_capture(event) {
pod.set_active(false);
}
return result;
}
let inside = pod.contains(event.position());
let result = if inside {
pod.event_child(ctx, event)
} else {
EventResult::Ignored
};
if is_pointer_down(event) && !inside && pod.is_focused() {
pod.set_focused(false);
}
result
}
#[cfg(test)]
mod authoring_surface_tests {
use frust_core::{AnyView, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent};
#[test]
#[allow(clippy::type_complexity)]
fn every_promoted_item_is_reachable_through_the_authoring_path() {
let _erased: Option<crate::authoring::ErasedCallback> = None;
let _erased_arg: Option<crate::authoring::ErasedArgCallback<f64>> = None;
let _typed: Option<crate::authoring::TypedArgCallback<(), f64>> = None;
let _erase: fn(&std::rc::Rc<dyn Fn(&mut ())>) -> crate::authoring::ErasedCallback =
crate::authoring::erase_callback::<()>;
let _erase_arg: fn(
&crate::authoring::TypedArgCallback<(), f64>,
) -> crate::authoring::ErasedArgCallback<f64> =
crate::authoring::erase_callback_arg::<(), f64>;
let _build: fn(&AnyView<()>, &mut BuildCtx<'_>) -> ChildPod =
crate::authoring::build_child::<()>;
let _rebuild: fn(
&AnyView<()>,
&AnyView<()>,
&mut ChildPod,
&mut BuildCtx<'_>,
) -> ChangeFlags = crate::authoring::rebuild_child::<()>;
let _teardown: fn(&AnyView<()>, &mut ChildPod, &mut BuildCtx<'_>) =
crate::authoring::teardown_child::<()>;
let mut counter = 0u64;
let mut build_ctx = BuildCtx::new(&mut counter);
let mut pods: Vec<ChildPod> = Vec::new();
let empty: Vec<AnyView<()>> = Vec::new();
crate::authoring::rebuild_children(
&empty,
&empty,
&mut pods,
&mut build_ctx,
|v: &AnyView<()>| v,
|_: &AnyView<()>| None::<crate::ChildKey>,
);
assert!(pods.is_empty());
let _route: fn(&mut [ChildPod], &mut EventCtx<'_>, &InputEvent) -> EventResult =
crate::authoring::route_event;
let _route_single: fn(&mut ChildPod, &mut EventCtx<'_>, &InputEvent) -> EventResult =
crate::authoring::route_event_single;
let _role: crate::authoring::ThemeTextColor = crate::authoring::ThemeTextColor::OnSurface;
let _themed_role: fn(crate::TextView, crate::authoring::ThemeTextColor) -> crate::TextView =
crate::TextView::themed_role;
let _type: crate::authoring::ThemeTextType = crate::authoring::ThemeTextType::BodyLarge;
let _themed_family: fn(
crate::TextView,
crate::authoring::ThemeTextType,
) -> crate::TextView = crate::TextView::themed_family;
let _pressed: f32 = crate::authoring::PRESSED_OPACITY;
let _place: fn(
kurbo::Rect,
kurbo::Size,
kurbo::Rect,
crate::authoring::OverlayPlacement,
) -> kurbo::Rect = crate::authoring::place;
let _side: crate::authoring::OverlaySide = crate::authoring::OverlaySide::Top;
let _align: crate::authoring::OverlayAlign = crate::authoring::OverlayAlign::Center;
let _anchor: crate::authoring::OverlayAnchor = crate::authoring::OverlayAnchor::Owner;
let slot: crate::authoring::OverlaySlot<()> = crate::authoring::OverlaySlot::new();
let _key: crate::authoring::OverlayKey = slot.key();
let _band: crate::authoring::OverlayBand = crate::authoring::OverlayBand::Floating;
let _input: crate::authoring::OverlayInput = crate::authoring::OverlayInput::Interactive;
let _tap: crate::authoring::OutsideTap =
crate::authoring::OutsideTap::Notify { consume: true };
let _visit: fn(&ChildPod, &mut dyn FnMut(&ChildPod)) =
<ChildPod as crate::authoring::VisitPods>::visit_pods;
let _visit_opt: fn(&Option<ChildPod>, &mut dyn FnMut(&ChildPod)) =
<Option<ChildPod> as crate::authoring::VisitPods>::visit_pods;
let _visit_vec: fn(&Vec<ChildPod>, &mut dyn FnMut(&ChildPod)) =
<Vec<ChildPod> as crate::authoring::VisitPods>::visit_pods;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{leaf_any, swap_leaf};
use frust_core::{BoxConstraints, BuildCtx, LayoutCtx, PaintCtx, PaintScene, any};
use kurbo::Size;
fn ctx(counter: &mut u64) -> BuildCtx<'_> {
BuildCtx::new(counter)
}
fn blurred_ctx(counter: &mut u64) -> BuildCtx<'_> {
let mut ctx = BuildCtx::new(counter);
ctx.set_has_focus(false);
ctx
}
struct NestView {
children: Vec<AnyView<()>>,
}
struct NestWidget {
children: Vec<ChildPod>,
}
impl View<()> for NestView {
type Element = NestWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> NestWidget {
NestWidget {
children: self.children.iter().map(|c| build_child(c, ctx)).collect(),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut NestWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
rebuild_children(
&prev.children,
&self.children,
&mut element.children,
ctx,
|v: &AnyView<()>| v,
|_: &AnyView<()>| None::<ChildKey>,
)
}
fn teardown(&self, element: &mut NestWidget, ctx: &mut BuildCtx<'_>) {
for (view, pod) in self.children.iter().zip(element.children.iter_mut()) {
teardown_child(view, pod, ctx);
}
}
}
impl Widget for NestWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
fn nest_in(pod: &mut ChildPod) -> &mut NestWidget {
let boxed = pod
.widget_mut()
.downcast_mut::<Box<dyn Widget>>()
.expect("pod holds a boxed AnyView widget");
(**boxed)
.downcast_mut::<NestWidget>()
.expect("the boxed widget is the NestWidget")
}
#[test]
fn rebuild_child_clears_active_on_type_swap() {
let mut counter = 0u64;
let prev: AnyView<()> = leaf_any(10.0, 10.0);
let mut pod = build_child(&prev, &mut ctx(&mut counter));
pod.set_active(true);
let next: AnyView<()> = swap_leaf().into_any();
rebuild_child::<()>(&prev, &next, &mut pod, &mut ctx(&mut counter));
assert!(
!pod.is_active(),
"a type-swapped child's stale capture must be dropped"
);
}
#[test]
fn rebuild_child_preserves_active_on_same_type_rebuild() {
let mut counter = 0u64;
let prev: AnyView<()> = leaf_any(10.0, 10.0);
let mut pod = build_child(&prev, &mut ctx(&mut counter));
pod.set_active(true);
let next: AnyView<()> = leaf_any(20.0, 20.0);
rebuild_child::<()>(&prev, &next, &mut pod, &mut ctx(&mut counter));
assert!(
pod.is_active(),
"a content-only rebuild must not clear an in-flight capture"
);
}
#[test]
fn a_wrapper_type_swap_marks_only_on_a_live_chain() {
fn swap_wrapped_child(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let prev: AnyView<()> = leaf_any(10.0, 10.0);
let mut pod = build_child(&prev, &mut ctx(&mut counter));
pod.set_focused(true);
let next: AnyView<()> = swap_leaf().into_any();
rebuild_child::<()>(&prev, &next, &mut pod, &mut build(&mut counter));
assert!(
!pod.is_focused(),
"a swapped-in widget must not inherit the old focus link"
);
frust_core::take_focus_orphaned()
}
assert!(
swap_wrapped_child(&mut ctx),
"a live focus path dying inside a wrapper's type swap releases the session"
);
assert!(
!swap_wrapped_child(&mut blurred_ctx),
"the same swap over a stale flag below a cleared link marks nothing"
);
}
#[test]
fn a_wrapper_content_only_rebuild_keeps_focus_and_marks_nothing() {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let prev: AnyView<()> = leaf_any(10.0, 10.0);
let mut pod = build_child(&prev, &mut ctx(&mut counter));
pod.set_focused(true);
let next: AnyView<()> = leaf_any(20.0, 20.0);
rebuild_child::<()>(&prev, &next, &mut pod, &mut ctx(&mut counter));
assert!(
pod.is_focused(),
"a content-only rebuild must not clear the recorded focus path"
);
assert!(
!frust_core::take_focus_orphaned(),
"an unchanged wrapper child must not orphan the focus session"
);
}
#[test]
fn a_truncated_focused_child_marks_the_focus_orphan() {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
let mut pods: Vec<ChildPod> = prev
.iter()
.map(|v| build_child(v, &mut ctx(&mut counter)))
.collect();
pods[1].set_focused(true);
assert!(
!frust_core::take_focus_orphaned(),
"building a list orphans nothing"
);
let next: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0)];
rebuild_children(
&prev,
&next,
&mut pods,
&mut ctx(&mut counter),
|v: &AnyView<()>| v,
|_: &AnyView<()>| None::<ChildKey>,
);
assert_eq!(pods.len(), 1);
assert!(
frust_core::take_focus_orphaned(),
"the focused child's unmount must be reported to the render root"
);
}
#[test]
fn a_truncated_stale_focused_child_under_a_blurred_link_marks_nothing() {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
let mut pods: Vec<ChildPod> = prev
.iter()
.map(|v| build_child(v, &mut ctx(&mut counter)))
.collect();
pods[1].set_focused(true);
let next: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0)];
rebuild_children(
&prev,
&next,
&mut pods,
&mut blurred_ctx(&mut counter),
|v: &AnyView<()>| v,
|_: &AnyView<()>| None::<ChildKey>,
);
assert_eq!(pods.len(), 1);
assert!(
!frust_core::take_focus_orphaned(),
"a stale flag below a cleared link owns no session — releasing here \
would kill the focus of whatever field really is focused"
);
}
#[test]
fn a_cleared_tail_marks_only_on_a_live_chain() {
fn shrink_tail(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
let mut pods: Vec<ChildPod> = prev
.iter()
.map(|v| build_child(v, &mut ctx(&mut counter)))
.collect();
pods[1].set_focused(true);
let next: Vec<AnyView<()>> = vec![swap_leaf().into_any(), leaf_any(10.0, 10.0)];
rebuild_children(
&prev,
&next,
&mut pods,
&mut build(&mut counter),
|v: &AnyView<()>| v,
|_: &AnyView<()>| None::<ChildKey>,
);
assert!(!pods[1].is_focused(), "the cleared tail drops the flag");
frust_core::take_focus_orphaned()
}
assert!(
shrink_tail(&mut ctx),
"a live focus link cleared by the tail sweep releases the session"
);
assert!(
!shrink_tail(&mut blurred_ctx),
"the same sweep over a stale flag below a cleared link marks nothing"
);
}
#[test]
fn a_keyed_type_swap_marks_only_on_a_live_chain() {
fn swap_keyed(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let key = ChildKey::new(1u32);
let prev: Vec<(AnyView<()>, ChildKey)> = vec![(leaf_any(10.0, 10.0), key)];
let mut pods: Vec<ChildPod> = prev
.iter()
.map(|c| build_child(&c.0, &mut ctx(&mut counter)))
.collect();
pods[0].set_focused(true);
let next: Vec<(AnyView<()>, ChildKey)> = vec![(swap_leaf().into_any(), key)];
rebuild_children(
&prev,
&next,
&mut pods,
&mut build(&mut counter),
|c: &(AnyView<()>, ChildKey)| &c.0,
|c: &(AnyView<()>, ChildKey)| Some(c.1),
);
assert!(!pods[0].is_focused(), "a swapped-away pod drops the flag");
frust_core::take_focus_orphaned()
}
assert!(
swap_keyed(&mut ctx),
"a live focus path dying inside a keyed swap releases the session"
);
assert!(
!swap_keyed(&mut blurred_ctx),
"a stale flag dying inside a keyed swap releases nothing"
);
}
#[test]
fn the_focus_chain_composes_through_nested_containers() {
fn drop_inner_leaf(outer_link_focused: bool) -> bool {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let prev: Vec<AnyView<()>> = vec![any(NestView {
children: vec![leaf_any(10.0, 10.0)],
})];
let mut pods: Vec<ChildPod> = prev
.iter()
.map(|v| build_child(v, &mut ctx(&mut counter)))
.collect();
nest_in(&mut pods[0]).children[0].set_focused(true);
pods[0].set_focused(outer_link_focused);
let next: Vec<AnyView<()>> = vec![any(NestView { children: vec![] })];
rebuild_children(
&prev,
&next,
&mut pods,
&mut ctx(&mut counter),
|v: &AnyView<()>| v,
|_: &AnyView<()>| None::<ChildKey>,
);
assert!(
nest_in(&mut pods[0]).children.is_empty(),
"the inner child was torn down"
);
frust_core::take_focus_orphaned()
}
assert!(
drop_inner_leaf(true),
"an unbroken chain root→container→leaf is a live session; its unmount releases it"
);
assert!(
!drop_inner_leaf(false),
"a cleared link at the nearest common ancestor makes every flag below it inert"
);
}
#[test]
fn a_content_only_rebuild_marks_no_focus_orphan() {
let _ = frust_core::take_focus_orphaned();
let mut counter = 0u64;
let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
let mut pods: Vec<ChildPod> = prev
.iter()
.map(|v| build_child(v, &mut ctx(&mut counter)))
.collect();
pods[1].set_focused(true);
let next: Vec<AnyView<()>> = vec![leaf_any(20.0, 20.0), leaf_any(20.0, 20.0)];
rebuild_children(
&prev,
&next,
&mut pods,
&mut ctx(&mut counter),
|v: &AnyView<()>| v,
|_: &AnyView<()>| None::<ChildKey>,
);
assert!(pods[1].is_focused(), "the focus path survives intact");
assert!(
!frust_core::take_focus_orphaned(),
"an unchanged child must not orphan the focus session"
);
}
}
#[cfg(test)]
mod focus_tests {
use super::*;
use frust_core::{
BoxConstraints, EditingState, EventCtx, ImeEvent, ImeState, Key, KeyEvent, LayoutCtx,
Modifiers, PaintCtx, PaintScene,
};
use kurbo::{Point, Rect, Size};
struct KeyLeaf {
id: u32,
takes_focus: bool,
publish_ime: bool,
}
impl Widget for KeyLeaf {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) => match p.phase {
PointerPhase::Down => {
if self.takes_focus {
ctx.request_focus();
}
ctx.state_mut::<Vec<u32>>().push(self.id + 100);
EventResult::Handled
}
PointerPhase::Move => {
ctx.state_mut::<Vec<u32>>().push(self.id + 200);
EventResult::Handled
}
_ => EventResult::Handled,
},
InputEvent::Key(_) | InputEvent::Ime(_) => {
ctx.state_mut::<Vec<u32>>().push(self.id);
if self.publish_ime {
ctx.publish_ime_state(ImeState {
active: true,
editing: EditingState {
text: format!("leaf{}", self.id),
selection_base: 1,
selection_extent: 1,
composing_base: -1,
composing_extent: -1,
},
caret: Some(Rect::new(0.0, 0.0, 1.0, 10.0)),
content_type: Default::default(),
suppress_soft_keyboard: false,
});
}
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
struct Nest {
children: Vec<ChildPod>,
}
impl Widget for Nest {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let w = bc.max().width;
for (i, pod) in self.children.iter_mut().enumerate() {
pod.set_origin(Point::new(0.0, i as f64 * 10.0));
pod.layout_child(ctx, &BoxConstraints::tight(Size::new(w, 10.0)));
}
Size::new(w, self.children.len() as f64 * 10.0)
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
route_event(&mut self.children, ctx, event)
}
}
fn leaf_pod(id: u32, takes_focus: bool, publish_ime: bool) -> ChildPod {
ChildPod::new(Box::new(KeyLeaf {
id,
takes_focus,
publish_ime,
}))
}
fn lay_out_vertically(pods: &mut [ChildPod]) {
let mut lctx = LayoutCtx::new();
for (i, pod) in pods.iter_mut().enumerate() {
pod.set_origin(Point::new(0.0, i as f64 * 10.0));
pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 10.0)));
}
}
fn down(y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(5.0, y),
button: PointerButton::Primary,
})
}
fn mv(y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Move,
position: Point::new(5.0, y),
button: PointerButton::Primary,
})
}
fn key() -> InputEvent {
InputEvent::Key(KeyEvent {
key: Key::Character("a".to_string()),
modifiers: Modifiers::default(),
repeat: false,
})
}
fn ime_apply() -> InputEvent {
InputEvent::Ime(ImeEvent::ApplyEditingState(EditingState {
text: "sync".to_string(),
selection_base: 4,
selection_extent: 4,
composing_base: -1,
composing_extent: -1,
}))
}
#[allow(clippy::ptr_arg)]
fn route(children: &mut [ChildPod], state: &mut Vec<u32>, event: &InputEvent) -> EventResult {
let mut ctx = EventCtx::new(state, Point::ZERO, Size::new(100.0, 100.0));
route_event(children, &mut ctx, event)
}
#[test]
fn key_routes_only_to_focused_child() {
let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
lay_out_vertically(&mut children);
let mut state = Vec::new();
route(&mut children, &mut state, &down(15.0));
assert!(children[1].is_focused());
assert!(!children[0].is_focused());
state.clear();
route(&mut children, &mut state, &key());
assert_eq!(state, vec![2]);
}
#[test]
fn key_reaches_focused_leaf_through_nested_container() {
let inner = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
let mut nest = Nest { children: inner };
{
let mut lctx = LayoutCtx::new();
nest.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 20.0)));
}
let mut outer = vec![ChildPod::new(Box::new(nest))];
outer[0].set_origin(Point::ZERO);
{
let mut lctx = LayoutCtx::new();
outer[0].layout_child(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 20.0)));
}
let mut state = Vec::new();
route(&mut outer, &mut state, &down(15.0));
assert!(outer[0].is_focused(), "the nest records the focus path");
state.clear();
route(&mut outer, &mut state, &key());
assert_eq!(state, vec![2]);
}
#[test]
fn blur_on_outside_tap_clears_focus_chain() {
let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, false, false)];
lay_out_vertically(&mut children);
let mut state = Vec::new();
route(&mut children, &mut state, &down(5.0)); assert!(children[0].is_focused());
route(&mut children, &mut state, &down(15.0));
assert!(
!children[0].is_focused(),
"outside tap blurs the focused child"
);
assert!(!children[1].is_focused());
state.clear();
let result = route(&mut children, &mut state, &key());
assert_eq!(result, EventResult::Ignored);
assert!(state.is_empty());
}
#[test]
fn tap_inside_focused_widget_keeps_focus() {
let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, false, false)];
lay_out_vertically(&mut children);
let mut state = Vec::new();
route(&mut children, &mut state, &down(5.0)); route(&mut children, &mut state, &down(5.0)); assert!(
children[0].is_focused(),
"a tap inside the focused widget keeps focus"
);
state.clear();
route(&mut children, &mut state, &key());
assert_eq!(state, vec![1]);
}
#[test]
fn second_focus_request_moves_focus() {
let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
lay_out_vertically(&mut children);
let mut state = Vec::new();
route(&mut children, &mut state, &down(5.0)); assert!(children[0].is_focused());
route(&mut children, &mut state, &down(15.0)); assert!(children[1].is_focused());
assert!(!children[0].is_focused(), "focus moved off child 1");
state.clear();
route(&mut children, &mut state, &key());
assert_eq!(
state,
vec![2],
"key now reaches only the newly focused child"
);
}
#[test]
fn ime_apply_routes_to_focused_child() {
let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
lay_out_vertically(&mut children);
let mut state = Vec::new();
route(&mut children, &mut state, &down(15.0)); state.clear();
route(&mut children, &mut state, &ime_apply());
assert_eq!(state, vec![2]);
}
#[test]
fn capture_and_focus_paths_are_independent() {
let mut children = vec![leaf_pod(1, false, false), leaf_pod(2, false, false)];
lay_out_vertically(&mut children);
children[0].set_active(true);
children[1].set_focused(true);
let mut state = Vec::new();
route(&mut children, &mut state, &mv(999.0));
assert_eq!(state, vec![201]);
state.clear();
route(&mut children, &mut state, &key());
assert_eq!(state, vec![2]);
assert!(children[0].is_active());
assert!(children[1].is_focused());
}
}