Skip to main content

ferroui_controls/
dock_panel.rs

1// This source file is adapted from the Windows Presentation Foundation
2// project (https://github.com/dotnet/wpf/), MIT License, courtesy of The .NET
3// Foundation.
4
5use crate::{Control, ControlImpl, Panel, PanelImpl};
6use ferroui_base::input::InputElementImpl;
7use ferroui_base::interactivity::InteractiveImpl;
8use ferroui_base::layout::{Layoutable, LayoutableImpl};
9use ferroui_base::utilities::math_utilities::{max, min};
10use ferroui_base::{
11    ferro_class, ferro_impl_classes, ferro_property, instantiate, AttachedProperty, FerroObjectImpl, FerroProperty, Rect, Ref, Size, StyledElementImpl, StyledProperty, VisualImpl,
12};
13
14/// Defines the available docking modes for a control in a [`DockPanel`].
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
16#[repr(i32)]
17pub enum Dock {
18    /// The control is docked to the left of the panel.
19    #[default]
20    Left = 0,
21    /// The control is docked to the bottom of the panel.
22    Bottom = 1,
23    /// The control is docked to the right of the panel.
24    Right = 2,
25    /// The control is docked to the top of the panel.
26    Top = 3,
27}
28
29/// A panel which arranges its children at the top, bottom, left, right or
30/// center.
31#[repr(C)]
32pub struct DockPanel {
33    base: Panel,
34}
35
36ferro_class!(DockPanel: Panel);
37ferroui_base::ferro_class_info!(DockPanel { new: DockPanel::new });
38ferro_impl_classes!(
39    DockPanel: StyledElementImpl,
40    VisualImpl,
41    InteractiveImpl,
42    InputElementImpl,
43    ControlImpl,
44    PanelImpl
45);
46
47ferroui_base::ferro_impl_classes!(DockPanel: FerroObjectImpl);
48
49impl LayoutableImpl for DockPanel {
50    /// Updates the desired size of the panel. Called by parent elements; this
51    /// is the first pass of layout.
52    ///
53    /// Children are measured based on their sizing properties and the dock
54    /// they have. Each child is allowed to consume all of the space on the
55    /// side on which it is docked; left/right docked children are granted all
56    /// vertical space for their entire width, and top/bottom docked children
57    /// are granted all horizontal space for their entire height.
58    fn measure_override(this: &Self, available_size: Size) -> Size {
59        let mut parent_width = 0.0;
60        let mut parent_height = 0.0;
61        let mut accumulated_width = 0.0;
62        let mut accumulated_height = 0.0;
63
64        let mut horizontal_spacing = false;
65        let mut vertical_spacing = false;
66        let children = this.children().snapshot();
67        let last_child_fill = this.last_child_fill();
68        let children_count = if last_child_fill { children.len().saturating_sub(1) } else { children.len() };
69
70        for child in children.iter().take(children_count) {
71            let child_constraint = Size::new(
72                max(0.0, available_size.width - accumulated_width),
73                max(0.0, available_size.height - accumulated_height),
74            );
75
76            child.measure(child_constraint);
77            let child_desired_size = child.desired_size();
78
79            // Now, we adjust:
80            // 1. Size consumed by children (accumulated size). This will be
81            //    used when computing subsequent children to determine how
82            //    much space is remaining for them.
83            // 2. Parent size implied by this child (parent size) when added
84            //    to the current children (accumulated size). This is
85            //    different from the size above in one respect: a `Dock::Left`
86            //    child implies a height, but does not actually consume any
87            //    height for subsequent children.
88            // If we accumulate size in a given dimension, the next child (or
89            // the end conditions after the child loop) will deal with
90            // computing our minimum size (parent size) due to that
91            // accumulation. Therefore, we only need to compute our minimum
92            // size (parent size) in dimensions that this child does not
93            // accumulate: width for top/bottom, height for left/right.
94            match child.get_value(Self::dock_property()) {
95                Dock::Left | Dock::Right => {
96                    parent_height = max(parent_height, accumulated_height + child_desired_size.height);
97                    if child.is_visible() {
98                        accumulated_width += this.horizontal_spacing();
99                        horizontal_spacing = true;
100                    }
101                    accumulated_width += child_desired_size.width;
102                }
103                Dock::Top | Dock::Bottom => {
104                    parent_width = max(parent_width, accumulated_width + child_desired_size.width);
105                    if child.is_visible() {
106                        accumulated_height += this.vertical_spacing();
107                        vertical_spacing = true;
108                    }
109                    accumulated_height += child_desired_size.height;
110                }
111            }
112        }
113
114        if last_child_fill && !children.is_empty() {
115            let child = &children[children.len() - 1];
116            let child_constraint = Size::new(
117                max(0.0, available_size.width - accumulated_width),
118                max(0.0, available_size.height - accumulated_height),
119            );
120
121            child.measure(child_constraint);
122            let child_desired_size = child.desired_size();
123            parent_height = max(parent_height, accumulated_height + child_desired_size.height);
124            parent_width = max(parent_width, accumulated_width + child_desired_size.width);
125            accumulated_height += child_desired_size.height;
126            accumulated_width += child_desired_size.width;
127        } else {
128            if horizontal_spacing {
129                accumulated_width -= this.horizontal_spacing();
130            }
131            if vertical_spacing {
132                accumulated_height -= this.vertical_spacing();
133            }
134        }
135
136        // Make sure the final accumulated size is reflected in parent size.
137        parent_width = max(parent_width, accumulated_width);
138        parent_height = max(parent_height, accumulated_height);
139        Size::new(parent_width, parent_height)
140    }
141
142    /// The dock panel computes a position and final size for each of its
143    /// children based upon their dock and sizing properties.
144    fn arrange_override(this: &Self, final_size: Size) -> Size {
145        let children = this.children().snapshot();
146        if children.is_empty() {
147            return final_size;
148        }
149
150        let mut current_bounds = Rect::from_size(final_size);
151        let last_child_fill = this.last_child_fill();
152        let children_count = if last_child_fill { children.len() - 1 } else { children.len() };
153
154        for child in children.iter().take(children_count) {
155            if !child.is_visible() {
156                continue;
157            }
158
159            let dock = child.get_value(Self::dock_property());
160            match dock {
161                Dock::Left => {
162                    let mut width = min(child.desired_size().width, current_bounds.width);
163                    child.arrange(current_bounds.with_width(width));
164                    width += this.horizontal_spacing();
165                    current_bounds = Rect::new(
166                        current_bounds.x + width,
167                        current_bounds.y,
168                        max(0.0, current_bounds.width - width),
169                        current_bounds.height,
170                    );
171                }
172                Dock::Top => {
173                    let mut height = min(child.desired_size().height, current_bounds.height);
174                    child.arrange(current_bounds.with_height(height));
175                    height += this.vertical_spacing();
176                    current_bounds = Rect::new(
177                        current_bounds.x,
178                        current_bounds.y + height,
179                        current_bounds.width,
180                        max(0.0, current_bounds.height - height),
181                    );
182                }
183                Dock::Right => {
184                    let mut width = min(child.desired_size().width, current_bounds.width);
185                    child.arrange(Rect::new(
186                        current_bounds.x + current_bounds.width - width,
187                        current_bounds.y,
188                        width,
189                        current_bounds.height,
190                    ));
191                    width += this.horizontal_spacing();
192                    current_bounds = current_bounds.with_width(max(0.0, current_bounds.width - width));
193                }
194                Dock::Bottom => {
195                    let mut height = min(child.desired_size().height, current_bounds.height);
196                    child.arrange(Rect::new(
197                        current_bounds.x,
198                        current_bounds.y + current_bounds.height - height,
199                        current_bounds.width,
200                        height,
201                    ));
202                    height += this.vertical_spacing();
203                    current_bounds = current_bounds.with_height(max(0.0, current_bounds.height - height));
204                }
205            }
206        }
207
208        if last_child_fill {
209            let child = &children[children.len() - 1];
210            child.arrange(Rect::new(current_bounds.x, current_bounds.y, current_bounds.width, current_bounds.height));
211        }
212
213        final_size
214    }
215}
216
217ferroui_base::ferro_properties! { impl DockPanel {
218    ferro_property!(
219        /// Defines the `Dock` attached property.
220        pub fn dock_property() -> AttachedProperty<Dock> {
221            FerroProperty::register_attached::<DockPanel, Control, _>("Dock", Dock::Left)
222        }
223    );
224
225    ferro_property!(
226        /// Defines the `LastChildFill` property.
227        pub fn last_child_fill_property() -> StyledProperty<bool> {
228            FerroProperty::register::<DockPanel, _>("LastChildFill", true)
229        }
230    );
231
232    ferro_property!(
233        /// Defines the `HorizontalSpacing` property.
234        pub fn horizontal_spacing_property() -> StyledProperty<f64> {
235            FerroProperty::register::<DockPanel, _>("HorizontalSpacing", 0.0)
236        }
237    );
238
239    ferro_property!(
240        /// Defines the `VerticalSpacing` property.
241        pub fn vertical_spacing_property() -> StyledProperty<f64> {
242            FerroProperty::register::<DockPanel, _>("VerticalSpacing", 0.0)
243        }
244    );
245} }
246
247impl DockPanel {
248    fn static_constructor() {
249        Panel::affects_parent_measure::<DockPanel>(&[Self::dock_property().as_property()]);
250        Layoutable::affects_measure::<DockPanel>(&[
251            Self::last_child_fill_property().as_property(),
252            Self::horizontal_spacing_property().as_property(),
253            Self::vertical_spacing_property().as_property(),
254        ]);
255    }
256
257    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
258    pub fn construct() -> Self {
259        Self { base: Panel::construct() }
260    }
261
262    pub fn new() -> Ref<Self> {
263        instantiate(Self::construct())
264    }
265
266    /// Gets the value of the `Dock` attached property on the specified
267    /// control.
268    pub fn get_dock(control: &Control) -> Dock {
269        control.get_value(Self::dock_property())
270    }
271
272    /// Sets the value of the `Dock` attached property on the specified
273    /// control.
274    pub fn set_dock(control: &Control, value: Dock) {
275        control.set_value(Self::dock_property(), value)
276    }
277
278    /// Whether the last child of the panel fills the remaining space in the
279    /// panel.
280    pub fn last_child_fill(&self) -> bool {
281        self.get_value(Self::last_child_fill_property())
282    }
283
284    pub fn set_last_child_fill(&self, value: bool) {
285        self.set_value(Self::last_child_fill_property(), value)
286    }
287
288    /// The horizontal distance between the child objects.
289    pub fn horizontal_spacing(&self) -> f64 {
290        self.get_value(Self::horizontal_spacing_property())
291    }
292
293    pub fn set_horizontal_spacing(&self, value: f64) {
294        self.set_value(Self::horizontal_spacing_property(), value)
295    }
296
297    /// The vertical distance between the child objects.
298    pub fn vertical_spacing(&self) -> f64 {
299        self.get_value(Self::vertical_spacing_property())
300    }
301
302    pub fn set_vertical_spacing(&self, value: f64) {
303        self.set_value(Self::vertical_spacing_property(), value)
304    }
305}