druid 0.8.3

Data-oriented Rust UI design toolkit.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// Copyright 2019 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! The fundamental Druid types.

use crate::kurbo::Size;
use crate::widget::Axis;

/// Constraints for layout.
///
/// The layout strategy for Druid is strongly inspired by Flutter,
/// and this struct is similar to the [Flutter BoxConstraints] class.
///
/// At the moment, it represents simply a minimum and maximum size.
/// A widget's [`layout`] method should choose an appropriate size that
/// meets these constraints.
///
/// Further, a container widget should compute appropriate constraints
/// for each of its child widgets, and pass those down when recursing.
///
/// The constraints are always [rounded away from zero] to integers
/// to enable pixel perfect layout.
///
/// [`layout`]: crate::Widget::layout
/// [Flutter BoxConstraints]: https://api.flutter.dev/flutter/rendering/BoxConstraints-class.html
/// [rounded away from zero]: Size::expand
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BoxConstraints {
    min: Size,
    max: Size,
}

impl BoxConstraints {
    /// An unbounded box constraints object.
    ///
    /// Can be satisfied by any nonnegative size.
    pub const UNBOUNDED: BoxConstraints = BoxConstraints {
        min: Size::ZERO,
        max: Size::new(f64::INFINITY, f64::INFINITY),
    };

    /// Create a new box constraints object.
    ///
    /// Create constraints based on minimum and maximum size.
    ///
    /// The given sizes are also [rounded away from zero],
    /// so that the layout is aligned to integers.
    ///
    /// [rounded away from zero]: Size::expand
    pub fn new(min: Size, max: Size) -> BoxConstraints {
        BoxConstraints {
            min: min.expand(),
            max: max.expand(),
        }
    }

    /// Create a "tight" box constraints object.
    ///
    /// A "tight" constraint can only be satisfied by a single size.
    ///
    /// The given size is also [rounded away from zero],
    /// so that the layout is aligned to integers.
    ///
    /// [rounded away from zero]: Size::expand
    pub fn tight(size: Size) -> BoxConstraints {
        let size = size.expand();
        BoxConstraints {
            min: size,
            max: size,
        }
    }

    /// Create a "loose" version of the constraints.
    ///
    /// Make a version with zero minimum size, but the same maximum size.
    pub fn loosen(&self) -> BoxConstraints {
        BoxConstraints {
            min: Size::ZERO,
            max: self.max,
        }
    }

    /// Clamp a given size so that it fits within the constraints.
    ///
    /// The given size is also [rounded away from zero],
    /// so that the layout is aligned to integers.
    ///
    /// [rounded away from zero]: Size::expand
    pub fn constrain(&self, size: impl Into<Size>) -> Size {
        size.into().expand().clamp(self.min, self.max)
    }

    /// Returns the max size of these constraints.
    pub fn max(&self) -> Size {
        self.max
    }

    /// Returns the min size of these constraints.
    pub fn min(&self) -> Size {
        self.min
    }

    /// Whether there is an upper bound on the width.
    pub fn is_width_bounded(&self) -> bool {
        self.max.width.is_finite()
    }

    /// Whether there is an upper bound on the height.
    pub fn is_height_bounded(&self) -> bool {
        self.max.height.is_finite()
    }

    /// Check to see if these constraints are legit.
    ///
    /// Logs a warning if BoxConstraints are invalid.
    pub fn debug_check(&self, name: &str) {
        if !(0.0 <= self.min.width
            && self.min.width <= self.max.width
            && 0.0 <= self.min.height
            && self.min.height <= self.max.height
            && self.min.expand() == self.min
            && self.max.expand() == self.max)
        {
            tracing::warn!("Bad BoxConstraints passed to {}:", name);
            tracing::warn!("{:?}", self);
        }

        if self.min.width.is_infinite() {
            tracing::warn!("Infinite minimum width constraint passed to {}:", name);
        }

        if self.min.height.is_infinite() {
            tracing::warn!("Infinite minimum height constraint passed to {}:", name);
        }
    }

