use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec::Vec;
use core::cell::RefCell;
use crate::WidgetNode;
use crate::event::{Event, Key};
use crate::layout::LayoutState;
use crate::renderer::{ClipRenderer, Renderer};
use crate::widget::{Rect, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GestureDir {
Up,
Down,
Left,
Right,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ObjectEvent {
Pressed {
x: i32,
y: i32,
},
Released {
x: i32,
y: i32,
},
Clicked {
x: i32,
y: i32,
},
DoubleClicked {
x: i32,
y: i32,
},
LongPressed {
x: i32,
y: i32,
},
LongPressedRepeat {
x: i32,
y: i32,
},
Key(
Key,
),
Rotary {
diff: i32,
},
Focused,
Defocused,
Gesture(
GestureDir,
),
Attached,
Detached,
ChildChanged,
ScrollBegin,
Scroll {
scroll_x: i32,
scroll_y: i32,
},
ScrollEnd,
ScrollThrow {
vel_x: i32,
vel_y: i32,
},
SizeChanged,
LayoutChanged,
}
#[derive(Debug, Clone, Copy)]
pub struct EventContext {
pub target_tag: Option<&'static str>,
pub current_tag: Option<&'static str>,
}
pub type ObjectHandler = Box<dyn FnMut(&ObjectEvent, EventContext) -> bool>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectFlags(u32);
impl ObjectFlags {
pub const EMPTY: Self = Self(0);
pub const HIDDEN: Self = Self(1 << 0);
pub const DISABLED: Self = Self(1 << 1);
pub const CLICKABLE: Self = Self(1 << 2);
pub const FOCUSABLE: Self = Self(1 << 3);
pub const SCROLLABLE: Self = Self(1 << 4);
pub const EVENT_BUBBLE: Self = Self(1 << 5);
pub const fn from_bits_truncate(bits: u32) -> Self {
Self(bits & Self::all_bits())
}
pub const fn bits(self) -> u32 {
self.0
}
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
pub fn remove(&mut self, other: Self) {
self.0 &= !other.0;
}
pub fn set(&mut self, other: Self, enabled: bool) {
if enabled {
self.insert(other);
} else {
self.remove(other);
}
}
const fn all_bits() -> u32 {
Self::HIDDEN.0
| Self::DISABLED.0
| Self::CLICKABLE.0
| Self::FOCUSABLE.0
| Self::SCROLLABLE.0
| Self::EVENT_BUBBLE.0
}
}
impl Default for ObjectFlags {
fn default() -> Self {
Self::EMPTY
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectStates(u32);
impl ObjectStates {
pub const DEFAULT: Self = Self(0);
pub const DISABLED: Self = Self(1 << 0);
pub const FOCUSED: Self = Self(1 << 1);
pub const PRESSED: Self = Self(1 << 2);
pub const CHECKED: Self = Self(1 << 3);
pub const EDITED: Self = Self(1 << 4);
pub const fn from_bits_truncate(bits: u32) -> Self {
Self(bits & Self::all_bits())
}
pub const fn bits(self) -> u32 {
self.0
}
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
pub fn remove(&mut self, other: Self) {
self.0 &= !other.0;
}
pub fn set(&mut self, other: Self, enabled: bool) {
if enabled {
self.insert(other);
} else {
self.remove(other);
}
}
const fn all_bits() -> u32 {
Self::DISABLED.0 | Self::FOCUSED.0 | Self::PRESSED.0 | Self::CHECKED.0 | Self::EDITED.0
}
}
impl Default for ObjectStates {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ObjectMeta {
flags: ObjectFlags,
states: ObjectStates,
detached: bool,
}
impl ObjectMeta {
pub const fn new() -> Self {
Self {
flags: ObjectFlags::EMPTY,
states: ObjectStates::DEFAULT,
detached: false,
}
}
pub const fn flags(&self) -> ObjectFlags {
self.flags
}
pub fn flags_mut(&mut self) -> &mut ObjectFlags {
&mut self.flags
}
pub const fn states(&self) -> ObjectStates {
self.states
}
pub fn states_mut(&mut self) -> &mut ObjectStates {
&mut self.states
}
pub const fn is_detached(&self) -> bool {
self.detached
}
fn set_detached(&mut self, detached: bool) {
self.detached = detached;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DispatchInput {
Pointer {
x: i32,
y: i32,
event: Event,
},
PointerObject {
x: i32,
y: i32,
event: ObjectEvent,
},
Focused {
event: ObjectEvent,
},
Container {
path: Vec<usize>,
event: ObjectEvent,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Disposition {
Consumed,
Unconsumed,
NoTarget,
}
pub struct ObjectNode {
widget: Rc<RefCell<dyn Widget>>,
children: Vec<ObjectNode>,
tag: Option<&'static str>,
meta: ObjectMeta,
trickle_handlers: Vec<ObjectHandler>,
target_handlers: Vec<ObjectHandler>,
bubble_handlers: Vec<ObjectHandler>,
pub(crate) scroll: Option<Box<crate::scroll::ScrollState>>,
pub(crate) anims: Option<Box<crate::object_anim::NodeAnimSet>>,
pub(crate) style: Option<Box<crate::style_cascade::StyleState>>,
pub(crate) layout: Option<Box<LayoutState>>,
}
impl ObjectNode {
pub fn new(widget: Rc<RefCell<dyn Widget>>) -> Self {
Self {
widget,
children: Vec::new(),
tag: None,
meta: ObjectMeta::new(),
trickle_handlers: Vec::new(),
target_handlers: Vec::new(),
bubble_handlers: Vec::new(),
scroll: None,
anims: None,
style: None,
layout: None,
}
}
pub fn adopt(node: WidgetNode) -> Self {
let mut object = Self::new(node.widget);
object.tag = node.tag;
object.children = node.children.into_iter().map(Self::adopt).collect();
object
}
pub fn with_tag(mut self, tag: &'static str) -> Self {
self.tag = Some(tag);
self
}
pub fn widget(&self) -> &Rc<RefCell<dyn Widget>> {
&self.widget
}
pub const fn tag(&self) -> Option<&'static str> {
self.tag
}
pub const fn meta(&self) -> &ObjectMeta {
&self.meta
}
pub fn meta_mut(&mut self) -> &mut ObjectMeta {
&mut self.meta
}
pub const fn flags(&self) -> ObjectFlags {
self.meta.flags()
}
pub const fn states(&self) -> ObjectStates {
self.meta.states()
}
pub fn set_flag(&mut self, flag: ObjectFlags, enabled: bool) {
self.meta.flags_mut().set(flag, enabled);
}
pub fn set_state(&mut self, state: ObjectStates, enabled: bool) {
self.meta.states_mut().set(state, enabled);
}
pub const fn is_detached(&self) -> bool {
self.meta.is_detached()
}
pub fn children(&self) -> &[ObjectNode] {
&self.children
}
pub fn children_mut(&mut self) -> &mut Vec<ObjectNode> {
&mut self.children
}
pub fn set_scroll_state(&mut self, state: Box<crate::scroll::ScrollState>) {
self.meta.flags_mut().insert(ObjectFlags::SCROLLABLE);
self.scroll = Some(state);
}
pub fn scroll_state(&self) -> Option<&crate::scroll::ScrollState> {
self.scroll.as_deref()
}
pub fn scroll_state_mut(&mut self) -> Option<&mut crate::scroll::ScrollState> {
self.scroll.as_deref_mut()
}
pub fn add_local_style(
&mut self,
patch: crate::style_cascade::StylePatch,
selector: crate::style_cascade::Selector,
) {
let slot = self
.style
.get_or_insert_with(|| Box::new(crate::style_cascade::StyleState::new()));
crate::style_cascade::push_local(slot, patch, selector);
}
pub fn add_style(
&mut self,
patch: &'static crate::style_cascade::StylePatch,
selector: crate::style_cascade::Selector,
) {
let slot = self
.style
.get_or_insert_with(|| Box::new(crate::style_cascade::StyleState::new()));
crate::style_cascade::push_added(slot, patch, selector);
}
pub fn add_theme_style(
&mut self,
patch: crate::style_cascade::StylePatch,
selector: crate::style_cascade::Selector,
) {
let slot = self
.style
.get_or_insert_with(|| Box::new(crate::style_cascade::StyleState::new()));
crate::style_cascade::push_theme(slot, patch, selector);
}
pub fn clear_theme_styles(&mut self) -> usize {
match self.style.as_deref_mut() {
Some(slot) => crate::style_cascade::clear_theme(slot),
None => 0,
}
}
pub fn remove_local_styles(
&mut self,
part: crate::style_cascade::Part,
states: ObjectStates,
) -> usize {
match self.style.as_deref_mut() {
Some(slot) => crate::style_cascade::remove_local_matching(slot, part, states),
None => 0,
}
}
pub fn remove_all_local_styles_by_part(&mut self, part: crate::style_cascade::Part) -> usize {
match self.style.as_deref_mut() {
Some(slot) => crate::style_cascade::remove_all_local_by_part(slot, part),
None => 0,
}
}
pub fn remove_all_local_styles(&mut self) -> usize {
match self.style.as_deref_mut() {
Some(slot) => crate::style_cascade::remove_all_local(slot),
None => 0,
}
}
pub fn effective_bounds(&self) -> Rect {
if let Some(ls) = self.layout.as_deref()
&& let Some(computed) = ls.computed
{
return computed;
}
self.widget.borrow().bounds()
}
pub fn set_layout_flex(&mut self, config: crate::layout::FlexConfig) {
let slot = self
.layout
.get_or_insert_with(|| Box::new(LayoutState::default()));
slot.role = crate::layout::LayoutRole::Container(crate::layout::EngineConfig::Flex(config));
slot.layout_dirty = true;
}
pub fn set_layout_grid(&mut self, config: crate::layout::GridConfig) {
let slot = self
.layout
.get_or_insert_with(|| Box::new(LayoutState::default()));
slot.role = crate::layout::LayoutRole::Container(crate::layout::EngineConfig::Grid(config));
slot.layout_dirty = true;
}
pub fn set_item_hints(&mut self, hints: crate::layout::ItemHints) {
let slot = self
.layout
.get_or_insert_with(|| Box::new(LayoutState::default()));
slot.role = crate::layout::LayoutRole::Item(hints);
slot.layout_dirty = true;
}
pub fn mark_layout_dirty(&mut self) {
if let Some(ls) = self.layout.as_deref_mut() {
ls.layout_dirty = true;
}
}
pub fn add_trickle_handler<F>(&mut self, handler: F)
where
F: FnMut(&ObjectEvent, EventContext) -> bool + 'static,
{
self.trickle_handlers.push(Box::new(handler));
}
pub fn add_target_handler<F>(&mut self, handler: F)
where
F: FnMut(&ObjectEvent, EventContext) -> bool + 'static,
{
self.target_handlers.push(Box::new(handler));
}
pub fn add_bubble_handler<F>(&mut self, handler: F)
where
F: FnMut(&ObjectEvent, EventContext) -> bool + 'static,
{
self.bubble_handlers.push(Box::new(handler));
}
pub fn invoke_handlers_for(&mut self, event: &ObjectEvent) -> bool {
let ctx = EventContext {
target_tag: self.tag,
current_tag: self.tag,
};
for handler in &mut self.target_handlers {
if handler(event, ctx) {
return true;
}
}
false
}
pub fn append_child(&mut self, mut child: ObjectNode) -> usize {
child.set_detached_recursive(false);
self.children.push(child);
let idx = self.children.len() - 1;
self.children[idx].invoke_handlers_for(&ObjectEvent::Attached);
self.invoke_handlers_for(&ObjectEvent::ChildChanged);
idx
}
pub fn insert_child(&mut self, index: usize, mut child: ObjectNode) -> bool {
if index > self.children.len() {
return false;
}
child.set_detached_recursive(false);
self.children.insert(index, child);
self.children[index].invoke_handlers_for(&ObjectEvent::Attached);
self.invoke_handlers_for(&ObjectEvent::ChildChanged);
true
}
pub fn detach_child(&mut self, index: usize) -> Option<ObjectNode> {
if index >= self.children.len() {
return None;
}
let mut child = self.children.remove(index);
child.set_detached_recursive(true);
child.invoke_handlers_for(&ObjectEvent::Detached);
self.invoke_handlers_for(&ObjectEvent::ChildChanged);
Some(child)
}
pub fn raise_child(&mut self, index: usize) -> bool {
if index >= self.children.len() {
return false;
}
let child = self.children.remove(index);
self.children.push(child);
self.invoke_handlers_for(&ObjectEvent::ChildChanged);
true
}
pub fn lower_child(&mut self, index: usize) -> bool {
if index >= self.children.len() {
return false;
}
let child = self.children.remove(index);
self.children.insert(0, child);
self.invoke_handlers_for(&ObjectEvent::ChildChanged);
true
}
pub fn move_child_before(&mut self, from: usize, before: usize) -> bool {
let len = self.children.len();
if from >= len || before >= len {
return false;
}
if from == before {
return true;
}
let child = self.children.remove(from);
let target = if from < before { before - 1 } else { before };
self.children.insert(target, child);
self.invoke_handlers_for(&ObjectEvent::ChildChanged);
true
}
pub fn move_child_after(&mut self, from: usize, after: usize) -> bool {
let len = self.children.len();
if from >= len || after >= len {
return false;
}
if from == after {
return true;
}
let child = self.children.remove(from);
let mut target = after + 1;
if from < target {
target -= 1;
}
if target > self.children.len() {
target = self.children.len();
}
self.children.insert(target, child);
self.invoke_handlers_for(&ObjectEvent::ChildChanged);
true
}
pub fn draw(&self, renderer: &mut dyn Renderer) {
if self.is_hidden_or_detached() {
return;
}
let intrinsic = self.widget.borrow().bounds();
let effective = self.effective_bounds();
if intrinsic.x != effective.x || intrinsic.y != effective.y {
let dx = effective.x - intrinsic.x;
let dy = effective.y - intrinsic.y;
let mut clip_r = ClipRenderer::with_offset(renderer, effective, dx, dy);
self.widget.borrow().draw(&mut clip_r);
} else {
self.widget.borrow().draw(renderer);
}
for child in &self.children {
child.draw(renderer);
}
}
pub fn hit_test(&self, x: i32, y: i32) -> Option<&ObjectNode> {
if self.is_hidden_or_detached() {
return None;
}
for child in self.children.iter().rev() {
if let Some(hit) = child.hit_test(x, y) {
return Some(hit);
}
}
if self.is_targetable_at(x, y) {
Some(self)
} else {
None
}
}
pub fn visible_subtree_extent(&self) -> Option<Rect> {
if self.is_hidden_or_detached() {
return None;
}
let bounds = self.effective_bounds();
let mut extent = if bounds.width > 0 && bounds.height > 0 {
Some(bounds)
} else {
None
};
for child in &self.children {
if let Some(child_extent) = child.visible_subtree_extent() {
extent = Some(match extent {
Some(current) => current.union(child_extent),
None => child_extent,
});
}
}
extent
}
fn set_detached_recursive(&mut self, detached: bool) {
self.meta.set_detached(detached);
for child in &mut self.children {
child.set_detached_recursive(detached);
}
}
fn is_hidden_or_detached(&self) -> bool {
self.is_detached() || self.flags().contains(ObjectFlags::HIDDEN)
}
pub(crate) fn is_hidden_or_detached_pub(&self) -> bool {
self.is_hidden_or_detached()
}
fn is_targetable_at(&self, x: i32, y: i32) -> bool {
let flags = self.flags();
flags.contains(ObjectFlags::CLICKABLE)
&& !flags.contains(ObjectFlags::DISABLED)
&& rect_contains(self.effective_bounds(), x, y)
}
}
pub fn dispatch_object_event(root: &mut ObjectNode, input: DispatchInput) -> Disposition {
let (target_path, object_event, raw_stream_event) = match resolve_target(root, &input) {
None => return Disposition::NoTarget,
Some(t) => t,
};
let target_tag = node_at_path(root, &target_path).tag();
for depth in 0..target_path.len() {
let node_path = &target_path[..depth];
let ctx = EventContext {
target_tag,
current_tag: node_at_path(root, node_path).tag(),
};
let consumed = invoke_trickle(root, node_path, &object_event, ctx);
if consumed {
return Disposition::Consumed;
}
}
{
let ctx = EventContext {
target_tag,
current_tag: target_tag,
};
let target_node = node_at_path_mut(root, &target_path);
for handler in &mut target_node.target_handlers {
if handler(&object_event, ctx) {
return Disposition::Consumed;
}
}
if let Some(ref ev) = raw_stream_event
&& target_node.widget.borrow_mut().handle_event(ev)
{
return Disposition::Consumed;
}
}
let mut depth = target_path.len();
loop {
let node_path = &target_path[..depth];
let ctx = EventContext {
target_tag,
current_tag: node_at_path(root, node_path).tag(),
};
if invoke_bubble(root, node_path, &object_event, ctx) {
return Disposition::Consumed;
}
if depth == 0 {
break;
}
if !node_at_path(root, node_path)
.flags()
.contains(ObjectFlags::EVENT_BUBBLE)
{
break;
}
depth -= 1;
}
Disposition::Unconsumed
}
fn resolve_target(
root: &ObjectNode,
input: &DispatchInput,
) -> Option<(Vec<usize>, ObjectEvent, Option<Event>)> {
match input {
DispatchInput::Pointer { x, y, event } => {
let obj_ev = stream_event_to_object_event(event)?;
let path = hit_test_path(root, *x, *y)?;
Some((path, obj_ev, Some(event.clone())))
}
DispatchInput::PointerObject { x, y, event } => {
let path = hit_test_path(root, *x, *y)?;
Some((path, event.clone(), None))
}
DispatchInput::Focused { event } => {
let path = find_focused_path_ro(root)?;
Some((path, event.clone(), None))
}
DispatchInput::Container { path, event } => Some((path.clone(), event.clone(), None)),
}
}
fn stream_event_to_object_event(ev: &Event) -> Option<ObjectEvent> {
match ev {
Event::PressDown { x, y } => Some(ObjectEvent::Pressed { x: *x, y: *y }),
Event::PressRelease { x, y } => Some(ObjectEvent::Clicked { x: *x, y: *y }),
Event::DoubleTap { x, y } => Some(ObjectEvent::DoubleClicked { x: *x, y: *y }),
Event::LongPress { x, y } => Some(ObjectEvent::LongPressed { x: *x, y: *y }),
Event::LongPressRepeat { x, y } => Some(ObjectEvent::LongPressedRepeat { x: *x, y: *y }),
Event::PointerUp { x, y } => Some(ObjectEvent::Released { x: *x, y: *y }),
_ => None,
}
}
fn hit_test_path(root: &ObjectNode, x: i32, y: i32) -> Option<Vec<usize>> {
hit_test_path_recursive(root, x, y, &mut Vec::new())
}
fn hit_test_path_recursive(
node: &ObjectNode,
x: i32,
y: i32,
path: &mut Vec<usize>,
) -> Option<Vec<usize>> {
if node.is_hidden_or_detached() {
return None;
}
for (i, child) in node.children.iter().enumerate().rev() {
path.push(i);
if let Some(p) = hit_test_path_recursive(child, x, y, path) {
return Some(p);
}
path.pop();
}
if node.is_targetable_at(x, y) {
Some(path.clone())
} else {
None
}
}
fn find_focused_path_ro(root: &ObjectNode) -> Option<Vec<usize>> {
find_focused_ro_recursive(root, &mut Vec::new())
}
fn find_focused_ro_recursive(node: &ObjectNode, path: &mut Vec<usize>) -> Option<Vec<usize>> {
if node.states().contains(ObjectStates::FOCUSED) {
return Some(path.clone());
}
for (i, child) in node.children.iter().enumerate() {
path.push(i);
if let Some(p) = find_focused_ro_recursive(child, path) {
return Some(p);
}
path.pop();
}
None
}
fn node_at_path<'a>(root: &'a ObjectNode, path: &[usize]) -> &'a ObjectNode {
let mut current = root;
for &idx in path {
current = ¤t.children[idx];
}
current
}
fn node_at_path_mut<'a>(root: &'a mut ObjectNode, path: &[usize]) -> &'a mut ObjectNode {
let mut current = root;
for &idx in path {
current = &mut current.children[idx];
}
current
}
fn invoke_trickle(
root: &mut ObjectNode,
node_path: &[usize],
event: &ObjectEvent,
ctx: EventContext,
) -> bool {
let node = node_at_path_mut(root, node_path);
for handler in &mut node.trickle_handlers {
if handler(event, ctx) {
return true;
}
}
false
}
fn invoke_bubble(
root: &mut ObjectNode,
node_path: &[usize],
event: &ObjectEvent,
ctx: EventContext,
) -> bool {
let node = node_at_path_mut(root, node_path);
for handler in &mut node.bubble_handlers {
if handler(event, ctx) {
return true;
}
}
false
}
fn rect_contains(rect: Rect, x: i32, y: i32) -> bool {
rect.width > 0
&& rect.height > 0
&& x >= rect.x
&& y >= rect.y
&& x < rect.x + rect.width
&& y < rect.y + rect.height
}
#[cfg(test)]
mod tests {
use alloc::rc::Rc;
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;
use super::*;
use crate::event::Event;
use crate::renderer::Renderer;
use crate::widget::{Color, Widget};
struct TestWidget {
tag: &'static str,
bounds: Rect,
consume_clicks: bool,
}
impl TestWidget {
fn node(tag: &'static str, bounds: Rect) -> ObjectNode {
ObjectNode::new(Rc::new(RefCell::new(Self {
tag,
bounds,
consume_clicks: false,
})))
.with_tag(tag)
}
}
impl Widget for TestWidget {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
renderer.draw_text(
(self.bounds.x, self.bounds.y),
self.tag,
Color(0, 0, 0, 255),
);
}
fn handle_event(&mut self, event: &Event) -> bool {
self.consume_clicks && matches!(event, Event::PressRelease { .. })
}
}
struct TestRenderer {
draws: Vec<&'static str>,
}
impl Renderer for TestRenderer {
fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
fn draw_text(&mut self, _position: (i32, i32), text: &str, _color: Color) {
match text {
"root" => self.draws.push("root"),
"a" => self.draws.push("a"),
"b" => self.draws.push("b"),
"c" => self.draws.push("c"),
"grand" => self.draws.push("grand"),
_ => {}
}
}
}
fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
Rect {
x,
y,
width,
height,
}
}
#[test]
fn flags_and_states_store_and_clear() {
let mut node = TestWidget::node("root", rect(0, 0, 10, 10));
node.set_flag(ObjectFlags::CLICKABLE, true);
node.set_flag(ObjectFlags::FOCUSABLE, true);
node.set_state(ObjectStates::FOCUSED, true);
assert!(node.flags().contains(ObjectFlags::CLICKABLE));
assert!(node.flags().contains(ObjectFlags::FOCUSABLE));
assert!(node.states().contains(ObjectStates::FOCUSED));
node.set_flag(ObjectFlags::FOCUSABLE, false);
node.set_state(ObjectStates::FOCUSED, false);
assert!(!node.flags().contains(ObjectFlags::FOCUSABLE));
assert!(!node.states().contains(ObjectStates::FOCUSED));
}
#[test]
fn draw_skips_hidden_subtree() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut hidden = TestWidget::node("a", rect(0, 0, 10, 10));
hidden.set_flag(ObjectFlags::HIDDEN, true);
hidden.append_child(TestWidget::node("grand", rect(0, 0, 10, 10)));
root.append_child(hidden);
root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
let mut renderer = TestRenderer { draws: Vec::new() };
root.draw(&mut renderer);
assert_eq!(renderer.draws, vec!["root", "b"]);
}
#[test]
fn hit_test_returns_topmost_clickable_node() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut a = TestWidget::node("a", rect(0, 0, 20, 20));
let mut b = TestWidget::node("b", rect(0, 0, 20, 20));
a.set_flag(ObjectFlags::CLICKABLE, true);
b.set_flag(ObjectFlags::CLICKABLE, true);
root.append_child(a);
root.append_child(b);
assert_eq!(root.hit_test(5, 5).and_then(ObjectNode::tag), Some("b"));
root.children_mut()[1].set_flag(ObjectFlags::DISABLED, true);
assert_eq!(root.hit_test(5, 5).and_then(ObjectNode::tag), Some("a"));
root.children_mut()[0].set_flag(ObjectFlags::HIDDEN, true);
assert_eq!(root.hit_test(5, 5).and_then(ObjectNode::tag), None);
}
#[test]
fn child_reorder_helpers_update_sibling_order() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
root.append_child(TestWidget::node("a", rect(0, 0, 10, 10)));
root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
root.append_child(TestWidget::node("c", rect(0, 0, 10, 10)));
assert!(root.raise_child(0));
assert_eq!(child_tags(&root), vec!["b", "c", "a"]);
assert!(root.lower_child(1));
assert_eq!(child_tags(&root), vec!["c", "b", "a"]);
assert!(root.move_child_before(2, 1));
assert_eq!(child_tags(&root), vec!["c", "a", "b"]);
assert!(root.move_child_after(0, 2));
assert_eq!(child_tags(&root), vec!["a", "b", "c"]);
assert!(!root.raise_child(3));
assert!(!root.move_child_before(0, 3));
}
#[test]
fn detach_child_marks_returned_subtree_detached() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut child = TestWidget::node("a", rect(0, 0, 10, 10));
child.append_child(TestWidget::node("grand", rect(0, 0, 10, 10)));
root.append_child(child);
let detached = root.detach_child(0).expect("child is present");
assert!(root.children().is_empty());
assert!(detached.is_detached());
assert!(detached.children()[0].is_detached());
}
#[test]
fn adopt_preserves_widget_node_shape() {
let mut widget_root = crate::WidgetNode::new(Rc::new(RefCell::new(TestWidget {
tag: "root",
bounds: rect(0, 0, 100, 100),
consume_clicks: false,
})))
.with_tag("root");
widget_root.children.push(
crate::WidgetNode::new(Rc::new(RefCell::new(TestWidget {
tag: "a",
bounds: rect(0, 0, 10, 10),
consume_clicks: false,
})))
.with_tag("a"),
);
let object_root = ObjectNode::adopt(widget_root);
assert_eq!(object_root.tag(), Some("root"));
assert_eq!(object_root.children()[0].tag(), Some("a"));
assert_eq!(object_root.flags(), ObjectFlags::EMPTY);
assert_eq!(object_root.children()[0].states(), ObjectStates::DEFAULT);
}
#[test]
fn visible_subtree_extent_unions_children() {
let mut root = TestWidget::node("root", rect(10, 10, 20, 20));
root.append_child(TestWidget::node("a", rect(40, 5, 10, 10)));
root.append_child(TestWidget::node("b", rect(0, 30, 10, 10)));
assert_eq!(root.visible_subtree_extent(), Some(rect(0, 5, 50, 35)));
}
#[test]
fn visible_subtree_extent_excludes_hidden_child() {
let mut root = TestWidget::node("root", rect(0, 0, 10, 10));
let mut hidden = TestWidget::node("a", rect(50, 50, 10, 10));
hidden.set_flag(ObjectFlags::HIDDEN, true);
root.append_child(hidden);
assert_eq!(root.visible_subtree_extent(), Some(rect(0, 0, 10, 10)));
}
#[test]
fn visible_subtree_extent_skips_zero_area_bounds() {
let mut root = TestWidget::node("root", rect(0, 0, 0, 0));
root.append_child(TestWidget::node("a", rect(5, 5, 10, 10)));
assert_eq!(root.visible_subtree_extent(), Some(rect(5, 5, 10, 10)));
}
#[test]
fn visible_subtree_extent_is_none_for_hidden_or_detached_root() {
let mut hidden = TestWidget::node("a", rect(0, 0, 10, 10));
hidden.set_flag(ObjectFlags::HIDDEN, true);
assert_eq!(hidden.visible_subtree_extent(), None);
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
let detached = root.detach_child(0).expect("child is present");
assert_eq!(detached.visible_subtree_extent(), None);
}
#[test]
fn hide_flow_computes_extent_before_hiding() {
use crate::invalidation::{InvalidationList, PresentPlan};
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut panel = TestWidget::node("a", rect(10, 10, 20, 20));
panel.append_child(TestWidget::node("grand", rect(25, 25, 20, 20)));
root.append_child(panel);
let extent = root.children()[0].visible_subtree_extent();
assert_eq!(extent, Some(rect(10, 10, 35, 35)));
root.children_mut()[0].set_flag(ObjectFlags::HIDDEN, true);
assert_eq!(root.children()[0].visible_subtree_extent(), None);
let mut list = InvalidationList::<4>::new(rect(0, 0, 100, 100));
list.push_opt(extent);
assert_eq!(list.plan(), PresentPlan::Rects(&[rect(10, 10, 35, 35)]));
}
#[test]
fn detach_flow_computes_extent_before_detaching() {
use crate::invalidation::{InvalidationList, PresentPlan};
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut panel = TestWidget::node("a", rect(60, 60, 30, 30));
panel.append_child(TestWidget::node("grand", rect(80, 80, 30, 30)));
root.append_child(panel);
let extent = root.children()[0]
.visible_subtree_extent()
.expect("visible subtree has an extent");
assert_eq!(extent, rect(60, 60, 50, 50));
let detached = root.detach_child(0).expect("child is present");
assert_eq!(detached.visible_subtree_extent(), None);
let mut list = InvalidationList::<4>::new(rect(0, 0, 100, 100));
list.push(extent);
assert_eq!(list.plan(), PresentPlan::Rects(&[rect(60, 60, 40, 40)]));
}
fn clickable(tag: &'static str, x: i32, y: i32) -> ObjectNode {
let mut n = TestWidget::node(tag, rect(x, y, 10, 10));
n.set_flag(ObjectFlags::CLICKABLE, true);
n
}
#[test]
fn dispatch_phase_order_trickle_target_bubble() {
let visit_log: Rc<RefCell<Vec<(&'static str, &'static str)>>> =
Rc::new(RefCell::new(Vec::new()));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut a = clickable("a", 0, 0);
a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
root.append_child(a);
{
let log = visit_log.clone();
root.add_trickle_handler(move |_ev, ctx| {
log.borrow_mut()
.push((ctx.current_tag.unwrap_or("?"), "trickle"));
false
});
}
{
let log = visit_log.clone();
root.add_bubble_handler(move |_ev, ctx| {
log.borrow_mut()
.push((ctx.current_tag.unwrap_or("?"), "bubble"));
false
});
}
{
let log = visit_log.clone();
root.children_mut()[0].add_target_handler(move |_ev, ctx| {
log.borrow_mut()
.push((ctx.current_tag.unwrap_or("?"), "target"));
false
});
}
let ev = DispatchInput::PointerObject {
x: 5,
y: 5,
event: ObjectEvent::Clicked { x: 5, y: 5 },
};
let result = dispatch_object_event(&mut root, ev);
assert_eq!(result, Disposition::Unconsumed);
let log = visit_log.borrow();
assert_eq!(
*log,
vec![("root", "trickle"), ("a", "target"), ("root", "bubble"),]
);
}
#[test]
fn stop_on_consume_at_target_prevents_bubble() {
let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
{
let called = bubble_called.clone();
root.add_bubble_handler(move |_ev, _ctx| {
*called.borrow_mut() = true;
false
});
}
let mut a = clickable("a", 0, 0);
a.add_target_handler(|_ev, _ctx| true);
a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
root.append_child(a);
let result = dispatch_object_event(
&mut root,
DispatchInput::PointerObject {
x: 5,
y: 5,
event: ObjectEvent::Clicked { x: 5, y: 5 },
},
);
assert_eq!(result, Disposition::Consumed);
assert!(
!*bubble_called.borrow(),
"bubble must not run after consume"
);
}
#[test]
fn stop_on_consume_at_trickle_prevents_target_and_bubble() {
let target_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut a = clickable("a", 0, 0);
a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
root.append_child(a);
root.add_trickle_handler(|_ev, _ctx| true);
{
let tc = target_called.clone();
root.children_mut()[0].add_target_handler(move |_ev, _ctx| {
*tc.borrow_mut() = true;
false
});
}
{
let bc = bubble_called.clone();
root.add_bubble_handler(move |_ev, _ctx| {
*bc.borrow_mut() = true;
false
});
}
let result = dispatch_object_event(
&mut root,
DispatchInput::PointerObject {
x: 5,
y: 5,
event: ObjectEvent::Clicked { x: 5, y: 5 },
},
);
assert_eq!(result, Disposition::Consumed);
assert!(!*target_called.borrow());
assert!(!*bubble_called.borrow());
}
#[test]
fn bubble_only_when_event_bubble_flag_set() {
let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
{
let called = bubble_called.clone();
root.add_bubble_handler(move |_ev, _ctx| {
*called.borrow_mut() = true;
false
});
}
let a = clickable("a", 0, 0);
root.append_child(a);
dispatch_object_event(
&mut root,
DispatchInput::PointerObject {
x: 5,
y: 5,
event: ObjectEvent::Clicked { x: 5, y: 5 },
},
);
assert!(
!*bubble_called.borrow(),
"bubble must not run without EVENT_BUBBLE"
);
}
#[test]
fn bubble_runs_when_event_bubble_flag_set() {
let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
{
let called = bubble_called.clone();
root.add_bubble_handler(move |_ev, _ctx| {
*called.borrow_mut() = true;
false
});
}
let mut a = clickable("a", 0, 0);
a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
root.append_child(a);
dispatch_object_event(
&mut root,
DispatchInput::PointerObject {
x: 5,
y: 5,
event: ObjectEvent::Clicked { x: 5, y: 5 },
},
);
assert!(*bubble_called.borrow(), "bubble must run with EVENT_BUBBLE");
}
#[test]
fn bubble_stops_at_ancestor_without_event_bubble_flag() {
let b_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let root_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut b = TestWidget::node("b", rect(0, 0, 20, 20));
let mut a = clickable("a", 0, 0);
a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
b.append_child(a);
root.append_child(b);
{
let called = b_called.clone();
root.children_mut()[0].add_bubble_handler(move |_ev, _ctx| {
*called.borrow_mut() = true;
false
});
}
{
let called = root_called.clone();
root.add_bubble_handler(move |_ev, _ctx| {
*called.borrow_mut() = true;
false
});
}
dispatch_object_event(
&mut root,
DispatchInput::PointerObject {
x: 5,
y: 5,
event: ObjectEvent::Clicked { x: 5, y: 5 },
},
);
assert!(
*b_called.borrow(),
"bubble must reach the flagged target's parent"
);
assert!(
!*root_called.borrow(),
"bubble must stop at an ancestor that lacks EVENT_BUBBLE"
);
}
#[test]
fn reorder_emits_child_changed_to_parent() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
root.append_child(TestWidget::node("a", rect(0, 0, 10, 10)));
root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
root.append_child(TestWidget::node("c", rect(0, 0, 10, 10)));
{
let c = count.clone();
root.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::ChildChanged) {
*c.borrow_mut() += 1;
}
false
});
}
assert!(root.raise_child(0));
assert!(root.lower_child(1));
assert!(root.move_child_before(2, 0));
assert!(root.move_child_after(0, 2));
assert_eq!(
*count.borrow(),
4,
"each effective reorder emits ChildChanged"
);
assert!(root.move_child_before(1, 1));
assert_eq!(
*count.borrow(),
4,
"a no-op move must not emit ChildChanged"
);
}
#[test]
fn pointer_target_resolved_via_hit_test() {
let target_seen: Rc<RefCell<Option<&'static str>>> = Rc::new(RefCell::new(None));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut a = clickable("a", 0, 0);
{
let ts = target_seen.clone();
a.add_target_handler(move |_ev, ctx| {
*ts.borrow_mut() = ctx.target_tag;
false
});
}
root.append_child(a);
dispatch_object_event(
&mut root,
DispatchInput::PointerObject {
x: 5,
y: 5,
event: ObjectEvent::Clicked { x: 5, y: 5 },
},
);
assert_eq!(*target_seen.borrow(), Some("a"));
}
#[test]
fn pointer_no_target_returns_no_target() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let result = dispatch_object_event(
&mut root,
DispatchInput::PointerObject {
x: 50,
y: 50,
event: ObjectEvent::Clicked { x: 50, y: 50 },
},
);
assert_eq!(result, Disposition::NoTarget);
}
#[test]
fn key_event_resolves_to_focused_node() {
let key_seen: Rc<RefCell<Option<&'static str>>> = Rc::new(RefCell::new(None));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut a = TestWidget::node("a", rect(0, 0, 10, 10));
a.set_flag(ObjectFlags::FOCUSABLE, true);
{
let ks = key_seen.clone();
a.add_target_handler(move |_ev, ctx| {
*ks.borrow_mut() = ctx.target_tag;
false
});
}
root.append_child(a);
root.children_mut()[0].set_state(ObjectStates::FOCUSED, true);
let result = dispatch_object_event(
&mut root,
DispatchInput::Focused {
event: ObjectEvent::Key(crate::event::Key::Enter),
},
);
assert_eq!(result, Disposition::Unconsumed);
assert_eq!(*key_seen.borrow(), Some("a"));
}
#[test]
fn key_event_no_focused_returns_no_target() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
root.append_child(TestWidget::node("a", rect(0, 0, 10, 10)));
let result = dispatch_object_event(
&mut root,
DispatchInput::Focused {
event: ObjectEvent::Key(crate::event::Key::Enter),
},
);
assert_eq!(result, Disposition::NoTarget);
}
#[test]
fn stream_press_release_maps_to_clicked() {
let ev_seen: Rc<RefCell<Option<ObjectEvent>>> = Rc::new(RefCell::new(None));
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut a = clickable("a", 0, 0);
{
let es = ev_seen.clone();
a.add_target_handler(move |ev, _ctx| {
*es.borrow_mut() = Some(ev.clone());
false
});
}
root.append_child(a);
dispatch_object_event(
&mut root,
DispatchInput::Pointer {
x: 5,
y: 5,
event: Event::PressRelease { x: 5, y: 5 },
},
);
assert_eq!(*ev_seen.borrow(), Some(ObjectEvent::Clicked { x: 5, y: 5 }));
}
#[test]
fn stream_tick_produces_no_target() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
root.append_child(clickable("a", 0, 0));
let result = dispatch_object_event(
&mut root,
DispatchInput::Pointer {
x: 5,
y: 5,
event: Event::Tick,
},
);
assert_eq!(result, Disposition::NoTarget);
}
#[test]
fn widget_handle_event_at_target_phase_can_consume() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let widget = Rc::new(RefCell::new(TestWidget {
tag: "a",
bounds: rect(0, 0, 10, 10),
consume_clicks: true,
}));
let mut a = ObjectNode::new(widget).with_tag("a");
a.set_flag(ObjectFlags::CLICKABLE, true);
root.append_child(a);
let result = dispatch_object_event(
&mut root,
DispatchInput::Pointer {
x: 5,
y: 5,
event: Event::PressRelease { x: 5, y: 5 },
},
);
assert_eq!(result, Disposition::Consumed);
}
#[test]
fn append_child_emits_attached_and_child_changed() {
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
let mut parent = TestWidget::node("parent", rect(0, 0, 100, 100));
{
let l = log.clone();
parent.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::ChildChanged) {
l.borrow_mut().push("parent:child_changed");
}
false
});
}
let mut child = TestWidget::node("child", rect(0, 0, 10, 10));
{
let l = log.clone();
child.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::Attached) {
l.borrow_mut().push("child:attached");
}
false
});
}
parent.append_child(child);
assert_eq!(
*log.borrow(),
vec!["child:attached", "parent:child_changed"]
);
}
#[test]
fn insert_child_emits_attached_and_child_changed() {
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
let mut parent = TestWidget::node("parent", rect(0, 0, 100, 100));
{
let l = log.clone();
parent.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::ChildChanged) {
l.borrow_mut().push("parent:child_changed");
}
false
});
}
let mut child = TestWidget::node("child", rect(0, 0, 10, 10));
{
let l = log.clone();
child.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::Attached) {
l.borrow_mut().push("child:attached");
}
false
});
}
let ok = parent.insert_child(0, child);
assert!(ok);
assert_eq!(
*log.borrow(),
vec!["child:attached", "parent:child_changed"]
);
}
#[test]
fn detach_child_emits_detached_and_child_changed() {
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
let mut parent = TestWidget::node("parent", rect(0, 0, 100, 100));
{
let l = log.clone();
parent.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::ChildChanged) {
l.borrow_mut().push("parent:child_changed");
}
false
});
}
let mut child = TestWidget::node("child", rect(0, 0, 10, 10));
{
let l = log.clone();
child.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::Detached) {
l.borrow_mut().push("child:detached");
}
false
});
}
parent.append_child(child);
log.borrow_mut().clear();
let detached = parent.detach_child(0).expect("has child");
drop(detached);
assert_eq!(
*log.borrow(),
vec!["child:detached", "parent:child_changed"]
);
}
#[test]
fn detach_child_still_marks_subtree_detached() {
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut child = TestWidget::node("a", rect(0, 0, 10, 10));
child.append_child(TestWidget::node("grand", rect(0, 0, 10, 10)));
root.append_child(child);
let detached = root.detach_child(0).expect("child is present");
assert!(root.children().is_empty());
assert!(detached.is_detached());
assert!(detached.children()[0].is_detached());
}
#[test]
fn focus_path_not_tag_based() {
use crate::focus::FocusGroup;
let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
let mut a = TestWidget::node("a", rect(0, 0, 10, 10));
a.set_flag(ObjectFlags::FOCUSABLE, true);
root.append_child(a);
let fg = FocusGroup::new();
let ok = fg.focus_path(&mut root, &[0usize]);
assert!(ok);
assert!(root.children()[0].states().contains(ObjectStates::FOCUSED));
}
fn child_tags(root: &ObjectNode) -> Vec<&'static str> {
root.children().iter().filter_map(ObjectNode::tag).collect()
}
}