yakui_core/layout/
mod.rs

1//! Defines yakui's layout protocol and Layout DOM.
2
3use std::collections::VecDeque;
4
5use glam::Vec2;
6use thunderdome::Arena;
7
8use crate::dom::Dom;
9use crate::event::EventInterest;
10use crate::geometry::{Constraints, Rect};
11use crate::id::WidgetId;
12use crate::input::{InputState, MouseInterest};
13use crate::widget::LayoutContext;
14
15/// Contains information on how each widget in the DOM is laid out and what
16/// events they're interested in.
17#[derive(Debug)]
18pub struct LayoutDom {
19    nodes: Arena<LayoutDomNode>,
20    clip_stack: Vec<WidgetId>,
21
22    unscaled_viewport: Rect,
23    scale_factor: f32,
24
25    pub(crate) interest_mouse: MouseInterest,
26}
27
28/// A node in a [`LayoutDom`].
29#[derive(Debug)]
30#[non_exhaustive]
31pub struct LayoutDomNode {
32    /// The bounding rectangle of the node in logical pixels.
33    pub rect: Rect,
34
35    /// This node will clip its descendants to its bounding rectangle.
36    pub clipping_enabled: bool,
37
38    /// This node is the beginning of a new layer, and all of its descendants
39    /// should be hit tested and painted with higher priority.
40    pub new_layer: bool,
41
42    /// This node is clipped to the region defined by the given node.
43    pub clipped_by: Option<WidgetId>,
44
45    /// What events the widget reported interest in.
46    pub event_interest: EventInterest,
47}
48
49impl LayoutDom {
50    /// Create an empty `LayoutDom`.
51    pub fn new() -> Self {
52        Self {
53            nodes: Arena::new(),
54            clip_stack: Vec::new(),
55
56            unscaled_viewport: Rect::ONE,
57            scale_factor: 1.0,
58
59            interest_mouse: MouseInterest::new(),
60        }
61    }
62
63    pub(crate) fn sync_removals(&mut self, removals: &[WidgetId]) {
64        for id in removals {
65            self.nodes.remove(id.index());
66        }
67    }
68
69    /// Get a widget's layout information.
70    pub fn get(&self, id: WidgetId) -> Option<&LayoutDomNode> {
71        self.nodes.get(id.index())
72    }
73
74    /// Get a mutable reference to a widget's layout information.
75    pub fn get_mut(&mut self, id: WidgetId) -> Option<&mut LayoutDomNode> {
76        self.nodes.get_mut(id.index())
77    }
78
79    /// Set the viewport of the DOM in unscaled units.
80    pub fn set_unscaled_viewport(&mut self, view: Rect) {
81        self.unscaled_viewport = view;
82    }
83
84    /// Set the scale factor to use for layout.
85    pub fn set_scale_factor(&mut self, scale: f32) {
86        self.scale_factor = scale;
87    }
88
89    /// Get the currently active scale factor.
90    pub fn scale_factor(&self) -> f32 {
91        self.scale_factor
92    }
93
94    /// Get the viewport in scaled units.
95    pub fn viewport(&self) -> Rect {
96        Rect::from_pos_size(
97            self.unscaled_viewport.pos() / self.scale_factor,
98            self.unscaled_viewport.size() / self.scale_factor,
99        )
100    }
101
102    /// Get the viewport in unscaled units.
103    pub fn unscaled_viewport(&self) -> Rect {
104        self.unscaled_viewport
105    }
106
107    /// Tells how many nodes are currently in the `LayoutDom`.
108    pub fn len(&self) -> usize {
109        self.nodes.len()
110    }
111
112    /// Tells whether the `LayoutDom` is currently empty.
113    pub fn is_empty(&self) -> bool {
114        self.nodes.is_empty()
115    }
116
117    /// Calculate the layout of all elements in the given DOM.
118    pub fn calculate_all(&mut self, dom: &Dom, input: &InputState) {
119        profiling::scope!("LayoutDom::calculate_all");
120        log::debug!("LayoutDom::calculate_all()");
121
122        self.clip_stack.clear();
123        self.interest_mouse.clear();
124
125        let constraints = Constraints::tight(self.viewport().size());
126
127        self.calculate(dom, input, dom.root(), constraints);
128        self.resolve_positions(dom);
129    }
130
131    /// Calculate the layout of a specific widget.
132    ///
133    /// This function must only be called from
134    /// [`Widget::layout`][crate::widget::Widget::layout] and should only be
135    /// called once per widget per layout pass.
136    pub fn calculate(
137        &mut self,
138        dom: &Dom,
139        input: &InputState,
140        id: WidgetId,
141        constraints: Constraints,
142    ) -> Vec2 {
143        dom.enter(id);
144        let dom_node = dom.get(id).unwrap();
145
146        let context = LayoutContext {
147            dom,
148            input,
149            layout: self,
150        };
151
152        let size = dom_node.widget.layout(context, constraints);
153
154        // If the widget called new_layer() during layout, it will be on top of
155        // the mouse interest layer stack.
156        let new_layer = self.interest_mouse.current_layer_root() == Some(id);
157
158        // Mouse interest will be registered into the layout created by the
159        // widget if there is one.
160        let event_interest = dom_node.widget.event_interest();
161        if event_interest.intersects(EventInterest::MOUSE_ALL) {
162            self.interest_mouse.insert(id, event_interest);
163        }
164
165        // If the widget created a new layer, we're done with it now, so it's
166        // time to clean it up.
167        if new_layer {
168            self.interest_mouse.pop_layer();
169        }
170
171        // If the widget called enable_clipping() during layout, it will be on
172        // top of the clip stack at this point.
173        let clipping_enabled = self.clip_stack.last() == Some(&id);
174
175        // If this node enabled clipping, the next node under that is the node
176        // that clips this one.
177        let clipped_by = if clipping_enabled {
178            self.clip_stack.iter().nth_back(2).copied()
179        } else {
180            self.clip_stack.last().copied()
181        };
182
183        self.nodes.insert_at(
184            id.index(),
185            LayoutDomNode {
186                rect: Rect::from_pos_size(Vec2::ZERO, size),
187                clipping_enabled,
188                new_layer,
189                clipped_by,
190                event_interest,
191            },
192        );
193
194        if clipping_enabled {
195            self.clip_stack.pop();
196        }
197
198        dom.exit(id);
199        size
200    }
201
202    /// Enables clipping for the currently active widget.
203    pub fn enable_clipping(&mut self, dom: &Dom) {
204        self.clip_stack.push(dom.current());
205    }
206
207    /// Put this widget and its children into a new layer.
208    pub fn new_layer(&mut self, dom: &Dom) {
209        self.interest_mouse.push_layer(dom.current());
210    }
211
212    /// Set the position of a widget.
213    pub fn set_pos(&mut self, id: WidgetId, pos: Vec2) {
214        if let Some(node) = self.nodes.get_mut(id.index()) {
215            node.rect.set_pos(pos);
216        }
217    }
218
219    fn resolve_positions(&mut self, dom: &Dom) {
220        let mut queue = VecDeque::new();
221
222        queue.push_back((dom.root(), Vec2::ZERO));
223
224        while let Some((id, parent_pos)) = queue.pop_front() {
225            if let Some(layout_node) = self.nodes.get_mut(id.index()) {
226                let node = dom.get(id).unwrap();
227                layout_node
228                    .rect
229                    .set_pos(layout_node.rect.pos() + parent_pos);
230
231                queue.extend(node.children.iter().map(|&id| (id, layout_node.rect.pos())));
232            }
233        }
234    }
235}