use geometry_core::{Rect, Transform};
use layout_core::{LayoutError, LayoutStyle, NodeId};
use platform_core::{Event, Key, NamedKey, PointerButton, PointerSource, ScrollDelta};
use reactive_core::{Effect, RwSignal, effect, signal};
use renderer_core::RectStyle;
use ui_tree::{Component, EventResult, RenderNode};
use crate::child_host::{ChildSlot, DynHost};
use crate::context::{new_container, track_layout};
use crate::drag::DragGesture;
use crate::focus::{self, FocusId};
use crate::layout_item::{LayoutItem, TrackedChildren, register_container};
use crate::pointer::dispatch_container_event;
use crate::press::PressGesture;
pub struct StyledContainer {
node: NodeId,
rect: RwSignal<Rect>,
style: Box<dyn Fn(Rect) -> RectStyle>,
hover_style: Option<Box<dyn Fn(Rect) -> RectStyle>>,
is_hovered: RwSignal<bool>,
active_style: Option<Box<dyn Fn(Rect) -> RectStyle>>,
is_active: RwSignal<bool>,
opacity: Box<dyn Fn() -> f32>,
transform: Box<dyn Fn(Rect) -> Option<[f32; 6]>>,
children: TrackedChildren,
dyn_host: Option<DynHost>,
press: PressGesture,
drag: DragGesture,
on_hover: Option<Box<dyn Fn(bool)>>,
on_scroll: Option<Box<dyn Fn(f32, f32)>>,
on_key: Option<Box<dyn Fn(&Key)>>,
focus_id: Option<FocusId>,
_focus_effect: Option<Effect>,
}
impl StyledContainer {
pub fn new(
layout_style: LayoutStyle,
style: impl Fn(Rect) -> RectStyle + 'static,
children: Vec<Box<dyn LayoutItem>>,
) -> Result<Self, LayoutError> {
let (node, rect, children) = register_container(layout_style, children)?;
Ok(Self {
node,
rect,
style: Box::new(style),
hover_style: None,
is_hovered: signal(false),
active_style: None,
is_active: signal(false),
opacity: Box::new(|| 1.0),
transform: Box::new(|_| None),
children,
dyn_host: None,
press: PressGesture::default(),
drag: DragGesture::default(),
on_hover: None,
on_scroll: None,
on_key: None,
focus_id: None,
_focus_effect: None,
})
}
pub fn from_slots(
layout_style: LayoutStyle,
style: impl Fn(Rect) -> RectStyle + 'static,
slots: Vec<ChildSlot>,
) -> Result<Self, LayoutError> {
let node = new_container(layout_style, &[])?;
let rect = track_layout(node).expect("new_container always registers a signal");
let dyn_host = DynHost::build(node, slots)?;
Ok(Self {
node,
rect,
style: Box::new(style),
hover_style: None,
is_hovered: signal(false),
active_style: None,
is_active: signal(false),
opacity: Box::new(|| 1.0),
transform: Box::new(|_| None),
children: Vec::new(),
dyn_host: Some(dyn_host),
press: PressGesture::default(),
drag: DragGesture::default(),
on_hover: None,
on_scroll: None,
on_key: None,
focus_id: None,
_focus_effect: None,
})
}
fn dispatch_children(&mut self, event: &Event) -> EventResult {
match &self.dyn_host {
Some(host) => host.dispatch(event),
None => dispatch_container_event(&mut self.children, event),
}
}
pub fn with_opacity(mut self, opacity: impl Fn() -> f32 + 'static) -> Self {
self.opacity = Box::new(opacity);
self
}
pub fn with_transform(
mut self,
transform: impl Fn(Rect) -> Option<[f32; 6]> + 'static,
) -> Self {
self.transform = Box::new(transform);
self
}
pub fn on_hover_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
self.hover_style = Some(Box::new(f));
self
}
pub fn on_active_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
self.active_style = Some(Box::new(f));
self
}
fn set_active(&self, active: bool) {
if self.active_style.is_some() && self.is_active.get() != active {
self.is_active.set(active);
}
}
pub fn on_press(mut self, f: impl Fn() + 'static) -> Self {
self.press.set(f);
self.mark_interactive();
self
}
pub fn on_alt_press(mut self, f: impl Fn(PointerButton) + 'static) -> Self {
self.press.set_alt_press(f);
self.mark_interactive();
self
}
pub fn on_long_press(mut self, f: impl Fn() + 'static) -> Self {
self.press.set_long_press(f);
self.mark_interactive();
self
}
pub fn on_drag_end(mut self, f: impl Fn(f32, f32) + 'static) -> Self {
self.drag.set_end(f);
self.mark_interactive();
self
}
pub fn on_drag(mut self, f: impl Fn(f32, f32) + 'static) -> Self {
self.drag.set(f);
self.mark_interactive();
self
}
fn mark_interactive(&self) {
crate::input_region::register_interactive(self.node, self.rect.read_only());
}
pub fn on_hover(mut self, f: impl Fn(bool) + 'static) -> Self {
self.on_hover = Some(Box::new(f));
self.mark_interactive();
self
}
pub fn on_scroll(mut self, f: impl Fn(f32, f32) + 'static) -> Self {
self.on_scroll = Some(Box::new(f));
self.mark_interactive();
self
}
pub fn on_key(mut self, f: impl Fn(&Key) + 'static) -> Self {
self.on_key = Some(Box::new(f));
self
}
pub fn on_focus(mut self, f: impl Fn(bool) + 'static) -> Self {
let id = *self.focus_id.get_or_insert_with(focus::next_id);
focus::register(id);
let last = std::rc::Rc::new(std::cell::Cell::new(focus::is_focused(id)));
self._focus_effect = Some(effect(move || {
let now = focus::is_focused(id);
if now != last.get() {
last.set(now);
f(now);
}
}));
self
}
}
impl LayoutItem for StyledContainer {
fn layout_node(&self) -> NodeId {
self.node
}
}
impl Component for StyledContainer {
fn view(&self) -> RenderNode {
let r = self.rect.get();
let style = if let Some(active) = &self.active_style
&& self.is_active.get()
{
active
} else if let Some(hover) = &self.hover_style
&& self.is_hovered.get()
{
hover
} else {
&self.style
};
let background = RenderNode::rect(
Rect {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
style(r),
);
let content = match &self.dyn_host {
Some(host) => {
RenderNode::group(std::iter::once(background).chain(host.child_boundaries()))
}
None => RenderNode::group(
std::iter::once(background)
.chain(self.children.iter().map(|c| c.segment.boundary())),
),
};
let opacity = (self.opacity)();
let composed = if opacity < 1.0 {
RenderNode::layer(opacity, 0.0, [content])
} else {
content
};
match (self.transform)(r) {
Some(matrix) => RenderNode::transform_with(matrix, [composed]),
None => composed,
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
if !self.press.is_set()
&& !self.drag.is_set()
&& self.hover_style.is_none()
&& self.active_style.is_none()
&& self.on_hover.is_none()
&& self.on_scroll.is_none()
&& self.on_key.is_none()
&& self.focus_id.is_none()
{
return self.dispatch_children(event);
}
let rect = self.rect.get();
match event {
Event::PointerMoved { x, y, source } => {
self.press.track_move(event);
let dragged = self.drag.moved(event, rect) == EventResult::Handled;
let child = self.dispatch_children(event);
let inside = rect.contains(*x as f32, *y as f32);
if !inside {
self.set_active(false);
}
let tracks_hover = self.hover_style.is_some()
|| self.on_hover.is_some()
|| self.on_scroll.is_some();
if tracks_hover
&& matches!(source, PointerSource::Mouse)
&& inside != self.is_hovered.get()
{
self.is_hovered.set(inside);
if let Some(cb) = &self.on_hover {
cb(inside);
}
return EventResult::Handled;
}
if dragged { EventResult::Handled } else { child }
}
Event::PointerPressed { x, y, button, .. } => {
let primary = *button == PointerButton::Primary;
if !primary && !self.press.wants_alt() {
return self.dispatch_children(event);
}
if self.dispatch_children(event) == EventResult::Handled {
self.press.cancel();
self.drag.end(None);
return EventResult::Handled;
}
if primary && rect.contains(*x as f32, *y as f32) {
self.set_active(true);
}
let focused = match self.focus_id {
Some(id) if primary && rect.contains(*x as f32, *y as f32) => {
focus::request(id);
true
}
_ => false,
};
let tapped =
self.press.is_set() && self.press.arm(event, rect) == EventResult::Handled;
let dragged = primary
&& self.drag.is_set()
&& self.drag.press(event, rect) == EventResult::Handled;
if tapped || dragged || focused {
EventResult::Handled
} else {
EventResult::Ignored
}
}
Event::PointerReleased { button, .. } => {
let primary = *button == PointerButton::Primary;
if !primary && !self.press.wants_alt() {
return self.dispatch_children(event);
}
if primary {
self.set_active(false);
}
if self.dispatch_children(event) == EventResult::Handled {
self.press.cancel();
self.drag.end(None);
return EventResult::Handled;
}
let released_at = match event {
Event::PointerReleased { x, y, .. } => {
Some((*x as f32 - rect.x, *y as f32 - rect.y))
}
_ => None,
};
let dragged = primary && self.drag.end(released_at);
let tapped =
self.press.is_set() && self.press.release(event, rect) == EventResult::Handled;
if tapped || dragged {
EventResult::Handled
} else {
EventResult::Ignored
}
}
Event::CursorLeft => {
self.press.cancel();
self.drag.end(None);
self.set_active(false);
let tracks_hover = self.hover_style.is_some()
|| self.on_hover.is_some()
|| self.on_scroll.is_some();
if tracks_hover && self.is_hovered.get() {
self.is_hovered.set(false);
if let Some(cb) = &self.on_hover {
cb(false);
}
}
self.dispatch_children(event)
}
Event::Scrolled { delta } => {
if self.dispatch_children(event) == EventResult::Handled {
return EventResult::Handled;
}
let Some(cb) = &self.on_scroll else {
return EventResult::Ignored;
};
if !self.is_hovered.get() {
return EventResult::Ignored;
}
let (dx, dy) = match delta {
ScrollDelta::Lines { x, y } => (*x * 20.0, *y * 20.0),
ScrollDelta::Pixels { x, y } => (*x, *y),
};
cb(dx, dy);
EventResult::Handled
}
Event::KeyPressed { key, modifiers } => {
if let Some(id) = self.focus_id
&& focus::is_focused(id)
&& matches!(key, Key::Named(NamedKey::Tab))
{
if modifiers.is_shift {
focus::focus_prev();
} else {
focus::focus_next();
}
return EventResult::Handled;
}
if let Some(cb) = &self.on_key {
cb(key);
}
self.dispatch_children(event)
}
_ => self.dispatch_children(event),
}
}
fn debug_name(&self) -> &'static str {
"StyledContainer"
}
}
impl Drop for StyledContainer {
fn drop(&mut self) {
self._focus_effect.take();
if let Some(id) = self.focus_id {
focus::unregister(id);
}
crate::input_region::unregister_interactive(self.node);
}
}
pub fn box_transform(
rect: Rect,
rotate_deg: f32,
scale_x: f32,
scale_y: f32,
translate_x: f32,
translate_y: f32,
) -> Option<[f32; 6]> {
if rotate_deg == 0.0
&& scale_x == 1.0
&& scale_y == 1.0
&& translate_x == 0.0
&& translate_y == 0.0
{
return None;
}
let cx = rect.x + rect.width / 2.0;
let cy = rect.y + rect.height / 2.0;
let matrix = Transform::rotate_around(rotate_deg, cx, cy)
.then(Transform::scale_around(scale_x, scale_y, cx, cy))
.then(Transform::translate(translate_x, translate_y));
Some(matrix.to_array())
}
#[cfg(test)]
mod tests {
use crate::context::reset_layout_runtime;
use std::cell::Cell;
use std::rc::Rc;
use layout_core::AvailableSpace;
use platform_core::{PointerButton, PointerSource};
use renderer_core::{Color, ShapeStyle};
use theme_core::{Theme, ThemeTokens, set_theme, use_theme};
use super::*;
#[test]
fn box_transform_identity_is_none() {
let r = Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
};
assert!(box_transform(r, 0.0, 1.0, 1.0, 0.0, 0.0).is_none());
}
#[test]
fn box_transform_scale_pivots_on_center() {
let r = Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
assert_eq!(
box_transform(r, 0.0, 2.0, 2.0, 0.0, 0.0).unwrap(),
[2.0, 0.0, 0.0, 2.0, -50.0, -50.0]
);
}
#[test]
fn box_transform_translate_offsets_origin() {
let r = Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
};
assert_eq!(
box_transform(r, 0.0, 1.0, 1.0, 8.0, -4.0).unwrap(),
[1.0, 0.0, 0.0, 1.0, 8.0, -4.0]
);
}
use crate::container::Container;
use crate::context::{compute_layout, track_layout};
fn press(x: f64, y: f64, source: PointerSource) -> Event {
Event::PointerPressed {
x,
y,
button: PointerButton::Primary,
source,
}
}
fn release(x: f64, y: f64, source: PointerSource) -> Event {
Event::PointerReleased {
x,
y,
button: PointerButton::Primary,
source,
}
}
#[test]
fn on_hover_fires_on_enter_and_leave() {
let seen: Rc<Cell<Option<bool>>> = Rc::new(Cell::new(None));
let sink = seen.clone();
reset_layout_runtime();
let inner = Container::new(LayoutStyle::new().width(100.0).height(100.0), vec![]).unwrap();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![Box::new(inner)],
)
.unwrap()
.on_hover(move |h| sink.set(Some(h)));
let node = card.layout_node();
compute_layout(
node,
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
card.on_event(&Event::PointerMoved {
x: 50.0,
y: 50.0,
source: PointerSource::Mouse,
});
assert_eq!(seen.get(), Some(true), "entering fires on_hover(true)");
card.on_event(&Event::CursorLeft);
assert_eq!(seen.get(), Some(false), "leaving fires on_hover(false)");
}
#[test]
fn on_scroll_fires_only_while_hovered_and_normalises_lines() {
let seen: Rc<Cell<(f32, f32)>> = Rc::new(Cell::new((0.0, 0.0)));
let sink = seen.clone();
reset_layout_runtime();
let inner = Container::new(LayoutStyle::new().width(100.0).height(100.0), vec![]).unwrap();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![Box::new(inner)],
)
.unwrap()
.on_scroll(move |dx, dy| sink.set((dx, dy)));
let node = card.layout_node();
compute_layout(
node,
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
assert_eq!(
card.on_event(&Event::Scrolled {
delta: ScrollDelta::Pixels { x: 0.0, y: -30.0 },
}),
EventResult::Ignored,
"a wheel event outside the box is ignored"
);
assert_eq!(seen.get(), (0.0, 0.0));
card.on_event(&Event::PointerMoved {
x: 50.0,
y: 50.0,
source: PointerSource::Mouse,
});
assert_eq!(
card.on_event(&Event::Scrolled {
delta: ScrollDelta::Pixels { x: 0.0, y: -30.0 },
}),
EventResult::Handled,
"hovering makes the wheel ours"
);
assert_eq!(seen.get(), (0.0, -30.0));
card.on_event(&Event::Scrolled {
delta: ScrollDelta::Lines { x: 0.0, y: 3.0 },
});
assert_eq!(seen.get(), (0.0, 60.0));
card.on_event(&Event::CursorLeft);
assert_eq!(
card.on_event(&Event::Scrolled {
delta: ScrollDelta::Pixels { x: 0.0, y: -10.0 },
}),
EventResult::Ignored,
"the wheel is released once the pointer leaves"
);
assert_eq!(seen.get(), (0.0, 60.0), "the handler did not fire again");
}
#[test]
fn on_key_fires_on_key_press() {
let count = Rc::new(Cell::new(0u32));
let sink = count.clone();
reset_layout_runtime();
let inner = Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column(),
|_r| RectStyle::default(),
vec![Box::new(inner)],
)
.unwrap()
.on_key(move |_k| sink.set(sink.get() + 1));
card.on_event(&Event::KeyPressed {
key: Key::Char('a'),
modifiers: platform_core::ModifiersState::default(),
});
assert_eq!(count.get(), 1, "a key press fires on_key");
}
#[derive(Clone)]
struct TestTheme(Color);
impl Theme for TestTheme {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ThemeTokens for TestTheme {
fn primary(&self) -> Color {
self.0
}
fn on_primary(&self) -> Color {
Color::WHITE
}
}
#[test]
fn theme_button_click_force_tick_no_panic() {
set_theme(TestTheme(Color::RED));
reset_layout_runtime();
let btn = StyledContainer::new(
LayoutStyle::new().width(50.0).height(30.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_press(move || set_theme(TestTheme(Color::GREEN)));
let btn_node = btn.layout_node();
let inner = Container::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
vec![Box::new(btn)],
)
.unwrap();
let card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default().with_fill(use_theme::<TestTheme>().0),
vec![Box::new(inner)],
)
.unwrap();
let card_node = card.layout_node();
compute_layout(
card_node,
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
let br = track_layout(btn_node).unwrap().get();
let mut tree = crate::ComponentList::new(card);
let _ = tree.commands();
reactive_core::begin_batch();
let handled = tree.on_event(&Event::PointerPressed {
x: (br.x + br.width / 2.0) as f64,
y: (br.y + br.height / 2.0) as f64,
button: PointerButton::Primary,
source: PointerSource::Mouse,
});
if handled == EventResult::Handled {
tree.bump_force_ticks();
reactive_core::end_batch();
reactive_core::begin_batch();
}
let _ = tree.commands();
reactive_core::end_batch();
}
#[test]
fn on_press_fires_on_tap_not_press() {
let flag = Rc::new(Cell::new(false));
let f = flag.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_press(move || f.set(true));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
assert_eq!(
card.on_event(&press(100.0, 50.0, PointerSource::Mouse)),
EventResult::Handled
);
assert!(!flag.get(), "press alone must not fire on_press");
assert_eq!(
card.on_event(&release(100.0, 50.0, PointerSource::Mouse)),
EventResult::Handled
);
assert!(flag.get(), "release inside the box fires on_press");
}
#[test]
fn on_long_press_fires_after_threshold_not_on_quick_release() {
let long_flag = Rc::new(Cell::new(false));
let tap_flag = Rc::new(Cell::new(false));
let lf = long_flag.clone();
let tf = tap_flag.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_press(move || tf.set(true))
.on_long_press(move || lf.set(true));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
assert!(tap_flag.get(), "a quick release still fires on_press");
assert!(
!long_flag.get(),
"a quick release must not fire on_long_press"
);
tap_flag.set(false);
card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
std::thread::sleep(std::time::Duration::from_millis(550));
card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
assert!(
long_flag.get(),
"a release after the threshold fires on_long_press"
);
assert!(!tap_flag.get(), "a long press must not also fire on_press");
}
fn press_with(x: f64, y: f64, button: PointerButton) -> Event {
Event::PointerPressed {
x,
y,
button,
source: PointerSource::Mouse,
}
}
fn release_with(x: f64, y: f64, button: PointerButton) -> Event {
Event::PointerReleased {
x,
y,
button,
source: PointerSource::Mouse,
}
}
fn laid_out_box() -> StyledContainer {
reset_layout_runtime();
StyledContainer::new(
LayoutStyle::new().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
}
fn settle(card: &mut StyledContainer) {
compute_layout(
card.layout_node(),
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
}
#[test]
fn on_alt_press_reports_which_non_primary_button_tapped() {
let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
let sink = seen.clone();
let mut card = laid_out_box().on_alt_press(move |b| sink.set(Some(b)));
settle(&mut card);
card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
card.on_event(&release_with(50.0, 50.0, PointerButton::Secondary));
assert_eq!(seen.take(), Some(PointerButton::Secondary));
card.on_event(&press_with(50.0, 50.0, PointerButton::Auxiliary));
card.on_event(&release_with(50.0, 50.0, PointerButton::Auxiliary));
assert_eq!(seen.take(), Some(PointerButton::Auxiliary));
}
#[test]
fn a_box_wanting_only_alt_presses_leaves_the_primary_one_alone() {
let alt = Rc::new(Cell::new(false));
let sink = alt.clone();
let mut card = laid_out_box().on_alt_press(move |_| sink.set(true));
settle(&mut card);
assert_eq!(
card.on_event(&press_with(50.0, 50.0, PointerButton::Primary)),
EventResult::Ignored,
"a primary press must still fall through to whatever is behind the box"
);
card.on_event(&release_with(50.0, 50.0, PointerButton::Primary));
assert!(!alt.get(), "the primary button is not an alt press");
}
#[test]
fn a_plain_pressable_box_still_ignores_non_primary_buttons() {
let tapped = Rc::new(Cell::new(false));
let sink = tapped.clone();
let mut card = laid_out_box().on_press(move || sink.set(true));
settle(&mut card);
assert_eq!(
card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary)),
EventResult::Ignored,
"right-click keeps passing through a box that never asked for it"
);
card.on_event(&release_with(50.0, 50.0, PointerButton::Secondary));
assert!(!tapped.get(), "on_press is a primary-button gesture");
}
#[test]
fn releasing_a_different_button_than_armed_completes_nothing() {
let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
let sink = seen.clone();
let tapped = Rc::new(Cell::new(false));
let tap_sink = tapped.clone();
let mut card = laid_out_box()
.on_press(move || tap_sink.set(true))
.on_alt_press(move |b| sink.set(Some(b)));
settle(&mut card);
card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
card.on_event(&release_with(50.0, 50.0, PointerButton::Primary));
assert_eq!(
seen.take(),
None,
"the right button armed it, the left cannot complete it"
);
assert!(!tapped.get());
}
#[test]
fn dragging_off_the_box_cancels_an_alt_press() {
let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
let sink = seen.clone();
let mut card = laid_out_box().on_alt_press(move |b| sink.set(Some(b)));
settle(&mut card);
card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
card.on_event(&Event::PointerMoved {
x: 95.0,
y: 95.0,
source: PointerSource::Mouse,
});
card.on_event(&release_with(95.0, 95.0, PointerButton::Secondary));
assert_eq!(
seen.take(),
None,
"travel past the tap slop cancels an alt press just as it cancels a tap"
);
}
#[test]
fn inner_button_press_wins_over_box() {
let card_flag = Rc::new(Cell::new(false));
let btn_flag = Rc::new(Cell::new(false));
let cf = card_flag.clone();
let bf = btn_flag.clone();
reset_layout_runtime();
let btn = StyledContainer::new(
LayoutStyle::new().width(50.0).height(30.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_press(move || bf.set(true));
let btn_node = btn.layout_node();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default(),
vec![Box::new(btn)],
)
.unwrap()
.on_press(move || cf.set(true));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
let br = track_layout(btn_node).unwrap().get();
let (cx, cy) = (
(br.x + br.width / 2.0) as f64,
(br.y + br.height / 2.0) as f64,
);
card.on_event(&press(cx, cy, PointerSource::Mouse));
card.on_event(&release(cx, cy, PointerSource::Mouse));
assert!(btn_flag.get(), "the inner button should fire");
assert!(
!card_flag.get(),
"the box on_press must not fire when a child handled the press"
);
}
#[test]
fn hover_style_swaps_on_mouse_move() {
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
vec![],
)
.unwrap()
.on_hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
let normal = fill_color(&card.view());
card.on_event(&Event::PointerMoved {
x: 100.0,
y: 50.0,
source: PointerSource::Mouse,
});
let hovered = fill_color(&card.view());
assert_ne!(normal, hovered, "hover should swap the fill");
card.on_event(&Event::PointerMoved {
x: 9999.0,
y: 9999.0,
source: PointerSource::Mouse,
});
assert_eq!(
fill_color(&card.view()),
normal,
"leaving the box restores the base fill"
);
}
#[test]
fn touch_move_does_not_set_hover() {
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
vec![],
)
.unwrap()
.on_hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
let normal = fill_color(&card.view());
card.on_event(&Event::PointerMoved {
x: 100.0,
y: 50.0,
source: PointerSource::Touch { id: 1 },
});
assert_eq!(
fill_color(&card.view()),
normal,
"a touch move must not trigger hover"
);
}
#[test]
fn active_style_swaps_on_press_and_clears_on_release() {
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
vec![],
)
.unwrap()
.on_active_style(|_r| RectStyle::default().with_fill(Color::rgba(0.5, 0.5, 0.5, 1.0)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
let normal = fill_color(&card.view());
card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
assert_ne!(
normal,
fill_color(&card.view()),
"press swaps to the active fill"
);
card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
assert_eq!(
fill_color(&card.view()),
normal,
"release restores the base fill"
);
}
#[test]
fn active_style_takes_precedence_over_hover() {
reset_layout_runtime();
let hover = Color::rgba(0.9, 0.9, 0.9, 1.0);
let active = Color::rgba(0.4, 0.4, 0.4, 1.0);
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
vec![],
)
.unwrap()
.on_hover_style(move |_r| RectStyle::default().with_fill(hover))
.on_active_style(move |_r| RectStyle::default().with_fill(active));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
card.on_event(&Event::PointerMoved {
x: 100.0,
y: 50.0,
source: PointerSource::Mouse,
});
assert_eq!(
fill_color(&card.view()),
hover,
"hovering shows the hover fill"
);
card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
assert_eq!(
fill_color(&card.view()),
active,
"pressing while hovered shows the active fill (precedence)"
);
card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
assert_eq!(
fill_color(&card.view()),
hover,
"releasing inside falls back to the hover fill"
);
}
#[test]
fn active_style_clears_when_press_drags_off() {
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(100.0),
|_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
vec![],
)
.unwrap()
.on_active_style(|_r| RectStyle::default().with_fill(Color::rgba(0.5, 0.5, 0.5, 1.0)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
let normal = fill_color(&card.view());
card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
assert_ne!(normal, fill_color(&card.view()), "press activates");
card.on_event(&Event::PointerMoved {
x: 9999.0,
y: 9999.0,
source: PointerSource::Mouse,
});
assert_eq!(
fill_color(&card.view()),
normal,
"dragging off the box clears the pressed state"
);
}
fn fill_color(view: &RenderNode) -> Color {
let group = match view {
RenderNode::Group { children, .. } => children,
_ => panic!("expected Group"),
};
if let RenderNode::Primitive(renderer_core::DrawCommand::Rect { style, .. }) = &group[0] {
if let Some(renderer_core::Paint::Solid(c)) = style.fill {
return c;
}
}
panic!("expected a solid-fill background rect");
}
#[test]
fn scroll_drag_does_not_press_box() {
let flag = Rc::new(Cell::new(false));
let f = flag.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(200.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_press(move || f.set(true));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(200.0),
)
.unwrap();
let touch = PointerSource::Touch { id: 1 };
card.on_event(&press(50.0, 20.0, touch.clone()));
card.on_event(&Event::PointerMoved {
x: 50.0,
y: 120.0, source: touch.clone(),
});
card.on_event(&release(50.0, 120.0, touch));
assert!(!flag.get(), "a scroll drag over the box must not press it");
}
#[test]
fn on_drag_reports_press_then_moves_until_release() {
use std::cell::RefCell;
let seen: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
let sink = seen.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(200.0).height(200.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_drag(move |x, y| sink.borrow_mut().push((x, y)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(200.0),
)
.unwrap();
let moved = |x: f64, y: f64| Event::PointerMoved {
x,
y,
source: PointerSource::Mouse,
};
card.on_event(&press(40.0, 40.0, PointerSource::Mouse));
card.on_event(&moved(80.0, 90.0));
card.on_event(&moved(400.0, 400.0)); card.on_event(&release(400.0, 400.0, PointerSource::Mouse));
card.on_event(&moved(10.0, 10.0));
assert_eq!(
*seen.borrow(),
vec![(40.0, 40.0), (80.0, 90.0), (400.0, 400.0)],
"drag reports the press point then each move until release"
);
}
#[test]
fn drag_released_outside_bounds_ends_via_parent_dispatch() {
use std::cell::RefCell;
let seen: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
let sink = seen.clone();
reset_layout_runtime();
let child = StyledContainer::new(
LayoutStyle::new().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_drag(move |x, y| sink.borrow_mut().push((x, y)));
let mut parent = Container::new(
LayoutStyle::new().flex_column().width(300.0).height(300.0),
vec![Box::new(child)],
)
.unwrap();
compute_layout(
parent.layout_node(),
AvailableSpace::Definite(300.0),
AvailableSpace::Definite(300.0),
)
.unwrap();
let moved = |x: f64, y: f64| Event::PointerMoved {
x,
y,
source: PointerSource::Mouse,
};
parent.on_event(&press(50.0, 50.0, PointerSource::Mouse));
parent.on_event(&moved(250.0, 250.0));
parent.on_event(&release(250.0, 250.0, PointerSource::Mouse));
parent.on_event(&moved(60.0, 60.0));
assert_eq!(
*seen.borrow(),
vec![(50.0, 50.0), (250.0, 250.0)],
"drag ended on the outside release; the post-release move must not fire"
);
}
#[test]
fn on_drag_end_fires_once_with_the_release_position() {
use std::cell::RefCell;
let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
let sink = ends.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
let moved = |x: f64, y: f64| Event::PointerMoved {
x,
y,
source: PointerSource::Mouse,
};
card.on_event(&moved(10.0, 10.0));
assert!(ends.borrow().is_empty(), "a move with no drag ends nothing");
card.on_event(&press(20.0, 20.0, PointerSource::Mouse));
card.on_event(&moved(70.0, 30.0));
assert!(ends.borrow().is_empty(), "still dragging");
card.on_event(&release(90.0, 40.0, PointerSource::Mouse));
assert_eq!(
*ends.borrow(),
vec![(90.0, 40.0)],
"the release position, not the last move — a drag can end past it"
);
card.on_event(&release(95.0, 45.0, PointerSource::Mouse));
assert_eq!(ends.borrow().len(), 1);
}
#[test]
fn on_drag_end_still_fires_when_the_cursor_leaves() {
use std::cell::RefCell;
let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
let sink = ends.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
card.on_event(&press(20.0, 20.0, PointerSource::Mouse));
card.on_event(&Event::PointerMoved {
x: 60.0,
y: 25.0,
source: PointerSource::Mouse,
});
card.on_event(&Event::CursorLeft);
assert_eq!(
*ends.borrow(),
vec![(60.0, 25.0)],
"the last position the drag reached"
);
}
#[test]
fn on_drag_end_works_without_an_on_drag() {
use std::cell::RefCell;
let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
let sink = ends.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
card.on_event(&press(10.0, 10.0, PointerSource::Mouse));
card.on_event(&release(80.0, 10.0, PointerSource::Mouse));
assert_eq!(*ends.borrow(), vec![(80.0, 10.0)]);
}
#[test]
fn on_focus_fires_on_gain_and_loss() {
use std::cell::RefCell;
let seen: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
let sink = seen.clone();
reset_layout_runtime();
let mut card = StyledContainer::new(
LayoutStyle::new().flex_column().width(100.0).height(100.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_focus(move |f| sink.borrow_mut().push(f));
compute_layout(
card.layout_node(),
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
card.on_event(&press(50.0, 50.0, PointerSource::Mouse)); crate::focus::clear(); assert_eq!(
*seen.borrow(),
vec![true, false],
"on_focus fires true on gain then false on loss"
);
}
#[test]
fn pressable_publishes_rect_to_interactive_registry_and_withdraws_on_drop() {
use crate::interactive_rects;
reset_layout_runtime();
let baseline = interactive_rects().len();
let card = StyledContainer::new(
LayoutStyle::new().width(120.0).height(40.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_press(|| {});
let node = card.layout_node();
assert_eq!(
interactive_rects().len(),
baseline,
"an unlaid-out pressable contributes no rect"
);
compute_layout(
node,
AvailableSpace::Definite(120.0),
AvailableSpace::Definite(40.0),
)
.unwrap();
let rects = interactive_rects();
assert_eq!(rects.len(), baseline + 1);
assert!(
rects.iter().any(|r| r.width == 120.0 && r.height == 40.0),
"a laid-out pressable reports its rect"
);
drop(card);
assert_eq!(
interactive_rects().len(),
baseline,
"dropping the pressable withdraws its rect"
);
}
}