cushy/widgets/
stack.rs

1//! A widget that combines a collection of [`WidgetList`] widgets into one.
2
3use figures::units::UPx;
4use figures::{IntoSigned, Rect, Round, ScreenScale, Size, Zero};
5
6use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable};
7use crate::styles::components::IntrinsicPadding;
8use crate::styles::FlexibleDimension;
9use crate::value::{Generation, IntoValue, Value};
10use crate::widget::{ChildrenSyncChange, MountedWidget, Widget, WidgetList, WidgetRef};
11use crate::widgets::grid::{GridDimension, GridLayout, Orientation};
12use crate::widgets::{Expand, Resize};
13use crate::ConstraintLimit;
14
15/// A widget that displays a collection of [`WidgetList`] widgets in a
16/// [orientation](Orientation).
17#[derive(Debug)]
18pub struct Stack {
19    orientation: Orientation,
20    /// The children widgets that belong to this array.
21    pub children: Value<WidgetList>,
22    /// The amount of space to place between each widget.
23    pub gutter: Value<FlexibleDimension>,
24    layout: GridLayout,
25    layout_generation: Option<Generation>,
26    synced_children: Vec<MountedWidget>,
27}
28
29impl Stack {
30    /// Returns a new widget with the given orientation and widgets.
31    pub fn new(orientation: Orientation, widgets: impl IntoValue<WidgetList>) -> Self {
32        Self {
33            orientation,
34            children: widgets.into_value(),
35            gutter: Value::Constant(FlexibleDimension::Auto),
36            layout: GridLayout::new(orientation),
37            layout_generation: None,
38            synced_children: Vec::new(),
39        }
40    }
41
42    /// Returns a new instance that displays `widgets` in a series of columns.
43    pub fn columns(widgets: impl IntoValue<WidgetList>) -> Self {
44        Self::new(Orientation::Column, widgets)
45    }
46
47    /// Returns a new instance that displays `widgets` in a series of rows.
48    pub fn rows(widgets: impl IntoValue<WidgetList>) -> Self {
49        Self::new(Orientation::Row, widgets)
50    }
51
52    /// Sets the space between each child to `gutter` and returns self.
53    #[must_use]
54    pub fn gutter(mut self, gutter: impl IntoValue<FlexibleDimension>) -> Self {
55        self.gutter = gutter.into_value();
56        self
57    }
58
59    fn synchronize_children(&mut self, context: &mut EventContext<'_>) {
60        let current_generation = self.children.generation();
61        self.children.invalidate_when_changed(context);
62        if current_generation.map_or_else(
63            || self.children.map(WidgetList::len) != self.layout.len(),
64            |gen| Some(gen) != self.layout_generation,
65        ) {
66            self.layout_generation = self.children.generation();
67            self.children.map(|children| {
68                children.synchronize_with(
69                    &mut self.synced_children,
70                    |this, index| this.get(index).map(MountedWidget::instance),
71                    |this, change| match change {
72                        ChildrenSyncChange::Insert(index, widget) => {
73                            // This is a brand new child.
74                            let guard = widget.lock();
75                            let (mut widget, dimension) = if let Some((weight, expand)) =
76                                guard.downcast_ref::<Expand>().and_then(|expand| {
77                                    expand
78                                        .weight(self.orientation == Orientation::Row)
79                                        .map(|weight| (weight, expand))
80                                }) {
81                                (expand.child().clone(), GridDimension::Fractional { weight })
82                            } else if let Some((child, size)) =
83                                guard.downcast_ref::<Resize>().and_then(|r| {
84                                    let range = match self.layout.orientation {
85                                        Orientation::Row => r.height,
86                                        Orientation::Column => r.width,
87                                    };
88                                    range.minimum().map(|size| {
89                                        (r.child().clone(), GridDimension::Measured { size })
90                                    })
91                                })
92                            {
93                                (child, size)
94                            } else {
95                                (WidgetRef::new(widget.clone()), GridDimension::FitContent)
96                            };
97                            drop(guard);
98                            this.insert(index, widget.mounted(context));
99
100                            self.layout
101                                .insert(index, dimension, context.kludgine.scale());
102                        }
103                        ChildrenSyncChange::Swap(a, b) => {
104                            this.swap(a, b);
105                            self.layout.swap(a, b);
106                        }
107                        ChildrenSyncChange::Truncate(length) => {
108                            for removed in this.drain(length..) {
109                                context.remove_child(&removed);
110                            }
111                            self.layout.truncate(length);
112                        }
113                    },
114                );
115            });
116        }
117    }
118}
119
120impl Widget for Stack {
121    fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
122        for (layout, child) in self.layout.iter().zip(&self.synced_children) {
123            if layout.size > 0 {
124                context.for_other(child).redraw();
125            }
126        }
127    }
128
129    fn mounted(&mut self, context: &mut EventContext<'_>) {
130        for child in &mut self.synced_children {
131            child.remount_if_needed(context);
132        }
133    }
134
135    fn layout(
136        &mut self,
137        available_space: Size<ConstraintLimit>,
138        context: &mut LayoutContext<'_, '_, '_, '_>,
139    ) -> Size<UPx> {
140        self.synchronize_children(&mut context.as_event_context());
141
142        self.gutter.invalidate_when_changed(context);
143        let gutter = match self.gutter.get() {
144            FlexibleDimension::Auto => context.get(&IntrinsicPadding),
145            FlexibleDimension::Dimension(dimension) => dimension,
146        }
147        .into_upx(context.gfx.scale())
148        .round();
149
150        let content_size = self.layout.update(
151            available_space,
152            gutter,
153            context.gfx.scale(),
154            |child_index, _element, constraints, persist| {
155                let mut context = context.for_other(&self.synced_children[child_index]);
156                if !persist {
157                    context = context.as_temporary();
158                }
159                context.layout(constraints)
160            },
161        );
162
163        for (layout, child) in self.layout.iter().zip(&self.synced_children) {
164            if layout.size > 0 {
165                context.set_child_layout(
166                    child,
167                    Rect::new(
168                        self.layout
169                            .orientation
170                            .make_point(layout.offset, UPx::ZERO)
171                            .into_signed(),
172                        self.layout
173                            .orientation
174                            .make_size(layout.size, self.layout.others[0])
175                            .into_signed(),
176                    ),
177                );
178            }
179        }
180
181        content_size
182    }
183
184    fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        fmt.debug_struct("Stack")
186            .field("orientation", &self.layout.orientation)
187            .field("children", &self.children)
188            .finish()
189    }
190}