Skip to main content

nifty_runtime/
layout.rs

1//! Flexbox layout over native widgets: Taffy computes frames, the caller applies them.
2
3use std::collections::{HashMap, HashSet};
4
5use taffy::{
6    AlignItems, AlignSelf, AvailableSpace, Dimension, Display, FlexDirection, JustifyContent,
7    LayoutInput, LayoutOutput, LengthPercentage, LengthPercentageAuto, NodeId, Overflow, Rect,
8    Style, TaffyTree, TraversePartialTree, compute_leaf_layout, style_helpers::length,
9};
10
11use crate::geometry::{Frame, Point, Size};
12use crate::style::{Align, Axis, Justify, StackStyle};
13
14const LIVE_NODE: &str = "layout nodes are only used until they are removed";
15
16type TaffySize = taffy::Size<f32>;
17
18/// Within this distance of the top of the content area, a surface counts as touching it.
19const TOP_EDGE_TOLERANCE: f32 = 0.5;
20
21/// Where layout puts a widget.
22pub(crate) enum Placed<'a, W> {
23    /// The widget's frame, relative to its parent's content.
24    Frame(&'a W, Frame),
25    /// The size of the content a scroll area scrolls; its children are placed in it.
26    ScrollContent(&'a W, Size),
27}
28
29/// One tree for every window: each window's content hangs off its own root node.
30/// A node's context is its natural size, present only on widget leaves.
31pub(crate) struct Layout<W> {
32    tree: TaffyTree<TaffySize>,
33    widgets: HashMap<NodeId, W>,
34    /// Stacks whose widget is the surface drawn behind their children.
35    surfaces: HashSet<NodeId>,
36    /// Stacks that scroll, and the scroll area that shows each one's children.
37    scrolls: HashMap<NodeId, W>,
38    /// Widgets that cover their stack's content, outside its layout.
39    overlays: HashSet<NodeId>,
40}
41
42impl<W> Layout<W> {
43    pub(crate) fn new() -> Self {
44        Self {
45            tree: TaffyTree::new(),
46            widgets: HashMap::new(),
47            surfaces: HashSet::new(),
48            scrolls: HashMap::new(),
49            overlays: HashSet::new(),
50        }
51    }
52
53    pub(crate) fn add_root(&mut self) -> NodeId {
54        self.tree
55            .new_leaf(root_style(Some(Size::ZERO), 0.0))
56            .expect(LIVE_NODE)
57    }
58
59    pub(crate) fn add_widget(&mut self, widget: W, natural: Size) -> NodeId {
60        self.add_leaf(widget, natural, Style::default())
61    }
62
63    /// A divider stretches across its parent's cross axis and stays one point thick.
64    pub(crate) fn add_divider(&mut self, widget: W, natural: Size) -> NodeId {
65        let style = Style {
66            align_self: Some(AlignSelf::STRETCH),
67            flex_shrink: 0.0,
68            ..Style::default()
69        };
70        self.add_leaf(widget, natural, style)
71    }
72
73    /// A widget that covers its whole stack, padding included, and takes no part in how
74    /// the stack lays out its other children: Taffy skips it, and `place` gives it the
75    /// stack's content frame.
76    pub(crate) fn add_overlay(&mut self, widget: W, natural: Size) -> NodeId {
77        let style = Style {
78            display: Display::None,
79            ..Style::default()
80        };
81        let node = self.add_leaf(widget, natural, style);
82        self.overlays.insert(node);
83        node
84    }
85
86    /// Empty space that takes whatever room its stack has left.
87    pub(crate) fn add_spacer(&mut self) -> NodeId {
88        let style = Style {
89            flex_grow: 1.0,
90            ..Style::default()
91        };
92        self.tree.new_leaf(style).expect(LIVE_NODE)
93    }
94
95    /// A stack, with the surface widget drawn behind its children if it has one. Stacks
96    /// size from their children, so a surface needs no natural size.
97    pub(crate) fn add_stack(&mut self, style: &StackStyle, surface: Option<W>) -> NodeId {
98        let node = self.tree.new_leaf(stack_style(style)).expect(LIVE_NODE);
99        if let Some(surface) = surface {
100            self.widgets.insert(node, surface);
101            self.surfaces.insert(node);
102        }
103        node
104    }
105
106    /// A stack shown in `scroll`, whose children scroll up and down when they are taller
107    /// than it, with its scroll bar taking `bar_width` beside them. It takes the space its
108    /// parent has left, and adds nothing to its parent's size, so a parent sized by its
109    /// content leaves it none.
110    pub(crate) fn add_scroll(
111        &mut self,
112        style: &StackStyle,
113        surface: Option<W>,
114        scroll: W,
115        bar_width: f32,
116    ) -> NodeId {
117        let node = self.add_stack(style, surface);
118        let style = Style {
119            overflow: taffy::Point {
120                x: Overflow::Visible,
121                y: Overflow::Scroll,
122            },
123            scrollbar_width: bar_width,
124            flex_grow: 1.0,
125            flex_basis: Dimension::length(0.0),
126            min_size: taffy::Size {
127                width: LengthPercentageAuto::auto(),
128                height: LengthPercentageAuto::length(0.0),
129            },
130            ..stack_style(style)
131        };
132        self.tree.set_style(node, style).expect(LIVE_NODE);
133        self.scrolls.insert(node, scroll);
134        node
135    }
136
137    /// Leaves `width` beside the content of every scroll area in the tree under `root`, for
138    /// its scroll bar.
139    pub(crate) fn set_scroll_bar_width(&mut self, root: NodeId, width: f32) {
140        if self.scrolls.contains_key(&root) {
141            let mut style = self.tree.style(root).expect(LIVE_NODE).clone();
142            style.scrollbar_width = width;
143            self.tree.set_style(root, style).expect(LIVE_NODE);
144        }
145        let children: Vec<NodeId> = self.tree.child_ids(root).collect();
146        for child in children {
147            self.set_scroll_bar_width(child, width);
148        }
149    }
150
151    /// The scroll area whose content `node` is in, counting a scroll's own children as in
152    /// it, or `None` when it is straight in the window.
153    pub(crate) fn scroll_around(&self, node: NodeId) -> Option<&W> {
154        let mut current = Some(node);
155        while let Some(node) = current {
156            if let Some(scroll) = self.scrolls.get(&node) {
157                return Some(scroll);
158            }
159            current = self.tree.parent(node);
160        }
161        None
162    }
163
164    fn add_leaf(&mut self, widget: W, natural: Size, style: Style) -> NodeId {
165        let node = self
166            .tree
167            .new_leaf_with_context(style, to_taffy(natural))
168            .expect(LIVE_NODE);
169        self.widgets.insert(node, widget);
170        node
171    }
172
173    pub(crate) fn insert(&mut self, parent: NodeId, index: usize, child: NodeId) {
174        self.tree
175            .insert_child_at_index(parent, index, child)
176            .expect(LIVE_NODE);
177    }
178
179    /// Makes `content` fill the window whose root is `root`, returning the content it
180    /// replaces, which is now detached and still holds its widgets.
181    pub(crate) fn set_content(&mut self, root: NodeId, content: NodeId) -> Option<NodeId> {
182        let previous = self.tree.child_ids(root).next();
183        let mut style = self.tree.style(content).expect(LIVE_NODE).clone();
184        style.flex_grow = 1.0;
185        self.tree.set_style(content, style).expect(LIVE_NODE);
186        self.tree.set_children(root, &[content]).expect(LIVE_NODE);
187        previous.filter(|&previous| previous != content)
188    }
189
190    /// Removes `node` and everything under it, handing back their widgets.
191    pub(crate) fn remove(&mut self, node: NodeId) -> Vec<W> {
192        let mut removed = Vec::new();
193        self.remove_into(node, &mut removed);
194        removed
195    }
196
197    fn remove_into(&mut self, node: NodeId, removed: &mut Vec<W>) {
198        let children: Vec<NodeId> = self.tree.child_ids(node).collect();
199        for child in children {
200            self.remove_into(child, removed);
201        }
202        self.tree.remove(node).expect(LIVE_NODE);
203        self.surfaces.remove(&node);
204        self.overlays.remove(&node);
205        removed.extend(self.widgets.remove(&node));
206        removed.extend(self.scrolls.remove(&node));
207    }
208
209    pub(crate) fn widget(&self, node: NodeId) -> Option<&W> {
210        self.widgets.get(&node)
211    }
212
213    /// Records a widget's new natural size; returns whether layout is now out of date.
214    pub(crate) fn set_natural(&mut self, node: NodeId, natural: Size) -> bool {
215        let natural = to_taffy(natural);
216        let current = self
217            .tree
218            .get_node_context_mut(node)
219            .expect("only widget nodes have a natural size");
220        if *current == natural {
221            return false;
222        }
223        *current = natural;
224        self.tree.mark_dirty(node).expect(LIVE_NODE);
225        true
226    }
227
228    /// The narrowest the window rooted at `root` can be without clipping its content.
229    pub(crate) fn min_width(&mut self, root: NodeId, title_bar_inset: f32) -> f32 {
230        self.tree
231            .set_style(root, root_style(None, title_bar_inset))
232            .expect(LIVE_NODE);
233        let min_content = taffy::Size {
234            width: AvailableSpace::MinContent,
235            height: AvailableSpace::MinContent,
236        };
237        self.tree
238            .compute_layout_with_measure(root, min_content, measure_leaf)
239            .expect(LIVE_NODE);
240        self.tree.layout(root).expect(LIVE_NODE).size.width
241    }
242
243    /// Lays out the window rooted at `root` and reports where each widget goes. Content
244    /// starts below the title bar; a surface whose top touches it reaches up under the
245    /// title bar, while its children stay below.
246    pub(crate) fn layout_window(
247        &mut self,
248        root: NodeId,
249        viewport: Size,
250        title_bar_inset: f32,
251        mut apply: impl FnMut(Placed<'_, W>),
252    ) {
253        self.tree
254            .set_style(root, root_style(Some(viewport), title_bar_inset))
255            .expect(LIVE_NODE);
256        let available = taffy::Size {
257            width: AvailableSpace::Definite(viewport.width),
258            height: AvailableSpace::Definite(viewport.height),
259        };
260        self.tree
261            .compute_layout_with_measure(root, available, measure_leaf)
262            .expect(LIVE_NODE);
263        self.place(root, Point::default(), Some(title_bar_inset), &mut apply);
264    }
265
266    /// Native widgets are flat children of the window or of a scroll area's content, so
267    /// frames are made relative to those here. Inside a scroll area there is no title bar
268    /// to reach under.
269    fn place(
270        &self,
271        node: NodeId,
272        parent_origin: Point,
273        title_bar_inset: Option<f32>,
274        apply: &mut impl FnMut(Placed<'_, W>),
275    ) {
276        let layout = self.tree.layout(node).expect(LIVE_NODE);
277        let origin = Point {
278            x: parent_origin.x + layout.location.x,
279            y: parent_origin.y + layout.location.y,
280        };
281        let frame = Frame {
282            origin,
283            size: Size::new(layout.size.width, layout.size.height),
284        };
285        if let Some(widget) = self.widgets.get(&node) {
286            let reaches_title_bar = self.surfaces.contains(&node)
287                && title_bar_inset
288                    .is_some_and(|inset| (origin.y - inset).abs() < TOP_EDGE_TOLERANCE);
289            let frame = if reaches_title_bar {
290                under_title_bar(frame)
291            } else {
292                frame
293            };
294            apply(Placed::Frame(widget, frame));
295        }
296        let (content, title_bar_inset) = match self.scrolls.get(&node) {
297            Some(scroll) => {
298                apply(Placed::Frame(scroll, frame));
299                let size = Size::new(
300                    layout.size.width - layout.scrollbar_size.width,
301                    layout.size.height + layout.scroll_height(),
302                );
303                apply(Placed::ScrollContent(scroll, size));
304                let content = Frame {
305                    origin: Point::default(),
306                    size,
307                };
308                (content, None)
309            }
310            None => (frame, title_bar_inset),
311        };
312        for child in self.tree.child_ids(node) {
313            match self
314                .widgets
315                .get(&child)
316                .filter(|_| self.overlays.contains(&child))
317            {
318                Some(overlay) => apply(Placed::Frame(overlay, content)),
319                None => self.place(child, content.origin, title_bar_inset, apply),
320            }
321        }
322    }
323}
324
325/// `frame`, stretched up to the top of the window.
326fn under_title_bar(frame: Frame) -> Frame {
327    Frame {
328        origin: Point {
329            x: frame.origin.x,
330            y: 0.0,
331        },
332        size: Size::new(frame.size.width, frame.size.height + frame.origin.y),
333    }
334}
335
336fn measure_leaf(
337    inputs: LayoutInput,
338    _node: NodeId,
339    natural: Option<&mut TaffySize>,
340    style: &Style,
341) -> LayoutOutput {
342    let natural = natural.map_or(TaffySize::ZERO, |natural| *natural);
343    compute_leaf_layout(
344        inputs,
345        style,
346        |_, _| 0.0,
347        |known, _available| known.unwrap_or(natural),
348    )
349}
350
351/// The window's root fills `viewport`, or fits its content when there is none, and keeps
352/// content out from under the title bar.
353fn root_style(viewport: Option<Size>, title_bar_inset: f32) -> Style {
354    Style {
355        display: Display::Flex,
356        flex_direction: FlexDirection::Column,
357        size: viewport.map_or(taffy::Size::auto(), |viewport| taffy::Size {
358            width: length(viewport.width),
359            height: length(viewport.height),
360        }),
361        padding: Rect {
362            top: LengthPercentage::length(title_bar_inset),
363            ..Rect::zero()
364        },
365        ..Style::default()
366    }
367}
368
369fn stack_style(stack: &StackStyle) -> Style {
370    Style {
371        display: Display::Flex,
372        flex_direction: match stack.axis {
373            Axis::Horizontal => FlexDirection::Row,
374            Axis::Vertical => FlexDirection::Column,
375        },
376        gap: taffy::Size::length(stack.spacing),
377        padding: Rect::length(stack.padding),
378        align_items: match stack.align {
379            Align::Stretch => AlignItems::STRETCH,
380            Align::Start => AlignItems::START,
381            Align::Center => AlignItems::CENTER,
382            Align::End => AlignItems::END,
383        },
384        justify_content: match stack.justify {
385            Justify::Start => JustifyContent::START,
386            Justify::Center => JustifyContent::CENTER,
387            Justify::End => JustifyContent::END,
388            Justify::SpaceBetween => JustifyContent::SPACE_BETWEEN,
389        },
390        flex_grow: if stack.grow { 1.0 } else { 0.0 },
391        size: taffy::Size {
392            width: stack_width(stack),
393            height: Dimension::auto(),
394        },
395        max_size: taffy::Size {
396            width: stack
397                .width_limit()
398                .map_or(LengthPercentageAuto::auto(), LengthPercentageAuto::length),
399            height: LengthPercentageAuto::auto(),
400        },
401        ..Style::default()
402    }
403}
404
405/// A fixed width, or the full width on offer when there is a limit, so a limited stack
406/// fills its parent up to the limit and can be centred in it; otherwise the content's width.
407fn stack_width(stack: &StackStyle) -> Dimension {
408    match (stack.fixed_width(), stack.width_limit()) {
409        (Some(width), _) => Dimension::length(width),
410        (None, Some(_)) => Dimension::percent(1.0),
411        (None, None) => Dimension::auto(),
412    }
413}
414
415fn to_taffy(size: Size) -> TaffySize {
416    TaffySize {
417        width: size.width,
418        height: size.height,
419    }
420}