use crate::core::a11y::Semantics;
use crate::core::element::{Align, BoxStyle, Element};
use crate::core::handle::AppHandle;
use crate::geometry::{Point, Rect};
use crate::platform::{CursorIcon, DragOffer, DropData};
use crate::render::Color;
use crate::style::Theme;
use std::collections::{HashMap, HashSet};
type TapFn<S> = Box<dyn FnMut(&mut S)>;
type KeyFn<S> = Box<dyn FnMut(&mut S, &KeyInput)>;
type DragFn<S> = Box<dyn FnMut(&mut S, &DragMotion)>;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DragMotion {
pub position: Point,
pub origin: Point,
pub bounds: Rect,
pub parent: Rect,
}
impl DragMotion {
pub fn fraction_x(&self) -> f64 {
if self.bounds.width() > 0.0 {
((self.position.x - self.bounds.min_x()) / self.bounds.width()).clamp(0.0, 1.0)
} else {
0.0
}
}
pub fn delta(&self) -> (f64, f64) {
(
self.position.x - self.origin.x,
self.position.y - self.origin.y,
)
}
pub fn mirrored(&self) -> DragMotion {
let axis = self.parent.min_x() + self.parent.max_x();
let point = |p: Point| Point::new(axis - p.x, p.y);
let rect = |r: Rect| Rect::from_xywh(axis - r.max_x(), r.min_y(), r.width(), r.height());
DragMotion {
position: point(self.position),
origin: point(self.origin),
bounds: rect(self.bounds),
parent: rect(self.parent),
}
}
}
type TextPosFn<S> = Box<dyn FnMut(&mut S, usize, bool)>;
type ContextFn<S> = Box<dyn FnMut(&mut S, Point)>;
type DropFn<S> = Box<dyn FnMut(&mut S, &DropData, Point)>;
type DropHoverFn<S> = Box<dyn FnMut(&mut S, bool)>;
type DragSourceFn<S> = Box<dyn FnMut(&mut S) -> Option<DropData>>;
struct DropTarget<S> {
accepts: DragOffer,
on_drop: DropFn<S>,
on_hover: Option<DropHoverFn<S>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ActionId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FocusId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DragId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TextPosId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ContextId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DropId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DragSourceId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ScrollId(pub(crate) u32);
#[derive(Clone, Debug, Default)]
pub struct ScrollRequests(std::rc::Rc<std::cell::RefCell<Vec<(ScrollId, f64)>>>);
impl ScrollRequests {
pub fn scroll_to(&self, id: ScrollId, offset: f64) {
self.0.borrow_mut().push((id, offset));
}
pub fn take(&self) -> Vec<(ScrollId, f64)> {
std::mem::take(&mut self.0.borrow_mut())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ViewportId(pub u32);
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Anchor {
At(Point),
Center,
Below(ActionId),
Popup(Point),
RightOf(ActionId),
Aligned {
horizontal: Align,
vertical: Align,
margin: f64,
},
}
#[derive(Clone, Debug)]
pub struct OverlaySpec {
pub content: Element,
pub anchor: Anchor,
pub modal: bool,
pub dismiss: Option<ActionId>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum KeyInput {
Text(String),
Backspace,
Delete,
Left,
Right,
Up,
Down,
Home,
End,
SelectLeft,
SelectRight,
SelectUp,
SelectDown,
SelectHome,
SelectEnd,
SelectAll,
Copy,
Cut,
Paste,
WordLeft,
WordRight,
SelectWordLeft,
SelectWordRight,
DeleteWordBackward,
DeleteWordForward,
SelectWord,
SelectLine,
Undo,
Redo,
Enter,
Escape,
Preedit {
text: String,
cursor: Option<(usize, usize)>,
},
DeleteSurrounding {
before: usize,
after: usize,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum ViewportEvent {
PointerDown { local: Point, button: u8 },
PointerUp { local: Point, button: u8 },
PointerMove { local: Point },
Wheel { local: Point, delta_y: f64 },
Key(KeyInput),
}
pub struct Cx<'a, S> {
theme: &'a Theme,
taps: Vec<TapFn<S>>,
keys: Vec<KeyFn<S>>,
drags: Vec<DragFn<S>>,
text_pos: Vec<TextPosFn<S>>,
contexts: Vec<ContextFn<S>>,
drops: Vec<DropTarget<S>>,
drag_sources: Vec<DragSourceFn<S>>,
drop_states: HashMap<DropId, crate::core::dnd::DropState>,
drop_queried: Vec<DropId>,
next_scroll: u32,
overlays: Vec<OverlaySpec>,
context_menus: Vec<(ContextId, crate::core::menu::Menu<S>)>,
memo: HashMap<u64, Element>,
memo_used: HashSet<u64>,
scroll_offsets: HashMap<ScrollId, f64>,
scroll_requests: ScrollRequests,
handle: AppHandle<S>,
frame: FrameInfo,
}
#[derive(Clone, Copy, Debug, Default)]
struct FrameInfo {
time: std::time::Duration,
requested: bool,
}
impl<'a, S> Cx<'a, S> {
pub fn new(theme: &'a Theme) -> Self {
Self::with_handle(theme, AppHandle::new())
}
pub fn with_handle(theme: &'a Theme, handle: AppHandle<S>) -> Self {
Self {
handle,
theme,
taps: Vec::new(),
keys: Vec::new(),
drags: Vec::new(),
text_pos: Vec::new(),
contexts: Vec::new(),
drops: Vec::new(),
drag_sources: Vec::new(),
drop_states: HashMap::new(),
drop_queried: Vec::new(),
next_scroll: 0,
overlays: Vec::new(),
context_menus: Vec::new(),
memo: HashMap::new(),
memo_used: HashSet::new(),
scroll_offsets: HashMap::new(),
scroll_requests: ScrollRequests::default(),
frame: FrameInfo {
time: crate::platform::frame::elapsed().unwrap_or_default(),
requested: false,
},
}
}
pub fn frame_time(&self) -> std::time::Duration {
self.frame.time
}
pub fn request_frame(&mut self) {
self.frame.requested = true;
}
pub fn frame_requested(&self) -> bool {
self.frame.requested
}
pub fn handle(&self) -> AppHandle<S> {
self.handle.clone()
}
pub fn theme(&self) -> &Theme {
self.theme
}
pub fn register(&mut self, handler: impl FnMut(&mut S) + 'static) -> ActionId {
let id = ActionId(self.taps.len() as u32);
self.taps.push(Box::new(handler));
id
}
pub fn register_key(&mut self, handler: impl FnMut(&mut S, &KeyInput) + 'static) -> FocusId {
let id = FocusId(self.keys.len() as u32);
self.keys.push(Box::new(handler));
id
}
pub fn register_drag(&mut self, mut handler: impl FnMut(&mut S, f64) + 'static) -> DragId {
self.register_drag_motion(move |s, m| handler(s, m.fraction_x()))
}
pub fn register_drag_motion(
&mut self,
handler: impl FnMut(&mut S, &DragMotion) + 'static,
) -> DragId {
let id = DragId(self.drags.len() as u32);
self.drags.push(Box::new(handler));
id
}
pub fn register_text_pos(
&mut self,
handler: impl FnMut(&mut S, usize, bool) + 'static,
) -> TextPosId {
let id = TextPosId(self.text_pos.len() as u32);
self.text_pos.push(Box::new(handler));
id
}
pub fn register_context(&mut self, handler: impl FnMut(&mut S, Point) + 'static) -> ContextId {
let id = ContextId(self.contexts.len() as u32);
self.contexts.push(Box::new(handler));
id
}
pub fn register_drop(
&mut self,
accepts: DragOffer,
handler: impl FnMut(&mut S, &DropData, Point) + 'static,
) -> DropId {
let id = DropId(self.drops.len() as u32);
self.drops.push(DropTarget {
accepts,
on_drop: Box::new(handler),
on_hover: None,
});
id
}
pub fn set_drop_hover(&mut self, id: DropId, handler: impl FnMut(&mut S, bool) + 'static) {
if let Some(target) = self.drops.get_mut(id.0 as usize) {
target.on_hover = Some(Box::new(handler));
}
}
pub fn drop_state(&mut self, id: DropId) -> crate::core::dnd::DropState {
if !self.drop_queried.contains(&id) {
self.drop_queried.push(id);
}
self.drop_states.get(&id).copied().unwrap_or_default()
}
pub fn set_drop_states(&mut self, states: HashMap<DropId, crate::core::dnd::DropState>) {
self.drop_states = states;
}
pub fn take_drop_queried(&mut self) -> Vec<DropId> {
std::mem::take(&mut self.drop_queried)
}
pub fn register_drag_source(
&mut self,
handler: impl FnMut(&mut S) -> Option<DropData> + 'static,
) -> DragSourceId {
let id = DragSourceId(self.drag_sources.len() as u32);
self.drag_sources.push(Box::new(handler));
id
}
pub fn register_context_menu(&mut self, menu: crate::core::menu::Menu<S>) -> ContextId {
let id = self.register_context(|_, _| {});
self.context_menus.push((id, menu));
id
}
pub fn take_context_menus(&mut self) -> Vec<(ContextId, crate::core::menu::Menu<S>)> {
std::mem::take(&mut self.context_menus)
}
pub fn register_scroll(&mut self) -> ScrollId {
let id = ScrollId(self.next_scroll);
self.next_scroll += 1;
id
}
pub fn scroll_offset(&self, id: ScrollId) -> f64 {
self.scroll_offsets
.get(&id)
.copied()
.unwrap_or(0.0)
.max(0.0)
}
pub fn scroll_requests(&self) -> ScrollRequests {
self.scroll_requests.clone()
}
pub fn set_scroll_state(&mut self, offsets: HashMap<ScrollId, f64>, requests: ScrollRequests) {
self.scroll_offsets = offsets;
self.scroll_requests = requests;
}
pub fn overlay(&mut self, spec: OverlaySpec) {
self.overlays.push(spec);
}
pub fn take_overlays(&mut self) -> Vec<OverlaySpec> {
std::mem::take(&mut self.overlays)
}
pub fn memo(&mut self, key: u64, build: impl FnOnce() -> Element) -> Element {
self.memo_used.insert(key);
if let Some(cached) = self.memo.get(&key) {
return cached.clone();
}
let element = build();
self.memo.insert(key, element.clone());
element
}
pub fn set_memo_cache(&mut self, cache: HashMap<u64, Element>) {
self.memo = cache;
self.memo_used.clear();
}
pub fn take_memo_cache(&mut self) -> HashMap<u64, Element> {
let used = std::mem::take(&mut self.memo_used);
let mut cache = std::mem::take(&mut self.memo);
cache.retain(|k, _| used.contains(k));
cache
}
pub fn into_handlers(self) -> Handlers<S> {
Handlers {
taps: self.taps,
keys: self.keys,
drags: self.drags,
text_pos: self.text_pos,
contexts: self.contexts,
drops: self.drops,
drag_sources: self.drag_sources,
}
}
}
impl<S> core::fmt::Debug for Cx<'_, S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Cx")
.field("taps", &self.taps.len())
.field("keys", &self.keys.len())
.field("drags", &self.drags.len())
.finish_non_exhaustive()
}
}
pub struct Handlers<S> {
taps: Vec<TapFn<S>>,
keys: Vec<KeyFn<S>>,
drags: Vec<DragFn<S>>,
text_pos: Vec<TextPosFn<S>>,
contexts: Vec<ContextFn<S>>,
drops: Vec<DropTarget<S>>,
drag_sources: Vec<DragSourceFn<S>>,
}
impl<S> Handlers<S> {
pub fn dispatch(&mut self, id: ActionId, state: &mut S) -> bool {
if let Some(handler) = self.taps.get_mut(id.0 as usize) {
handler(state);
true
} else {
false
}
}
pub fn dispatch_key(&mut self, id: FocusId, input: &KeyInput, state: &mut S) -> bool {
if let Some(handler) = self.keys.get_mut(id.0 as usize) {
handler(state, input);
true
} else {
false
}
}
pub fn dispatch_drag(&mut self, id: DragId, fraction: f64, state: &mut S) -> bool {
let unit = Rect::from_xywh(0.0, 0.0, 1.0, 1.0);
let at = Point::new(fraction, 0.0);
let motion = DragMotion {
position: at,
origin: at,
bounds: unit,
parent: unit,
};
self.dispatch_drag_motion(id, &motion, state)
}
pub fn dispatch_drag_motion(&mut self, id: DragId, motion: &DragMotion, state: &mut S) -> bool {
if let Some(handler) = self.drags.get_mut(id.0 as usize) {
handler(state, motion);
true
} else {
false
}
}
pub fn dispatch_text_pos(
&mut self,
id: TextPosId,
index: usize,
extend: bool,
state: &mut S,
) -> bool {
if let Some(handler) = self.text_pos.get_mut(id.0 as usize) {
handler(state, index, extend);
true
} else {
false
}
}
pub fn dispatch_context(&mut self, id: ContextId, pos: Point, state: &mut S) -> bool {
if let Some(handler) = self.contexts.get_mut(id.0 as usize) {
handler(state, pos);
true
} else {
false
}
}
pub fn drop_accepts(&self, id: DropId, offer: DragOffer) -> bool {
self.drops
.get(id.0 as usize)
.is_some_and(|t| t.accepts.accepts(offer))
}
pub fn dispatch_drop(
&mut self,
id: DropId,
data: &DropData,
pos: Point,
state: &mut S,
) -> bool {
match self.drops.get_mut(id.0 as usize) {
Some(target) => {
(target.on_drop)(state, data, pos);
true
}
None => false,
}
}
pub fn dispatch_drop_hover(&mut self, id: DropId, hovering: bool, state: &mut S) -> bool {
match self
.drops
.get_mut(id.0 as usize)
.and_then(|t| t.on_hover.as_mut())
{
Some(handler) => {
handler(state, hovering);
true
}
None => false,
}
}
pub fn dispatch_drag_source(&mut self, id: DragSourceId, state: &mut S) -> Option<DropData> {
self.drag_sources.get_mut(id.0 as usize)?(state)
}
pub fn len(&self) -> usize {
self.taps.len()
+ self.keys.len()
+ self.drags.len()
+ self.text_pos.len()
+ self.contexts.len()
}
pub fn is_empty(&self) -> bool {
self.taps.is_empty()
&& self.keys.is_empty()
&& self.drags.is_empty()
&& self.text_pos.is_empty()
&& self.contexts.is_empty()
}
}
impl<S> Default for Handlers<S> {
fn default() -> Self {
Self {
taps: Vec::new(),
keys: Vec::new(),
drags: Vec::new(),
text_pos: Vec::new(),
contexts: Vec::new(),
drops: Vec::new(),
drag_sources: Vec::new(),
}
}
}
impl<S> core::fmt::Debug for Handlers<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Handlers")
.field("taps", &self.taps.len())
.field("keys", &self.keys.len())
.field("drags", &self.drags.len())
.field("text_pos", &self.text_pos.len())
.finish()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum NodeContent {
#[default]
None,
Text {
text: String,
size: f64,
color: Color,
direction: crate::render::TextDirection,
underline: Option<(usize, usize)>,
},
Viewport(ViewportId),
Image {
source: crate::render::ImageSource,
fit: crate::render::ImageFit,
padding: crate::geometry::Insets,
},
}
#[derive(Clone, Debug)]
pub struct LayoutNode {
pub bounds: Rect,
pub decoration: BoxStyle,
pub content: NodeContent,
pub action: Option<ActionId>,
pub focus: Option<FocusId>,
pub drag: Option<DragId>,
pub context: Option<ContextId>,
pub drop: Option<DropId>,
pub drag_source: Option<DragSourceId>,
pub caret: Option<usize>,
pub selection: Option<(usize, usize)>,
pub preedit: Option<(usize, usize)>,
pub text_pos: Option<TextPosId>,
pub wrap: bool,
pub native: Option<crate::platform::native::NativeDeco>,
pub scroll: Option<ScrollId>,
pub clip: bool,
pub cursor: Option<CursorIcon>,
pub semantics: Option<Semantics>,
pub rtl: bool,
pub children: Vec<LayoutNode>,
}
impl LayoutNode {
pub fn container(bounds: Rect, children: Vec<LayoutNode>) -> LayoutNode {
LayoutNode {
bounds,
decoration: BoxStyle::default(),
content: NodeContent::None,
action: None,
focus: None,
drag: None,
context: None,
drop: None,
drag_source: None,
caret: None,
selection: None,
preedit: None,
text_pos: None,
wrap: false,
native: None,
scroll: None,
clip: false,
cursor: None,
semantics: None,
rtl: false,
children,
}
}
}
fn clipped_out(node: &LayoutNode, point: Point) -> bool {
node.clip && !node.bounds.contains(point)
}
pub fn hit_test(node: &LayoutNode, point: Point) -> Option<ActionId> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(id) = hit_test(child, point) {
return Some(id);
}
}
if node.action.is_some() && node.bounds.contains(point) {
node.action
} else {
None
}
}
pub fn cursor_at(node: &LayoutNode, point: Point) -> CursorIcon {
fn find(node: &LayoutNode, point: Point) -> Option<CursorIcon> {
let inside = node.bounds.contains(point);
if node.clip && !inside {
return None;
}
for child in node.children.iter().rev() {
if let Some(c) = find(child, point) {
return Some(c);
}
}
if !inside {
return None;
}
node.cursor.or(if node.text_pos.is_some() {
Some(CursorIcon::Text)
} else if node.action.is_some() || node.drag.is_some() {
Some(CursorIcon::Pointer)
} else {
None
})
}
find(node, point).unwrap_or_default()
}
pub fn context_at(node: &LayoutNode, point: Point) -> Option<ContextId> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(id) = context_at(child, point) {
return Some(id);
}
}
if node.context.is_some() && node.bounds.contains(point) {
node.context
} else {
None
}
}
pub fn drop_at(node: &LayoutNode, point: Point) -> Option<(DropId, Rect)> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(hit) = drop_at(child, point) {
return Some(hit);
}
}
match node.drop {
Some(id) if node.bounds.contains(point) => Some((id, node.bounds)),
_ => None,
}
}
pub fn find_drop(node: &LayoutNode, id: DropId) -> Option<&LayoutNode> {
if node.drop == Some(id) {
return Some(node);
}
node.children.iter().find_map(|c| find_drop(c, id))
}
pub fn drag_source_at(node: &LayoutNode, point: Point) -> Option<DragSourceId> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(id) = drag_source_at(child, point) {
return Some(id);
}
}
match node.drag_source {
Some(id) if node.bounds.contains(point) => Some(id),
_ => None,
}
}
pub fn text_pos_at(node: &LayoutNode, point: Point) -> Option<(TextPosId, &LayoutNode)> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(hit) = text_pos_at(child, point) {
return Some(hit);
}
}
match node.text_pos {
Some(id) if node.bounds.contains(point) => Some((id, node)),
_ if node.focus.is_some() && node.bounds.contains(point) => first_text_pos(node),
_ => None,
}
}
fn first_text_pos(node: &LayoutNode) -> Option<(TextPosId, &LayoutNode)> {
if let Some(id) = node.text_pos {
return Some((id, node));
}
node.children.iter().find_map(first_text_pos)
}
pub fn focus_at(node: &LayoutNode, point: Point) -> Option<FocusId> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(id) = focus_at(child, point) {
return Some(id);
}
}
if node.focus.is_some() && node.bounds.contains(point) {
node.focus
} else {
None
}
}
pub fn drag_at(node: &LayoutNode, point: Point) -> Option<(DragId, Rect)> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(hit) = drag_at(child, point) {
return Some(hit);
}
}
match node.drag {
Some(id) if node.bounds.contains(point) => Some((id, node.bounds)),
_ => None,
}
}
pub fn drag_hit_at(node: &LayoutNode, point: Point) -> Option<(DragId, Rect, Rect, bool)> {
fn walk(node: &LayoutNode, parent: Rect, point: Point) -> Option<(DragId, Rect, Rect, bool)> {
for child in node.children.iter().rev() {
if let Some(hit) = walk(child, node.bounds, point) {
return Some(hit);
}
}
match node.drag {
Some(id) if node.bounds.contains(point) => Some((id, node.bounds, parent, node.rtl)),
_ => None,
}
}
walk(node, node.bounds, point)
}
pub fn scroll_at(node: &LayoutNode, point: Point) -> Option<ScrollId> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(id) = scroll_at(child, point) {
return Some(id);
}
}
match node.scroll {
Some(id) if node.bounds.contains(point) => Some(id),
_ => None,
}
}
pub fn find_scroll(node: &LayoutNode, id: ScrollId) -> Option<&LayoutNode> {
if node.scroll == Some(id) {
return Some(node);
}
node.children.iter().find_map(|c| find_scroll(c, id))
}
pub fn find_focus(node: &LayoutNode, id: FocusId) -> Option<&LayoutNode> {
if node.focus == Some(id) {
return Some(node);
}
node.children.iter().find_map(|c| find_focus(c, id))
}
pub fn find_action(node: &LayoutNode, id: ActionId) -> Option<&LayoutNode> {
if node.action == Some(id) {
return Some(node);
}
node.children.iter().find_map(|c| find_action(c, id))
}
pub fn find_text_pos(node: &LayoutNode, id: TextPosId) -> Option<&LayoutNode> {
if node.text_pos == Some(id) {
return Some(node);
}
node.children.iter().find_map(|c| find_text_pos(c, id))
}
pub fn first_text(node: &LayoutNode) -> Option<&LayoutNode> {
if matches!(node.content, NodeContent::Text { .. }) {
return Some(node);
}
node.children.iter().find_map(first_text)
}
pub fn collect_viewports(node: &LayoutNode, out: &mut Vec<(ViewportId, Rect)>) {
if let NodeContent::Viewport(id) = node.content {
out.push((id, node.bounds));
}
for child in &node.children {
collect_viewports(child, out);
}
}
pub fn viewport_at(node: &LayoutNode, point: Point) -> Option<(ViewportId, Rect)> {
if clipped_out(node, point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(hit) = viewport_at(child, point) {
return Some(hit);
}
}
match node.content {
NodeContent::Viewport(id) if node.bounds.contains(point) => Some((id, node.bounds)),
_ => None,
}
}
pub fn collect_focusables(node: &LayoutNode, out: &mut Vec<FocusId>) {
if let Some(id) = node.focus {
out.push(id);
}
for child in &node.children {
collect_focusables(child, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_mirrored_drag_reads_from_the_right() {
let motion = DragMotion {
position: Point::new(130.0, 7.0),
origin: Point::new(120.0, 5.0),
bounds: Rect::from_xywh(100.0, 0.0, 100.0, 20.0),
parent: Rect::from_xywh(50.0, 0.0, 200.0, 20.0),
};
assert!((motion.fraction_x() - 0.3).abs() < 1e-9);
let m = motion.mirrored();
assert!((m.fraction_x() - 0.7).abs() < 1e-9);
assert_eq!(m.delta(), (-10.0, 2.0));
assert_eq!(m.parent, motion.parent);
assert_eq!(m.bounds, Rect::from_xywh(100.0, 0.0, 100.0, 20.0));
assert_eq!(m.mirrored(), motion);
}
fn leaf(bounds: Rect, action: Option<ActionId>, focus: Option<FocusId>) -> LayoutNode {
LayoutNode {
bounds,
decoration: BoxStyle::default(),
content: NodeContent::None,
action,
focus,
drag: None,
context: None,
drop: None,
drag_source: None,
caret: None,
selection: None,
preedit: None,
text_pos: None,
wrap: false,
native: None,
scroll: None,
clip: false,
cursor: None,
semantics: None,
rtl: false,
children: Vec::new(),
}
}
#[derive(Default)]
struct St {
n: i32,
s: String,
}
#[test]
fn dispatch_tap_and_key() {
let theme = Theme::light();
let mut cx = Cx::new(&theme);
let tap = cx.register(|st: &mut St| st.n += 5);
let focus = cx.register_key(|st: &mut St, k: &KeyInput| {
if let KeyInput::Text(t) = k {
st.s.push_str(t);
}
});
let mut handlers = cx.into_handlers();
let mut st = St::default();
assert!(handlers.dispatch(tap, &mut st));
assert_eq!(st.n, 5);
assert!(handlers.dispatch_key(focus, &KeyInput::Text("hi".into()), &mut st));
assert_eq!(st.s, "hi");
assert!(!handlers.dispatch_key(FocusId(99), &KeyInput::Backspace, &mut st));
}
#[test]
fn memo_skips_rebuild_for_unchanged_keys() {
use std::cell::Cell;
let theme = Theme::light();
let builds = Cell::new(0);
let make = |cx: &mut Cx<St>, key: u64| {
cx.memo(key, || {
builds.set(builds.get() + 1);
Element::text("static", 14.0, Color::BLACK)
})
};
let mut cache = std::collections::HashMap::new();
let mut cx = Cx::<St>::new(&theme);
cx.set_memo_cache(cache);
let _ = make(&mut cx, 1);
cache = cx.take_memo_cache();
assert_eq!(builds.get(), 1);
assert!(cache.contains_key(&1));
let mut cx = Cx::<St>::new(&theme);
cx.set_memo_cache(cache);
let _ = make(&mut cx, 1);
cache = cx.take_memo_cache();
assert_eq!(builds.get(), 1, "unchanged key must not rebuild");
let mut cx = Cx::<St>::new(&theme);
cx.set_memo_cache(cache);
let _ = make(&mut cx, 2);
cache = cx.take_memo_cache();
assert_eq!(builds.get(), 2, "changed key rebuilds");
assert!(cache.contains_key(&2));
assert!(!cache.contains_key(&1), "stale key should be evicted");
}
#[test]
fn hit_test_and_focus_prefer_topmost() {
let root = LayoutNode {
bounds: Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
decoration: BoxStyle::default(),
content: NodeContent::None,
action: Some(ActionId(0)),
focus: None,
drag: None,
context: None,
drop: None,
drag_source: None,
caret: None,
selection: None,
preedit: None,
text_pos: None,
wrap: false,
native: None,
scroll: None,
clip: false,
cursor: None,
semantics: None,
rtl: false,
children: vec![
leaf(
Rect::from_xywh(10.0, 10.0, 30.0, 30.0),
Some(ActionId(1)),
Some(FocusId(0)),
),
leaf(
Rect::from_xywh(20.0, 20.0, 30.0, 30.0),
Some(ActionId(2)),
Some(FocusId(1)),
),
],
};
assert_eq!(hit_test(&root, Point::new(25.0, 25.0)), Some(ActionId(2)));
assert_eq!(focus_at(&root, Point::new(12.0, 12.0)), Some(FocusId(0)));
let mut focusables = Vec::new();
collect_focusables(&root, &mut focusables);
assert_eq!(focusables, vec![FocusId(0), FocusId(1)]);
}
#[test]
fn cursor_follows_handlers_and_explicit_choice() {
let mut button = leaf(
Rect::from_xywh(0.0, 0.0, 50.0, 20.0),
Some(ActionId(0)),
None,
);
button
.children
.push(leaf(Rect::from_xywh(5.0, 5.0, 20.0, 10.0), None, None));
let mut field = leaf(
Rect::from_xywh(0.0, 30.0, 50.0, 20.0),
None,
Some(FocusId(0)),
);
field.text_pos = Some(TextPosId(0));
let mut splitter = leaf(
Rect::from_xywh(60.0, 0.0, 4.0, 50.0),
Some(ActionId(1)),
None,
);
splitter.cursor = Some(CursorIcon::EwResize);
let root = LayoutNode::container(
Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
vec![button, field, splitter],
);
assert_eq!(
cursor_at(&root, Point::new(10.0, 10.0)),
CursorIcon::Pointer
);
assert_eq!(cursor_at(&root, Point::new(10.0, 40.0)), CursorIcon::Text);
assert_eq!(
cursor_at(&root, Point::new(61.0, 10.0)),
CursorIcon::EwResize
);
assert_eq!(
cursor_at(&root, Point::new(90.0, 90.0)),
CursorIcon::Default
);
}
#[test]
fn clipped_children_cannot_be_hit_outside_the_clip() {
let mut row = leaf(
Rect::from_xywh(0.0, 0.0, 100.0, 40.0),
Some(ActionId(1)),
None,
);
row.drag = Some(DragId(0));
let mut body = leaf(Rect::from_xywh(0.0, 20.0, 100.0, 80.0), None, None);
body.clip = true;
body.children = vec![row];
let header = leaf(
Rect::from_xywh(0.0, 0.0, 100.0, 20.0),
Some(ActionId(0)),
None,
);
let root =
LayoutNode::container(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), vec![header, body]);
assert_eq!(hit_test(&root, Point::new(10.0, 10.0)), Some(ActionId(0)));
assert_eq!(hit_test(&root, Point::new(10.0, 30.0)), Some(ActionId(1)));
assert!(drag_at(&root, Point::new(10.0, 10.0)).is_none());
}
#[test]
fn context_handler_resolves_and_receives_the_click_point() {
struct St {
at: Option<Point>,
}
let theme = Theme::light();
let mut cx = Cx::<St>::new(&theme);
let id = cx.register_context(|s: &mut St, p: Point| s.at = Some(p));
let mut handlers = cx.into_handlers();
let mut node = leaf(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), None, None);
node.context = Some(id);
assert_eq!(context_at(&node, Point::new(50.0, 50.0)), Some(id));
assert_eq!(context_at(&node, Point::new(150.0, 50.0)), None);
let mut st = St { at: None };
assert!(handlers.dispatch_context(id, Point::new(12.0, 34.0), &mut st));
assert_eq!(st.at, Some(Point::new(12.0, 34.0)));
}
}