fyrox_ui/
wrap_panel.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Wrap panel is used to stack children widgets either in vertical or horizontal direction with overflow. See [`WrapPanel`]
22//! docs for more info and usage examples.
23
24#![warn(missing_docs)]
25#![allow(clippy::reversed_empty_ranges)]
26
27use crate::{
28    core::{
29        algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
30        visitor::prelude::*,
31    },
32    define_constructor,
33    message::{MessageDirection, UiMessage},
34    widget::{Widget, WidgetBuilder},
35    BuildContext, Control, Orientation, UiNode, UserInterface,
36};
37
38use core::f32;
39use fyrox_core::uuid_provider;
40use fyrox_core::variable::InheritableVariable;
41use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
42use fyrox_graph::BaseSceneGraph;
43use std::{
44    cell::RefCell,
45    ops::{Deref, DerefMut, Range},
46};
47
48/// A set of possible [`WrapPanel`] widget messages.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum WrapPanelMessage {
51    /// The message is used to change orientation of the wrap panel.
52    Orientation(Orientation),
53}
54
55impl WrapPanelMessage {
56    define_constructor!(
57        /// Creates [`WrapPanelMessage::Orientation`] message.
58        WrapPanelMessage:Orientation => fn orientation(Orientation), layout: false
59    );
60}
61
62/// Wrap panel is used to stack children widgets either in vertical or horizontal direction with overflow - every widget
63/// that does not have enough space on current line, will automatically be placed on the next line (either vertical or
64/// horizontal, depending on the orientation).
65///
66/// ## How to create
67///
68/// Use `WrapPanelBuilder` to create new wrap panel instance:
69///
70/// ```rust,no_run
71/// # use fyrox_ui::{
72/// #     core::pool::Handle,
73/// #     widget::WidgetBuilder, wrap_panel::WrapPanelBuilder, BuildContext, Orientation, UiNode,
74/// # };
75/// #
76/// fn create_wrap_panel(ctx: &mut BuildContext) -> Handle<UiNode> {
77///     WrapPanelBuilder::new(WidgetBuilder::new())
78///         .with_orientation(Orientation::Horizontal)
79///         .build(ctx)
80/// }
81/// ```
82///
83/// All widgets, that needs to be arranged, should be direct children of the wrap panel. Use [`WidgetBuilder::with_children`]
84/// or [`WidgetBuilder::with_child`] to add children nodes.
85///
86/// ## Orientation
87///
88/// Wrap panel can stack your widgets either in vertical or horizontal direction. Use `.with_orientation` while building
89/// the panel to switch orientation to desired.
90#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
91#[reflect(derived_type = "UiNode")]
92pub struct WrapPanel {
93    /// Base widget of the wrap panel.
94    pub widget: Widget,
95    /// Current orientation of the wrap panel.
96    pub orientation: InheritableVariable<Orientation>,
97    /// Internal lines storage.
98    #[visit(skip)]
99    #[reflect(hidden)]
100    pub lines: RefCell<Vec<Line>>,
101}
102
103impl ConstructorProvider<UiNode, UserInterface> for WrapPanel {
104    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
105        GraphNodeConstructor::new::<Self>()
106            .with_variant("Wrap Panel", |ui| {
107                WrapPanelBuilder::new(WidgetBuilder::new().with_name("Wrap Panel"))
108                    .build(&mut ui.build_ctx())
109                    .into()
110            })
111            .with_group("Layout")
112    }
113}
114
115crate::define_widget_deref!(WrapPanel);
116
117/// Represents a single line (either vertical or horizontal) with arranged widgets.
118#[derive(Clone, Debug)]
119pub struct Line {
120    /// Indices of the children widgets that belongs to this line.
121    pub children: Range<usize>,
122    /// Bounds of this line.
123    pub bounds: Rect<f32>,
124}
125
126impl Default for Line {
127    fn default() -> Self {
128        Self {
129            children: 0..0,
130            bounds: Default::default(),
131        }
132    }
133}
134
135uuid_provider!(WrapPanel = "f488ab8e-8f8b-473c-a450-5ac33f1afb39");
136
137impl Control for WrapPanel {
138    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
139        let mut measured_size: Vector2<f32> = Vector2::default();
140        let mut line_size = Vector2::default();
141        for child_handle in self.widget.children() {
142            let child = ui.node(*child_handle);
143            ui.measure_node(*child_handle, Vector2::new(f32::INFINITY, f32::INFINITY));
144            let desired = child.desired_size();
145            match *self.orientation {
146                Orientation::Vertical => {
147                    if line_size.y + desired.y > available_size.y {
148                        // Commit column.
149                        measured_size.y = measured_size.y.max(line_size.y);
150                        measured_size.x += line_size.x;
151                        line_size = Vector2::default();
152                    }
153                    line_size.x = line_size.x.max(desired.x);
154                    line_size.y += desired.y;
155                }
156                Orientation::Horizontal => {
157                    if line_size.x + desired.x > available_size.x {
158                        // Commit row.
159                        measured_size.x = measured_size.x.max(line_size.x);
160                        measured_size.y += line_size.y;
161                        line_size = Vector2::default();
162                    }
163                    line_size.x += desired.x;
164                    line_size.y = line_size.y.max(desired.y);
165                }
166            }
167        }
168
169        // Commit rest.
170        match *self.orientation {
171            Orientation::Vertical => {
172                measured_size.y = measured_size.y.max(line_size.y);
173                measured_size.x += line_size.x;
174            }
175            Orientation::Horizontal => {
176                measured_size.x = measured_size.x.max(line_size.x);
177                measured_size.y += line_size.y;
178            }
179        }
180
181        measured_size
182    }
183
184    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
185        // First pass - arrange lines.
186        let mut lines = self.lines.borrow_mut();
187        lines.clear();
188        let mut line = Line::default();
189        for child_handle in self.widget.children() {
190            let child = ui.node(*child_handle);
191            let desired = child.desired_size();
192            match *self.orientation {
193                Orientation::Vertical => {
194                    if line.bounds.h() + desired.y > final_size.y {
195                        // Commit column.
196                        lines.push(line.clone());
197                        // Advance column.
198                        line.bounds.position.x += line.bounds.w();
199                        line.bounds.position.y = 0.0;
200                        line.bounds.size.x = desired.x;
201                        line.bounds.size.y = desired.y;
202                        // Reset children.
203                        line.children.start = line.children.end;
204                        line.children.end = line.children.start + 1;
205                    } else {
206                        line.bounds.size.y += desired.y;
207                        line.bounds.size.x = line.bounds.w().max(desired.x);
208                        line.children.end += 1;
209                    }
210                }
211                Orientation::Horizontal => {
212                    if line.bounds.w() + desired.x > final_size.x {
213                        // Commit row.
214                        lines.push(line.clone());
215                        // Advance row.
216                        line.bounds.position.x = 0.0;
217                        line.bounds.position.y += line.bounds.h();
218                        line.bounds.size.x = desired.x;
219                        line.bounds.size.y = desired.y;
220                        // Reset children.
221                        line.children.start = line.children.end;
222                        line.children.end = line.children.start + 1;
223                    } else {
224                        line.bounds.size.x += desired.x;
225                        line.bounds.size.y = line.bounds.h().max(desired.y);
226                        line.children.end += 1;
227                    }
228                }
229            }
230        }
231
232        // Commit rest.
233        lines.push(line);
234
235        // Second pass - arrange children of lines.
236        let mut full_size = Vector2::default();
237        for line in lines.iter() {
238            let mut cursor = line.bounds.position;
239            for child_index in line.children.clone() {
240                let child_handle = self.children()[child_index];
241                let child = ui.node(child_handle);
242                let desired = child.desired_size();
243                match *self.orientation {
244                    Orientation::Vertical => {
245                        let child_bounds =
246                            Rect::new(line.bounds.x(), cursor.y, line.bounds.w(), desired.y);
247                        ui.arrange_node(child_handle, &child_bounds);
248                        cursor.y += desired.y;
249                    }
250                    Orientation::Horizontal => {
251                        let child_bounds =
252                            Rect::new(cursor.x, line.bounds.y(), desired.x, line.bounds.h());
253                        ui.arrange_node(child_handle, &child_bounds);
254                        cursor.x += desired.x;
255                    }
256                }
257            }
258            match *self.orientation {
259                Orientation::Vertical => {
260                    full_size.x += line.bounds.w();
261                    full_size.y = final_size.y.max(line.bounds.h());
262                }
263                Orientation::Horizontal => {
264                    full_size.x = final_size.x.max(line.bounds.w());
265                    full_size.y += line.bounds.h();
266                }
267            }
268        }
269
270        full_size
271    }
272
273    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
274        self.widget.handle_routed_message(ui, message);
275
276        if message.destination() == self.handle && message.direction() == MessageDirection::ToWidget
277        {
278            if let Some(WrapPanelMessage::Orientation(orientation)) = message.data() {
279                if *orientation != *self.orientation {
280                    self.orientation.set_value_and_mark_modified(*orientation);
281                    self.invalidate_layout();
282                }
283            }
284        }
285    }
286}
287
288/// Wrap panel builder creates [`WrapPanel`] widget and adds it to the user interface.
289pub struct WrapPanelBuilder {
290    widget_builder: WidgetBuilder,
291    orientation: Option<Orientation>,
292}
293
294impl WrapPanelBuilder {
295    /// Creates a new wrap panel builder.
296    pub fn new(widget_builder: WidgetBuilder) -> Self {
297        Self {
298            widget_builder,
299            orientation: None,
300        }
301    }
302
303    /// Sets the desired orientation of the wrap panel.
304    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
305        self.orientation = Some(orientation);
306        self
307    }
308
309    /// Finishes wrap panel building and returns its instance.
310    pub fn build_node(self, ctx: &BuildContext) -> UiNode {
311        let stack_panel = WrapPanel {
312            widget: self.widget_builder.build(ctx),
313            orientation: self.orientation.unwrap_or(Orientation::Vertical).into(),
314            lines: Default::default(),
315        };
316
317        UiNode::new(stack_panel)
318    }
319
320    /// Finishes wrap panel building, adds it to the user interface and returns its handle.
321    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
322        ctx.add_node(self.build_node(ctx))
323    }
324}
325
326#[cfg(test)]
327mod test {
328    use crate::wrap_panel::WrapPanelBuilder;
329    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
330
331    #[test]
332    fn test_deletion() {
333        test_widget_deletion(|ctx| WrapPanelBuilder::new(WidgetBuilder::new()).build(ctx));
334    }
335}