druid_widget_nursery/
stack.rs

1// Copyright 2022 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use druid::{
16    BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
17    Point, Rect, RenderContext, Size, UnitPoint, UpdateCtx, Widget, WidgetPod,
18};
19use tracing::warn;
20
21use crate::animation::{Animated, AnimationCurve, Interpolate};
22use druid::kurbo::Shape;
23
24/// Stack child position
25///
26/// Stack children are positioned relative to the container edges.
27///
28/// Horizontal position is determined by `(left, right,
29/// width)`. Maximal two of those values may be defined (one value need
30/// to be `None`).
31///
32/// Vertical position is determined by `(top, bottom,
33/// height)`. Maximal two of those values may be defined (one value
34/// need to be `None`).
35///
36/// If `width` or `height` is unconstrained, they are positioned
37/// according to the [Stack::align] property.
38#[derive(Clone, Debug, Default, PartialEq, Data)]
39pub struct StackChildPosition {
40    /// Disance from left edge.
41    pub left: Option<f64>,
42    /// Disance from right edge.
43    pub right: Option<f64>,
44    /// Disance from top edge.
45    pub top: Option<f64>,
46    /// Disance from bottom edge.
47    pub bottom: Option<f64>,
48    /// Widget width.
49    pub width: Option<f64>,
50    /// Widhet height.
51    pub height: Option<f64>,
52}
53
54impl Interpolate for StackChildPosition {
55    fn interpolate(&self, other: &Self, fraction: f64) -> Self {
56        let lerp = |a: Option<f64>, b: Option<f64>, f: f64| -> Option<f64> {
57            match (a, b) {
58                (Some(a), Some(b)) => Some(a + (b - a) * f),
59                (Some(a), None) => {
60                    if fraction < 0.5 {
61                        Some(a)
62                    } else {
63                        None
64                    }
65                }
66                (None, Some(b)) => {
67                    if fraction < 0.5 {
68                        None
69                    } else {
70                        Some(b)
71                    }
72                }
73                (None, None) => None,
74            }
75        };
76        StackChildPosition {
77            left: lerp(self.left, other.left, fraction),
78            right: lerp(self.right, other.right, fraction),
79            top: lerp(self.top, other.top, fraction),
80            bottom: lerp(self.bottom, other.bottom, fraction),
81            width: lerp(self.width, other.width, fraction),
82            height: lerp(self.height, other.height, fraction),
83        }
84    }
85}
86
87impl StackChildPosition {
88    /// Create a new instance, all values set to `None`.
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Builder-style method to set distance from left edge.
94    pub fn left(mut self, value: Option<f64>) -> Self {
95        self.left = value;
96        self
97    }
98
99    /// Builder-style method to set distance from right edge.
100    pub fn right(mut self, value: Option<f64>) -> Self {
101        self.right = value;
102        self
103    }
104
105    /// Builder-style method to set distance from top edge.
106    pub fn top(mut self, value: Option<f64>) -> Self {
107        self.top = value;
108        self
109    }
110
111    /// Builder-style method to set distance from bottom edge.
112    pub fn bottom(mut self, value: Option<f64>) -> Self {
113        self.bottom = value;
114        self
115    }
116
117    /// Builder-style method to set child width.
118    pub fn width(mut self, value: Option<f64>) -> Self {
119        self.width = value;
120        self
121    }
122
123    /// Builder-style method to set child height.
124    pub fn height(mut self, value: Option<f64>) -> Self {
125        self.height = value;
126        self
127    }
128}
129
130type PositionCallback<T> = Box<dyn for<'a> Fn(&'a T, &Env) -> &'a StackChildPosition>;
131
132enum Position<T> {
133    None,
134    Fixed(StackChildPosition),
135    Dynamic(PositionCallback<T>),
136}
137
138/// Stack child configuration
139///
140/// This struct allows to configure additional aspects like the
141/// [`StackChildPosition`] or animation attributes for dynamic
142/// positioned children.
143pub struct StackChildParams<T> {
144    position: Position<T>,
145    // We also store the animation state here - just to keep it simple
146    animated_position: Animated<StackChildPosition>,
147}
148
149impl<T> From<StackChildPosition> for StackChildParams<T> {
150    fn from(position: StackChildPosition) -> Self {
151        StackChildParams::fixed(position)
152    }
153}
154
155impl<T> StackChildParams<T> {
156    // setup for non-positioned children
157    fn new() -> Self {
158        Self {
159            position: Position::None,
160            animated_position: Animated::jump(StackChildPosition::new()).layout(true),
161        }
162    }
163
164    /// Create a *positioned* stack child
165    pub fn fixed(position: StackChildPosition) -> Self {
166        Self {
167            position: Position::Fixed(position),
168            animated_position: Animated::jump(StackChildPosition::new()).layout(true),
169        }
170    }
171
172    /// Create a dynamically *positioned* stack child
173    pub fn dynamic<F>(position: F) -> Self
174    where
175        F: 'static + for<'a> Fn(&'a T, &Env) -> &'a StackChildPosition,
176    {
177        Self {
178            position: Position::Dynamic(Box::new(position)),
179            animated_position: Animated::new(StackChildPosition::new())
180                .curve(AnimationCurve::EASE_OUT)
181                .duration(0.3)
182                .layout(true),
183        }
184    }
185
186    /// Builder-style method for specifying the [`AnimationCurve`].
187    ///
188    /// For the non-builder varient, see [`set_curve`].
189    ///
190    /// [`set_curve`]: #method.set_curve
191    pub fn curve(mut self, curve: AnimationCurve) -> Self {
192        self.animated_position.set_curve(curve);
193        self
194    }
195
196    /// Set the [`AnimationCurve`].
197    ///
198    /// The curve is used by dynamically positioned children to
199    /// animate the position change.
200    pub fn set_curve(&mut self, curve: AnimationCurve) {
201        self.animated_position.set_curve(curve);
202    }
203
204    /// Builder-style method for specifying the animation duration.
205    ///
206    /// For the non-builder varient, see [`set_duration`].
207    ///
208    /// [`set_duration`]: #method.set_duration
209    pub fn duration(mut self, duration: f64) -> Self {
210        self.animated_position.set_duration(duration);
211        self
212    }
213
214    /// Set the animation duration in seconds.
215    ///
216    /// The duration is used by dynamically positioned children to
217    /// animate the position change.
218    pub fn set_duration(&mut self, duration: f64) {
219        self.animated_position.set_duration(duration);
220    }
221}
222
223struct StackChild<T> {
224    widget: WidgetPod<T, Box<dyn Widget<T>>>,
225    params: StackChildParams<T>,
226}
227
228impl<T: Data> StackChild<T> {
229    pub fn new(widget: impl Widget<T> + 'static, params: StackChildParams<T>) -> Self {
230        Self {
231            widget: WidgetPod::new(Box::new(widget)),
232            params,
233        }
234    }
235}
236
237/// Stack of widgets
238///
239/// Stack provides an easy way to stack widgets on top of each
240/// other. Children are positioned relative to the container edges.
241///
242/// Children are either *positioned* or *non-positioned*.
243///
244/// *Non-positioned* children are used to compute the size of the
245/// Stack widget itself. They are aligned using the [Stack::align] setting.
246///
247/// *Positioned* children are layed-out after *non-positioned*
248/// children. Their position is relative to the container edges (see
249/// [`StackChildPosition`]).
250pub struct Stack<T> {
251    children: Vec<StackChild<T>>,
252    align: UnitPoint,
253    fit: bool,
254    clip: bool,
255}
256
257impl<T: Data> Default for Stack<T> {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263impl<T: Data> Stack<T> {
264    /// Create a new Stack widget.
265    ///
266    /// Child alignment is set to [UnitPoint::TOP_LEFT], `fit` and
267    /// `clip` flags are set to `false`.
268    pub fn new() -> Self {
269        Self {
270            children: Vec::new(),
271            align: UnitPoint::TOP_LEFT,
272            fit: false,
273            clip: false,
274        }
275    }
276
277    /// Builder-style method for specifying the `fit` attribute.
278    pub fn fit(mut self, fit: bool) -> Self {
279        self.set_fit(fit);
280        self
281    }
282
283    /// Set the `fit` attribute.
284    ///
285    /// Fit *non-positioned* children to the size of the container.
286    pub fn set_fit(&mut self, fit: bool) {
287        self.fit = fit;
288    }
289
290    /// Builder-style method for specifying the `clip` attribute.
291    pub fn clip(mut self, clip: bool) -> Self {
292        self.set_clip(clip);
293        self
294    }
295
296    /// Set the `clip` attribute.
297    ///
298    /// Clip paint region at container boundaries.
299    pub fn set_clip(&mut self, clip: bool) {
300        self.clip = clip;
301    }
302
303    /// Builder-style method for specifying the default child alignment.
304    pub fn align(mut self, align: UnitPoint) -> Self {
305        self.set_align(align);
306        self
307    }
308
309    /// Set the default child alignment.
310    pub fn set_align(&mut self, align: UnitPoint) {
311        self.align = align;
312    }
313
314    /// Builder-style variant of `add_child`.
315    pub fn with_child(mut self, child: impl Widget<T> + 'static) -> Self {
316        self.add_child(child);
317        self
318    }
319
320    /// Add another stack child.
321    pub fn add_child(&mut self, child: impl Widget<T> + 'static) {
322        let child = StackChild::new(child, StackChildParams::new());
323        self.children.push(child);
324    }
325
326    /// Builder-style variant of `add_positioned_child`.
327    pub fn with_positioned_child(
328        mut self,
329        child: impl Widget<T> + 'static,
330        params: impl Into<StackChildParams<T>>,
331    ) -> Self {
332        self.add_positioned_child(child, params);
333        self
334    }
335
336    /// Add another *positioned* child.
337    pub fn add_positioned_child(
338        &mut self,
339        child: impl Widget<T> + 'static,
340        params: impl Into<StackChildParams<T>>,
341    ) {
342        let child = StackChild::new(child, params.into());
343        self.children.push(child);
344    }
345}
346
347impl<T: Data> Widget<T> for Stack<T> {
348    fn event(&mut self, ctx: &mut EventCtx<'_, '_>, event: &Event, data: &mut T, env: &Env) {
349        for child in self.children.iter_mut().rev() {
350            if ctx.is_handled() {
351                return;
352            }
353
354            let rect = child.widget.layout_rect();
355            let pos_match = match event {
356                Event::MouseMove(mouse_event)
357                | Event::MouseDown(mouse_event)
358                | Event::MouseUp(mouse_event)
359                | Event::Wheel(mouse_event) => rect.winding(mouse_event.pos) != 0,
360                _ => false,
361            };
362
363            child.widget.event(ctx, event, data, env);
364
365            // only send to one widget (top widget)
366            if pos_match {
367                break;
368            }
369        }
370
371        if let Event::AnimFrame(nanos) = event {
372            for child in self.children.iter_mut() {
373                if let Position::Dynamic(_) = &child.params.position {
374                    child.params.animated_position.update(ctx, *nanos);
375                }
376            }
377        }
378    }
379
380    fn lifecycle(
381        &mut self,
382        ctx: &mut LifeCycleCtx<'_, '_>,
383        event: &LifeCycle,
384        data: &T,
385        env: &Env,
386    ) {
387        for child in &mut self.children {
388            child.widget.lifecycle(ctx, event, data, env);
389            if let LifeCycle::WidgetAdded = event {
390                if let Position::Dynamic(position_cb) = &child.params.position {
391                    child
392                        .params
393                        .animated_position
394                        .jump_to_value(position_cb(data, env).clone());
395                }
396            }
397        }
398    }
399
400    fn update(&mut self, ctx: &mut UpdateCtx<'_, '_>, _old_data: &T, data: &T, env: &Env) {
401        for child in &mut self.children {
402            child.widget.update(ctx, data, env);
403            // update position for dynamic children
404            if let Position::Dynamic(position_cb) = &child.params.position {
405                let new_position = position_cb(data, env);
406                if new_position != &child.params.animated_position.end() {
407                    child
408                        .params
409                        .animated_position
410                        .animate(ctx, new_position.clone());
411                }
412            };
413        }
414    }
415
416    fn layout(
417        &mut self,
418        ctx: &mut LayoutCtx<'_, '_>,
419        bc: &BoxConstraints,
420        data: &T,
421        env: &Env,
422    ) -> druid::Size {
423        let child_bc = if self.fit {
424            BoxConstraints::tight(bc.max())
425        } else {
426            bc.loosen()
427        };
428
429        // Compute size for non-positioned children
430        let mut stack_width = 0f64;
431        let mut stack_height = 0f64;
432        for child in &mut self.children {
433            if !matches!(child.params.position, Position::None) {
434                continue;
435            }
436            let child_size = child.widget.layout(ctx, &child_bc, data, env);
437            stack_width = stack_width.max(child_size.width);
438            stack_height = stack_height.max(child_size.height);
439            child.widget.set_origin(ctx, Point::ORIGIN);
440        }
441
442        let size = Size::new(stack_width, stack_height);
443
444        // Compute size for positioned children
445        for child in &mut self.children {
446            let animated_position = child.params.animated_position.get();
447            let position = match &child.params.position {
448                Position::None => continue,
449                Position::Fixed(position) => position,
450                Position::Dynamic(_) => &animated_position,
451            };
452
453            let mut min_width = 0f64;
454            let mut max_width = std::f64::INFINITY;
455
456            match (position.left, position.right, position.width) {
457                (Some(left), Some(right), unused) => {
458                    let width = (stack_width - right - left).max(0.);
459                    min_width = width;
460                    max_width = width;
461                    if unused.is_some() {
462                        warn!("detected over-constrained stack element");
463                    }
464                }
465                (_, _, Some(width)) => {
466                    min_width = width;
467                    max_width = width;
468                }
469                _ => { /* no width constraint */ }
470            }
471
472            let mut min_height = 0f64;
473            let mut max_height = std::f64::INFINITY;
474
475            match (position.top, position.bottom, position.height) {
476                (Some(top), Some(bottom), unused) => {
477                    let height = (stack_height - bottom - top).max(0.);
478                    min_height = height;
479                    max_height = height;
480                    if unused.is_some() {
481                        warn!("detected over-constrained stack element");
482                    }
483                }
484                (_, _, Some(height)) => {
485                    min_height = height;
486                    max_height = height;
487                }
488                _ => { /* no height constraint */ }
489            }
490
491            let child_bc = BoxConstraints::new(
492                Size::new(min_width, min_height),
493                Size::new(max_width, max_height),
494            );
495
496            let child_size = child.widget.layout(ctx, &child_bc, data, env);
497            let align = self.align;
498
499            let offset_x = match (position.left, position.right) {
500                (Some(left), _) => left,
501                (None, Some(right)) => stack_width - right - child_size.width,
502                (None, None) => {
503                    let extra_width = stack_width - child_size.width;
504                    align.resolve(Rect::new(0., 0., extra_width, 0.)).expand().x
505                }
506            };
507
508            let offset_y = match (position.top, position.bottom) {
509                (Some(top), _) => top,
510                (None, Some(bottom)) => stack_height - bottom - child_size.height,
511                (None, None) => {
512                    let extra_height = stack_height - child_size.height;
513                    align
514                        .resolve(Rect::new(0., 0., 0., extra_height))
515                        .expand()
516                        .y
517                }
518            };
519
520            let origin = Point::new(offset_x, offset_y);
521            child.widget.set_origin(ctx, origin);
522        }
523
524        size
525    }
526
527    fn paint(&mut self, ctx: &mut PaintCtx<'_, '_, '_>, data: &T, env: &Env) {
528        let size = ctx.size();
529
530        if self.clip {
531            ctx.clip(size.to_rect());
532        }
533        for child in &mut self.children {
534            child.widget.paint(ctx, data, env);
535        }
536    }
537}