kas_core/layout/
sizer.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Layout solver
7
8use super::{AxisInfo, Margins, SizeRules};
9use crate::cast::Conv;
10use crate::event::ConfigCx;
11use crate::geom::{Rect, Size};
12use crate::layout::AlignHints;
13use crate::theme::SizeCx;
14use crate::util::WidgetHierarchy;
15use crate::{Node, Tile};
16use log::trace;
17
18/// A [`SizeRules`] solver for layouts
19///
20/// Typically, a solver is invoked twice, once for each axis, before the
21/// corresponding [`RulesSetter`] is invoked. This is managed by [`SolveCache`].
22///
23/// Implementations require access to storage able to persist between multiple
24/// solver runs and a subsequent setter run. This storage is of type
25/// [`RulesSolver::Storage`] and is passed via reference to the constructor.
26pub trait RulesSolver {
27    /// Type of storage
28    type Storage: Clone;
29
30    /// Type required by [`RulesSolver::for_child`] (see implementation documentation)
31    type ChildInfo;
32
33    /// Called once for each child. For most layouts the order is important.
34    fn for_child<CR: FnOnce(AxisInfo) -> SizeRules>(
35        &mut self,
36        storage: &mut Self::Storage,
37        child_info: Self::ChildInfo,
38        child_rules: CR,
39    );
40
41    /// Called at the end to output [`SizeRules`].
42    ///
43    /// Note that this does not include margins!
44    fn finish(self, storage: &mut Self::Storage) -> SizeRules;
45}
46
47/// Resolves a [`RulesSolver`] solution for each child
48pub trait RulesSetter {
49    /// Type of storage
50    type Storage: Clone;
51
52    /// Type required by [`RulesSolver::for_child`] (see implementation documentation)
53    type ChildInfo;
54
55    /// Called once for each child. The order is unimportant.
56    fn child_rect(&mut self, storage: &mut Self::Storage, child_info: Self::ChildInfo) -> Rect;
57
58    /// Calculates the maximal rect of a given child
59    ///
60    /// This assumes that all other entries have minimum size.
61    fn maximal_rect_of(&mut self, storage: &mut Self::Storage, index: Self::ChildInfo) -> Rect;
62}
63
64/// Solve size rules for a widget
65///
66/// Automatic layout solving requires that a widget's `size_rules` method is
67/// called for each axis before `set_rect`. This method simply calls
68/// `size_rules` on each axis.
69///
70/// If `size_rules` is not called, internal layout may be poor (depending on the
71/// widget). If widget content changes, it is recommended to call
72/// `solve_size_rules` and `set_rect` again.
73///
74/// Parameters `x_size` and `y_size` should be passed where this dimension is
75/// fixed and are used e.g. for text wrapping.
76pub fn solve_size_rules<W: Tile + ?Sized>(
77    widget: &mut W,
78    sizer: SizeCx,
79    x_size: Option<i32>,
80    y_size: Option<i32>,
81) {
82    trace!(
83        "solve_size_rules({}, _, {:?}, {:?})",
84        widget.identify(),
85        x_size,
86        y_size
87    );
88    widget.size_rules(sizer.re(), AxisInfo::new(false, y_size));
89    widget.size_rules(sizer.re(), AxisInfo::new(true, x_size));
90}
91
92/// Size solver
93///
94/// This struct is used to solve widget layout, read size constraints and
95/// cache the results until the next solver run.
96///
97/// [`SolveCache::find_constraints`] constructs an instance of this struct,
98/// solving for size constraints.
99///
100/// [`SolveCache::apply_rect`] accepts a [`Rect`], updates constraints as
101/// necessary and sets widget positions within this `rect`.
102pub struct SolveCache {
103    // Technically we don't need to store min and ideal here, but it simplifies
104    // the API for very little real cost.
105    min: Size,
106    ideal: Size,
107    margins: Margins,
108    refresh_rules: bool,
109    last_width: i32,
110}
111
112impl SolveCache {
113    /// Get the minimum size
114    ///
115    /// If `inner_margin` is true, margins are included in the result.
116    pub fn min(&self, inner_margin: bool) -> Size {
117        if inner_margin {
118            self.margins.pad(self.min)
119        } else {
120            self.min
121        }
122    }
123
124    /// Get the ideal size
125    ///
126    /// If `inner_margin` is true, margins are included in the result.
127    pub fn ideal(&self, inner_margin: bool) -> Size {
128        if inner_margin {
129            self.margins.pad(self.ideal)
130        } else {
131            self.ideal
132        }
133    }
134
135    /// Get the margins
136    pub fn margins(&self) -> Margins {
137        self.margins
138    }
139
140    /// Calculate required size of widget
141    ///
142    /// Assumes no explicit alignment.
143    pub fn find_constraints(mut widget: Node<'_>, sizer: SizeCx) -> Self {
144        let start = std::time::Instant::now();
145
146        let w = widget.size_rules(sizer.re(), AxisInfo::new(false, None));
147        let h = widget.size_rules(sizer.re(), AxisInfo::new(true, Some(w.ideal_size())));
148
149        let min = Size(w.min_size(), h.min_size());
150        let ideal = Size(w.ideal_size(), h.ideal_size());
151        let margins = Margins::hv(w.margins(), h.margins());
152
153        log::trace!(
154            target: "kas_perf::layout", "find_constraints: {}μs",
155            start.elapsed().as_micros(),
156        );
157        log::debug!("find_constraints: min={min:?}, ideal={ideal:?}, margins={margins:?}");
158        let refresh_rules = false;
159        let last_width = ideal.0;
160        SolveCache {
161            min,
162            ideal,
163            margins,
164            refresh_rules,
165            last_width,
166        }
167    }
168
169    /// Force updating of size rules
170    ///
171    /// This should be called whenever widget size rules have been changed. It
172    /// forces [`SolveCache::apply_rect`] to recompute these rules when next
173    /// called.
174    pub fn invalidate_rule_cache(&mut self) {
175        self.refresh_rules = true;
176    }
177
178    /// Apply layout solution to a widget
179    ///
180    /// The widget's layout is solved for the given `rect` and assigned.
181    /// If `inner_margin` is true, margins are internal to this `rect`; if not,
182    /// the caller is responsible for handling margins.
183    ///
184    /// If [`SolveCache::invalidate_rule_cache`] was called since rules were
185    /// last calculated then this method will recalculate all rules; otherwise
186    /// it will only do so if necessary (when dimensions do not match those
187    /// last used).
188    pub fn apply_rect(
189        &mut self,
190        mut widget: Node<'_>,
191        cx: &mut ConfigCx,
192        mut rect: Rect,
193        inner_margin: bool,
194    ) {
195        let start = std::time::Instant::now();
196
197        let mut width = rect.size.0;
198        if inner_margin {
199            width -= self.margins.sum_horiz();
200        }
201
202        // We call size_rules not because we want the result, but to allow
203        // internal layout solving.
204        if self.refresh_rules || width != self.last_width {
205            if self.refresh_rules {
206                let w = widget.size_rules(cx.size_cx(), AxisInfo::new(false, None));
207                self.min.0 = w.min_size();
208                self.ideal.0 = w.ideal_size();
209                self.margins.horiz = w.margins();
210                width = rect.size.0 - self.margins.sum_horiz();
211            }
212
213            let h = widget.size_rules(cx.size_cx(), AxisInfo::new(true, Some(width)));
214            self.min.1 = h.min_size();
215            self.ideal.1 = h.ideal_size();
216            self.margins.vert = h.margins();
217            self.last_width = width;
218        }
219
220        if inner_margin {
221            rect.pos += Size::conv((self.margins.horiz.0, self.margins.vert.0));
222            rect.size.0 = width;
223            rect.size.1 -= self.margins.sum_vert();
224        }
225        widget.set_rect(cx, rect, AlignHints::NONE);
226
227        log::trace!(target: "kas_perf::layout", "apply_rect: {}μs", start.elapsed().as_micros());
228        self.refresh_rules = false;
229    }
230
231    /// Print widget heirarchy in the trace log
232    ///
233    /// This is sometimes called after [`Self::apply_rect`].
234    pub fn print_widget_heirarchy(&mut self, widget: &dyn Tile) {
235        let rect = widget.rect();
236        let hier = WidgetHierarchy::new(widget, None);
237        log::trace!(
238            target: "kas_core::layout::hierarchy",
239            "apply_rect: rect={rect:?}:{hier}",
240        );
241    }
242}