use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use crate::core::element::{Element, ElementKind};
use crate::core::runtime::{ActionId, DragId, DragSourceId, DropId, FocusId, LayoutNode};
use crate::core::transition::{AnimProp, Transition, Transitions};
use crate::geometry::{Insets, Point, Transform};
use crate::platform::CursorIcon;
use crate::render::Color;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Interaction(u16);
impl Interaction {
pub const NONE: Self = Self(0);
pub const HOVERED: Self = Self(1);
pub const PRESSED: Self = Self(1 << 1);
pub const FOCUSED: Self = Self(1 << 2);
pub const FOCUS_VISIBLE: Self = Self(1 << 3);
pub const DISABLED: Self = Self(1 << 4);
pub const SELECTED: Self = Self(1 << 5);
pub const DRAGGED: Self = Self(1 << 6);
pub const DRAG_OVER: Self = Self(1 << 7);
pub const DRAG_REFUSED: Self = Self(1 << 8);
pub(crate) const DYNAMIC: Self = Self(
Self::HOVERED.0
| Self::PRESSED.0
| Self::FOCUSED.0
| Self::FOCUS_VISIBLE.0
| Self::DRAGGED.0
| Self::DRAG_OVER.0
| Self::DRAG_REFUSED.0,
);
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
pub const fn intersects(self, other: Self) -> bool {
self.0 & other.0 != 0
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub const fn hovered(self) -> bool {
self.contains(Self::HOVERED)
}
pub const fn pressed(self) -> bool {
self.contains(Self::PRESSED)
}
pub const fn focused(self) -> bool {
self.contains(Self::FOCUSED)
}
pub const fn focus_visible(self) -> bool {
self.contains(Self::FOCUS_VISIBLE)
}
pub const fn disabled(self) -> bool {
self.contains(Self::DISABLED)
}
pub const fn selected(self) -> bool {
self.contains(Self::SELECTED)
}
pub const fn dragged(self) -> bool {
self.contains(Self::DRAGGED)
}
pub const fn drag_hover(self) -> bool {
self.intersects(Self(Self::DRAG_OVER.0 | Self::DRAG_REFUSED.0))
}
pub const fn drag_accepted(self) -> bool {
self.contains(Self::DRAG_OVER)
}
}
impl std::ops::BitOr for Interaction {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for Interaction {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl std::ops::BitAnd for Interaction {
type Output = Self;
fn bitand(self, rhs: Self) -> Self {
Self(self.0 & rhs.0)
}
}
impl std::fmt::Debug for Interaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const NAMES: [&str; 9] = [
"HOVERED",
"PRESSED",
"FOCUSED",
"FOCUS_VISIBLE",
"DISABLED",
"SELECTED",
"DRAGGED",
"DRAG_OVER",
"DRAG_REFUSED",
];
let names: Vec<&str> = NAMES
.iter()
.enumerate()
.filter(|(i, _)| self.0 & (1 << i) != 0)
.map(|(_, n)| *n)
.collect();
write!(f, "Interaction({})", names.join(" | "))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ElementKey(pub(crate) u64);
impl ElementKey {
pub fn new(key: impl Hash) -> Self {
let mut h = std::hash::DefaultHasher::new();
0x6b65_795fu32.hash(&mut h);
key.hash(&mut h);
Self(h.finish())
}
pub(crate) fn child(parent: ElementKey, index: usize) -> Self {
let mut h = std::hash::DefaultHasher::new();
parent.0.hash(&mut h);
index.hash(&mut h);
Self(h.finish())
}
pub(crate) fn root(layer: usize) -> Self {
Self::child(Self(0x726f_6f74), layer)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct StylePatch {
fill: Option<Option<Color>>,
radius: Option<f64>,
border: Option<Option<(Color, f64)>>,
padding: Option<Insets>,
width: Option<Option<f64>>,
height: Option<Option<f64>>,
text_color: Option<Color>,
cursor: Option<Option<CursorIcon>>,
opacity: Option<f32>,
transform: Option<Option<Transform>>,
}
fn text_color(e: &Element) -> Option<Color> {
match &e.kind {
ElementKind::Text { color, .. } => Some(*color),
_ => None,
}
}
impl StylePatch {
pub(crate) fn diff(before: &Element, after: &Element) -> Self {
fn changed<T: PartialEq + Copy>(a: T, b: T) -> Option<T> {
(a != b).then_some(b)
}
Self {
fill: changed(before.decoration.fill, after.decoration.fill),
radius: changed(before.decoration.radius, after.decoration.radius),
border: changed(before.decoration.border, after.decoration.border),
padding: changed(before.layout.padding, after.layout.padding),
width: changed(before.layout.size.width, after.layout.size.width),
height: changed(before.layout.size.height, after.layout.size.height),
text_color: changed(text_color(before), text_color(after)).flatten(),
cursor: changed(before.cursor, after.cursor),
opacity: changed(before.opacity, after.opacity),
transform: changed(before.transform, after.transform),
}
}
pub(crate) fn apply(&self, e: &mut Element) {
if let Some(v) = self.fill {
e.decoration.fill = v;
}
if let Some(v) = self.radius {
e.decoration.radius = v;
}
if let Some(v) = self.border {
e.decoration.border = v;
}
if let Some(v) = self.padding {
e.layout.padding = v;
}
if let Some(v) = self.width {
e.layout.size.width = v;
}
if let Some(v) = self.height {
e.layout.size.height = v;
}
if let (Some(v), ElementKind::Text { color, .. }) = (self.text_color, &mut e.kind) {
*color = v;
}
if let Some(v) = self.cursor {
e.cursor = v;
}
if let Some(v) = self.opacity {
e.opacity = v;
}
if let Some(v) = self.transform {
e.transform = v;
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Reactive {
pub(crate) key: Option<ElementKey>,
pub(crate) id: Option<ElementKey>,
pub(crate) disabled: bool,
pub(crate) overrides: Vec<(Interaction, StylePatch)>,
pub(crate) transitions: Vec<(Option<AnimProp>, Transition)>,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Inputs {
pub pointer: Option<Point>,
pub press: Option<Point>,
pub focused: Option<FocusId>,
pub focus_visible: bool,
pub drag_source: Option<DragSourceId>,
pub drag: Option<DragId>,
pub drop_over: Option<(DropId, bool)>,
}
pub(crate) fn collect(tree: &LayoutNode, inputs: &Inputs) -> HashMap<ElementKey, Interaction> {
fn walk(
node: &LayoutNode,
inputs: &Inputs,
pointer: Option<Point>,
press: Option<Point>,
out: &mut HashMap<ElementKey, Interaction>,
) -> Interaction {
let local = |p: Option<Point>| {
p.and_then(|p| node.to_local(p))
.filter(|p| node.bounds.contains(*p))
};
let (pointer, press) = (local(pointer), local(press));
let (hovered, pressed) = (pointer.is_some(), press.is_some());
let top = |p: Option<Point>| {
p.and_then(|p| {
node.children
.iter()
.rposition(|c| c.to_local(p).is_some_and(|q| c.bounds.contains(q)))
})
};
let (hover_child, press_child) = (top(pointer), top(press));
let mut within = Interaction::NONE;
if node.focus.is_some() && node.focus == inputs.focused {
within |= Interaction::FOCUSED;
if inputs.focus_visible {
within |= Interaction::FOCUS_VISIBLE;
}
}
if (node.drag_source.is_some() && node.drag_source == inputs.drag_source)
|| (node.drag.is_some() && node.drag == inputs.drag)
{
within |= Interaction::DRAGGED;
}
if let (Some(id), Some((over, accepted))) = (node.drop, inputs.drop_over)
&& id == over
{
within |= if accepted {
Interaction::DRAG_OVER
} else {
Interaction::DRAG_REFUSED
};
}
for (i, child) in node.children.iter().enumerate() {
within |= walk(
child,
inputs,
pointer.filter(|_| hover_child == Some(i)),
press.filter(|_| press_child == Some(i)),
out,
);
}
if let Some(key) = node.key {
let mut states = within;
if hovered {
states |= Interaction::HOVERED;
if pressed {
states |= Interaction::PRESSED;
}
}
*out.entry(key).or_default() |= states;
}
within
}
let mut out = HashMap::new();
walk(tree, inputs, inputs.pointer, inputs.press, &mut out);
out
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Watch {
entries: Vec<(ElementKey, Interaction, Interaction)>,
}
impl Watch {
pub(crate) fn add(&mut self, id: ElementKey, mask: Interaction, states: Interaction) {
let mask = mask & Interaction::DYNAMIC;
if !mask.is_empty() {
self.entries.push((id, mask, states & mask));
}
}
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub(crate) fn changed(&self, now: &HashMap<ElementKey, Interaction>) -> bool {
self.entries
.iter()
.any(|&(id, mask, was)| now.get(&id).copied().unwrap_or_default() & mask != was)
}
}
pub(crate) struct Resolver<'a> {
pub states: &'a HashMap<ElementKey, Interaction>,
pub transitions: &'a mut Transitions,
pub watch: Watch,
pub styled_actions: Vec<ActionId>,
pub styled_drops: Vec<DropId>,
}
impl Resolver<'_> {
pub(crate) fn resolve(&mut self, el: &mut Element, id: ElementKey) {
let id = el.reactive.as_ref().and_then(|r| r.key).unwrap_or(id);
if let Some(mut re) = el.reactive.take() {
re.id = Some(id);
let mut states = self.states.get(&id).copied().unwrap_or_default();
if re.disabled {
states |= Interaction::DISABLED;
}
if el.semantics.as_ref().is_some_and(|s| s.selected) {
states |= Interaction::SELECTED;
}
let mask = re
.overrides
.iter()
.fold(Interaction::NONE, |m, (when, _)| m | *when);
self.watch.add(id, mask, states);
if mask.intersects(Interaction::HOVERED | Interaction::PRESSED)
&& let Some(a) = el.action
{
self.styled_actions.push(a);
}
if mask.intersects(Interaction::DRAG_OVER | Interaction::DRAG_REFUSED)
&& let Some(d) = el.drop
{
self.styled_drops.push(d);
}
for (when, patch) in &re.overrides {
if states.contains(*when) {
patch.apply(el);
}
}
if !re.transitions.is_empty() {
self.transitions.animate(id, el, &re.transitions);
}
el.reactive = Some(re);
}
if let Some(children) = el.children_mut() {
for (i, child) in children.iter_mut().enumerate() {
self.resolve(child, ElementKey::child(id, i));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::layout;
use crate::geometry::Rect;
use crate::layout::Axis;
fn tree(el: &mut Element, states: &HashMap<ElementKey, Interaction>) -> (LayoutNode, Watch) {
let mut transitions = Transitions::default();
let mut r = Resolver {
states,
transitions: &mut transitions,
watch: Watch::default(),
styled_actions: Vec::new(),
styled_drops: Vec::new(),
};
r.resolve(el, ElementKey::root(0));
let watch = std::mem::take(&mut r.watch);
(
layout(el, Rect::from_xywh(0.0, 0.0, 200.0, 100.0), None),
watch,
)
}
fn card(key: &str) -> Element {
Element::stack(Axis::Vertical, vec![])
.width(50.0)
.height(50.0)
.fill(Color::rgb(0, 0, 0))
.hovered(|e| e.fill(Color::rgb(255, 0, 0)))
.key(key)
}
#[test]
fn hover_follows_the_pointer_and_restyles() {
let view = || Element::stack(Axis::Horizontal, vec![card("a"), card("b")]);
let (laid, watch) = tree(&mut view(), &HashMap::new());
let (a, b) = (ElementKey::new("a"), ElementKey::new("b"));
let inputs = Inputs {
pointer: Some(Point::new(60.0, 10.0)),
..Inputs::default()
};
let states = collect(&laid, &inputs);
assert!(states[&b].hovered() && !states[&a].hovered());
assert!(watch.changed(&states), "b's look depends on it");
let (laid, watch) = tree(&mut view(), &states);
assert_eq!(
laid.children[1].decoration.fill,
Some(Color::rgb(255, 0, 0))
);
assert_eq!(laid.children[0].decoration.fill, Some(Color::rgb(0, 0, 0)));
assert!(!watch.changed(&collect(&laid, &inputs)), "stable");
}
#[test]
fn unwatched_states_do_not_rebuild() {
let el = || {
Element::stack(Axis::Vertical, vec![])
.width(50.0)
.height(50.0)
.drag_over(|e| e.fill(Color::rgb(0, 0, 255)))
};
let (laid, watch) = tree(&mut el(), &HashMap::new());
let hover = Inputs {
pointer: Some(Point::new(10.0, 10.0)),
..Inputs::default()
};
assert!(collect(&laid, &hover).values().any(|s| s.hovered()));
assert!(!watch.changed(&collect(&laid, &hover)));
}
#[test]
fn untouched_elements_keep_their_position_identity() {
let view = |n: usize| {
let mut kids: Vec<Element> = (0..n)
.map(|_| Element::stack(Axis::Vertical, vec![]).width(10.0))
.collect();
kids.push(Element::stack(Axis::Vertical, vec![]).pressed(|e| e.radius(4.0)));
Element::stack(Axis::Horizontal, kids)
};
let id = |el: &Element, i: usize| match &el.kind {
ElementKind::Stack { children, .. } => children[i].reactive.as_ref().unwrap().id,
_ => None,
};
let (mut a, mut b) = (view(1), view(1));
tree(&mut a, &HashMap::new());
tree(&mut b, &HashMap::new());
assert_eq!(id(&a, 1), id(&b, 1));
let mut c = view(2);
tree(&mut c, &HashMap::new());
assert_ne!(id(&a, 1), id(&c, 2), "a new position is a new element");
}
#[test]
fn press_focus_and_drag_states() {
let el = Element::stack(
Axis::Vertical,
vec![
Element::stack(Axis::Vertical, vec![])
.width(40.0)
.height(40.0)
.key("inner"),
],
)
.key("outer");
let mut laid = layout(&el, Rect::from_xywh(0.0, 0.0, 100.0, 100.0), None);
laid.key = Some(ElementKey::new("outer"));
laid.children[0].key = Some(ElementKey::new("inner"));
laid.children[0].focus = Some(FocusId(0));
laid.children[0].drop = Some(DropId(3));
let inputs = Inputs {
pointer: Some(Point::new(10.0, 10.0)),
press: Some(Point::new(5.0, 5.0)),
focused: Some(FocusId(0)),
focus_visible: true,
drop_over: Some((DropId(3), false)),
..Inputs::default()
};
let s = collect(&laid, &inputs);
let inner = s[&ElementKey::new("inner")];
assert!(inner.pressed() && inner.focus_visible() && inner.drag_hover());
assert!(!inner.drag_accepted() && inner.contains(Interaction::DRAG_REFUSED));
let outer = s[&ElementKey::new("outer")];
assert!(outer.focused(), "focus within counts");
let off = Inputs {
pointer: Some(Point::new(80.0, 80.0)),
..inputs
};
assert!(!collect(&laid, &off)[&ElementKey::new("inner")].pressed());
}
#[test]
fn overrides_compose_in_order_and_need_all_their_states() {
let mut el = Element::text("x", 12.0, Color::rgb(0, 0, 0))
.hovered(|e| e.padding(Insets::uniform(2.0)))
.when(Interaction::HOVERED | Interaction::PRESSED, |e| {
e.padding(Insets::uniform(4.0))
})
.disabled(true)
.when(Interaction::DISABLED, |e| {
Element::text("ignored", 1.0, Color::rgb(9, 9, 9)).padding(e.layout.padding)
})
.key("t");
let states = HashMap::from([(ElementKey::new("t"), Interaction::HOVERED)]);
let (laid, _) = tree(&mut el, &states);
assert_eq!(laid.decoration, Default::default());
let crate::core::NodeContent::Text { color, size, .. } = laid.content else {
panic!("text");
};
assert_eq!(color, Color::rgb(9, 9, 9), "the disabled color");
assert_eq!(size, 12.0, "an override changes style, not content");
assert_eq!(
el.layout.padding,
Insets::uniform(2.0),
"hovered, not pressed"
);
}
}