    /// Shrink min and max constraints by size
    ///
    /// The given size is also [rounded away from zero],
    /// so that the layout is aligned to integers.
    ///
    /// [rounded away from zero]: Size::expand
    pub fn shrink(&self, diff: impl Into<Size>) -> BoxConstraints {
        let diff = diff.into().expand();
        let min = Size::new(
            (self.min().width - diff.width).max(0.),
            (self.min().height - diff.height).max(0.),
        );
        let max = Size::new(
            (self.max().width - diff.width).max(0.),
            (self.max().height - diff.height).max(0.),
        );

        BoxConstraints::new(min, max)
    }

    /// Test whether these constraints contain the given `Size`.
    pub fn contains(&self, size: impl Into<Size>) -> bool {
        let size = size.into();
        (self.min.width <= size.width && size.width <= self.max.width)
            && (self.min.height <= size.height && size.height <= self.max.height)
    }

    /// Find the `Size` within these `BoxConstraint`s that minimises the difference between the
    /// returned `Size`'s aspect ratio and `aspect_ratio`, where *aspect ratio* is defined as
    /// `height / width`.
    ///
    /// If multiple `Size`s give the optimal `aspect_ratio`, then the one with the `width` nearest
    /// the supplied width will be used. Specifically, if `width == 0.0` then the smallest possible
    /// `Size` will be chosen, and likewise if `width == f64::INFINITY`, then the largest `Size`
    /// will be chosen.
    ///
    /// Use this function when maintaining an aspect ratio is more important than minimizing the
    /// distance between input and output size width and height.
    pub fn constrain_aspect_ratio(&self, aspect_ratio: f64, width: f64) -> Size {
        // Minimizing/maximizing based on aspect ratio seems complicated, but in reality everything
        // is linear, so the amount of work to do is low.
        let ideal_size = Size {
            width,
            height: width * aspect_ratio,
        };

        // It may be possible to remove these in the future if the invariant is checked elsewhere.
        let aspect_ratio = aspect_ratio.abs();
        let width = width.abs();

        // Firstly check if we can simply return the exact requested
        if self.contains(ideal_size) {
            return ideal_size;
        }

        // Then we check if any `Size`s with our desired aspect ratio are inside the constraints.
        // TODO this currently outputs garbage when things are < 0.
        let min_w_min_h = self.min.height / self.min.width;
        let max_w_min_h = self.min.height / self.max.width;
        let min_w_max_h = self.max.height / self.min.width;
        let max_w_max_h = self.max.height / self.max.width;

        // When the aspect ratio line crosses the constraints, the closest point must be one of the
        // two points where the aspect ratio enters/exits.

        // When the aspect ratio line doesn't intersect the box of possible sizes, the closest
        // point must be either (max width, min height) or (max height, min width). So all we have
        // to do is check which one of these has the closest aspect ratio.

        // Check each possible intersection (or not) of the aspect ratio line with the constraints
        if aspect_ratio > min_w_max_h {
            // outside max height min width
            Size {
                width: self.min.width,
                height: self.max.height,
            }
        } else if aspect_ratio < max_w_min_h {
            // outside min height max width
            Size {
                width: self.max.width,
                height: self.min.height,
            }
        } else if aspect_ratio > min_w_min_h {
            // hits the constraints on the min width line
            if width < self.min.width {
                // we take the point on the min width
                Size {
                    width: self.min.width,
                    height: self.min.width * aspect_ratio,
                }
            } else if aspect_ratio < max_w_max_h {
                // exits through max.width
                Size {
                    width: self.max.width,
                    height: self.max.width * aspect_ratio,
                }
            } else {
                // exits through max.height
                Size {
                    width: self.max.height * aspect_ratio.recip(),
                    height: self.max.height,
                }
            }
        } else {
            // final case is where we hit constraints on the min height line
            if width < self.min.width {
                // take the point on the min height
                Size {
                    width: self.min.height * aspect_ratio.recip(),
                    height: self.min.height,
                }
            } else if aspect_ratio > max_w_max_h {
                // exit thru max height
                Size {
                    width: self.max.height * aspect_ratio.recip(),
                    height: self.max.height,
                }
            } else {
                // exit thru max width
                Size {
                    width: self.max.width,
                    height: self.max.width * aspect_ratio,
                }
            }
        }
    }

    /// Sets the max on a given axis to infinity.
    pub fn unbound_max(&self, axis: Axis) -> Self {
        match axis {
            Axis::Horizontal => self.unbound_max_width(),
            Axis::Vertical => self.unbound_max_height(),
        }
    }

