use crate::element::{BoxStyle, Element};
use std::collections::{HashMap, HashSet};
use stipple_geometry::{Point, Rect};
use stipple_render::Color;
use stipple_style::Theme;
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, f64)>;
type TextPosFn<S> = Box<dyn FnMut(&mut S, usize, bool)>;
type ContextFn<S> = Box<dyn FnMut(&mut S, Point)>;
#[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 ScrollId(pub(crate) u32);
#[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,
}
#[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,
Enter,
Escape,
}
#[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>>,
next_scroll: u32,
overlays: Vec<OverlaySpec>,
memo: HashMap<u64, Element>,
memo_used: HashSet<u64>,
}
impl<'a, S> Cx<'a, S> {
pub fn new(theme: &'a Theme) -> Self {
Self {
theme,
taps: Vec::new(),
keys: Vec::new(),
drags: Vec::new(),
text_pos: Vec::new(),
contexts: Vec::new(),
next_scroll: 0,
overlays: Vec::new(),
memo: HashMap::new(),
memo_used: HashSet::new(),
}
}
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, handler: impl FnMut(&mut S, f64) + '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_scroll(&mut self) -> ScrollId {
let id = ScrollId(self.next_scroll);
self.next_scroll += 1;
id
}
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,
}
}
}
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>>,
}
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 {
if let Some(handler) = self.drags.get_mut(id.0 as usize) {
handler(state, fraction);
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 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(),
}
}
}
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,
},
Viewport(ViewportId),
}
#[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 caret: Option<usize>,
pub selection: Option<(usize, usize)>,
pub text_pos: Option<TextPosId>,
pub wrap: bool,
pub scroll: Option<ScrollId>,
pub clip: 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,
caret: None,
selection: None,
text_pos: None,
wrap: false,
scroll: None,
clip: false,
children,
}
}
}
pub fn hit_test(node: &LayoutNode, point: Point) -> Option<ActionId> {
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 context_at(node: &LayoutNode, point: Point) -> Option<ContextId> {
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 text_pos_at(node: &LayoutNode, point: Point) -> Option<(TextPosId, &LayoutNode)> {
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)),
_ => None,
}
}
pub fn focus_at(node: &LayoutNode, point: Point) -> Option<FocusId> {
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)> {
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 scroll_at(node: &LayoutNode, point: Point) -> Option<ScrollId> {
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)> {
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::*;
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,
caret: None,
selection: None,
text_pos: None,
wrap: false,
scroll: None,
clip: 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,
caret: None,
selection: None,
text_pos: None,
wrap: false,
scroll: None,
clip: 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 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)));
}
}