From 577e97908defb6cdadee3fbfd1112e2c1482a584 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 15 Nov 2023 07:53:31 -0800 Subject: [PATCH] Optimizing widgets at point Given the goal of this function, I'm not sure it can get more optimal than this even with specialized data structures like KD-trees. The problem is that we want all widgets that are hovered, not just some, and that makes nearest neighbor useless. The main optimizations here are simple: - Group up all the render data we need in a single vec to help cache. - Precompute the rect's extents to make the contains check at most 4 comparisons. This had a noticable effect on the "wiggle the mouse frantically" performance, where Gooey isn't actually repainting but is routing mouse events. --- src/tree.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/src/tree.rs b/src/tree.rs index 3d23f82..5a25dd5 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -103,7 +103,7 @@ impl Tree { pub(crate) fn new_frame(&self, invalidations: impl IntoIterator) { let mut data = self.data.lock().ignore_poison(); - data.render_order.clear(); + data.render_info.clear(); for id in invalidations { let Some(id) = data.nodes_by_id.get(&id).copied() else { @@ -116,7 +116,10 @@ impl Tree { pub(crate) fn note_widget_rendered(&self, widget: LotId) { let mut data = self.data.lock().ignore_poison(); - data.render_order.push(widget); + let Some(layout) = data.nodes.get(widget).and_then(|node| node.layout) else { + return; + }; + data.render_info.push(widget, layout); } pub(crate) fn begin_layout( @@ -307,15 +310,7 @@ impl Tree { pub(crate) fn widgets_at_point(&self, point: Point) -> Vec { let data = self.data.lock().ignore_poison(); - let mut hits = Vec::new(); - for id in data.render_order.iter().rev() { - if let Some(last_rendered) = data.nodes.get(*id).and_then(|widget| widget.layout) { - if last_rendered.contains(point) { - hits.push(data.widget_from_node(*id, self).expect("just accessed")); - } - } - } - hits + data.render_info.widgets_under_point(point, &data, self) } pub(crate) fn parent(&self, id: LotId) -> Option { @@ -373,7 +368,7 @@ struct TreeData { hover: Option, defaults: Vec, escapes: Vec, - render_order: Vec, + render_info: RenderInfo, previous_focuses: AHashMap, } @@ -496,6 +491,62 @@ impl TreeData { } } +#[derive(Default)] +struct RenderInfo { + order: Vec, +} + +impl RenderInfo { + pub fn push(&mut self, node: LotId, region: Rect) { + let area = RenderArea::new(node, region); + self.order.push(area); + } + + pub fn clear(&mut self) { + self.order.clear(); + } + + fn widgets_under_point( + &self, + point: Point, + tree_data: &TreeData, + tree: &Tree, + ) -> Vec { + // We pessimistically allocate a vector as if all widgets match, up to a + // reasonable limit. This should ensure minimal allocations in all but + // extreme circumstances where widgets are nested with a significant + // amount of depth. + let mut hits = Vec::with_capacity(self.order.len().min(256)); + for area in self.order.iter().rev() { + if area.min.x <= point.x + && area.min.y <= point.y + && area.max.x >= point.x + && area.max.y >= point.y + { + let Some(widget) = tree_data.widget_from_node(area.node, tree) else { + continue; + }; + hits.push(widget); + } + } + hits + } +} + +#[derive(Eq, PartialEq, Clone, Copy)] +struct RenderArea { + node: LotId, + min: Point, + max: Point, +} + +impl RenderArea { + fn new(node: LotId, area: Rect) -> Self { + let (min, max) = area.extents(); + Self { node, min, max } + } +} + struct Node { widget: WidgetInstance, children: Vec,