    /// Sets max width to infinity.
    pub fn unbound_max_width(&self) -> Self {
        let mut max = self.max();
        max.width = f64::INFINITY;
        BoxConstraints::new(self.min(), max)
    }

    /// Sets max height to infinity.
    pub fn unbound_max_height(&self) -> Self {
        let mut max = self.max();
        max.height = f64::INFINITY;
        BoxConstraints::new(self.min(), max)
    }

    /// Shrinks the max dimension on the given axis.
    /// Does NOT shrink beyond min.
    pub fn shrink_max_to(&self, axis: Axis, dim: f64) -> Self {
        match axis {
            Axis::Horizontal => self.shrink_max_width_to(dim),
            Axis::Vertical => self.shrink_max_height_to(dim),
        }
    }

    /// Shrinks the max width to dim.
    /// Does NOT shrink beyond min width.
    pub fn shrink_max_width_to(&self, dim: f64) -> Self {
        let mut max = self.max();
        max.width = f64::max(dim, self.min().width);
        BoxConstraints::new(self.min(), max)
    }

    /// Shrinks the max height to dim.
    /// Does NOT shrink beyond min height.
    pub fn shrink_max_height_to(&self, dim: f64) -> Self {
        let mut max = self.max();
        max.height = f64::max(dim, self.min().height);
        BoxConstraints::new(self.min(), max)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_log::test;

    fn bc(min_width: f64, min_height: f64, max_width: f64, max_height: f64) -> BoxConstraints {
        BoxConstraints::new(
            Size::new(min_width, min_height),
            Size::new(max_width, max_height),
        )
    }

    #[test]
    fn constrain_aspect_ratio() {
        for (bc, aspect_ratio, width, output) in [
            // The ideal size lies within the constraints
            (bc(0.0, 0.0, 100.0, 100.0), 1.0, 50.0, Size::new(50.0, 50.0)),
            (bc(0.0, 10.0, 90.0, 100.0), 1.0, 50.0, Size::new(50.0, 50.0)),
            // The correct aspect ratio is available (but not width)
            // min height
            (
                bc(10.0, 10.0, 100.0, 100.0),
                1.0,
                5.0,
                Size::new(10.0, 10.0),
            ),
            (
                bc(40.0, 90.0, 60.0, 100.0),
                2.0,
                30.0,
                Size::new(45.0, 90.0),
            ),
            (
                bc(10.0, 10.0, 100.0, 100.0),
                0.5,
                5.0,
                Size::new(20.0, 10.0),
            ),
            // min width
            (
                bc(10.0, 10.0, 100.0, 100.0),
                2.0,
                5.0,
                Size::new(10.0, 20.0),
            ),
            (
                bc(90.0, 40.0, 100.0, 60.0),
                0.5,
                60.0,
                Size::new(90.0, 45.0),
            ),
            (
                bc(50.0, 0.0, 50.0, 100.0),
                1.0,
                100.0,
                Size::new(50.0, 50.0),
            ),
            // max height
            (
                bc(10.0, 10.0, 100.0, 100.0),
                2.0,
                105.0,
                Size::new(50.0, 100.0),
            ),
            (
                bc(10.0, 10.0, 100.0, 100.0),
                0.5,
                105.0,
                Size::new(100.0, 50.0),
            ),
            // The correct aspet ratio is not available
            (
                bc(20.0, 20.0, 40.0, 40.0),
                10.0,
                30.0,
                Size::new(20.0, 40.0),
            ),
            (bc(20.0, 20.0, 40.0, 40.0), 0.1, 30.0, Size::new(40.0, 20.0)),
            // non-finite
            (
                bc(50.0, 0.0, 50.0, f64::INFINITY),
                1.0,
                100.0,
                Size::new(50.0, 50.0),
            ),
        ]
        .iter()
        {
            assert_eq!(
                bc.constrain_aspect_ratio(*aspect_ratio, *width),
                *output,
                "bc:{bc:?}, ar:{aspect_ratio}, w:{width}"
            );
        }
    }

    #[test]
    fn unbounded() {
        assert!(!BoxConstraints::UNBOUNDED.is_width_bounded());
        assert!(!BoxConstraints::UNBOUNDED.is_height_bounded());

        assert_eq!(BoxConstraints::UNBOUNDED.min(), Size::ZERO);
    }
}