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};
37use fyrox_core::uuid_provider;
38use fyrox_core::variable::InheritableVariable;
39use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
40use fyrox_graph::BaseSceneGraph;
41use std::{
42    cell::RefCell,
43    ops::{Deref, DerefMut, Range},
44};
45
46/// A set of possible [`WrapPanel`] widget messages.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum WrapPanelMessage {
49    /// The message is used to change orientation of the wrap panel.
50    Orientation(Orientation),
51}
52
53impl WrapPanelMessage {
54    define_constructor!(
55        /// Creates [`WrapPanelMessage::Orientation`] message.
56        WrapPanelMessage:Orientation => fn orientation(Orientation), layout: false
57    );
58}
59
60/// Wrap panel is used to stack children widgets either in vertical or horizontal direction with overflow - every widget
61/// that does not have enough space on current line, will automatically be placed on the next line (either vertical or
62/// horizontal, depending on the orientation).
63///
64/// ## How to create
65///
66/// Use `WrapPanelBuilder` to create new wrap panel instance:
67///
68/// ```rust,no_run
69/// # use fyrox_ui::{
70/// #     core::pool::Handle,
71/// #     widget::WidgetBuilder, wrap_panel::WrapPanelBuilder, BuildContext, Orientation, UiNode,
72/// # };
73/// #
74/// fn create_wrap_panel(ctx: &mut BuildContext) -> Handle<UiNode> {
75///     WrapPanelBuilder::new(WidgetBuilder::new())
76///         .with_orientation(Orientation::Horizontal)
77///         .build(ctx)
78/// }
79/// ```
80///
81/// All widgets, that needs to be arranged, should be direct children of the wrap panel. Use [`WidgetBuilder::with_children`]
82/// or [`WidgetBuilder::with_child`] to add children nodes.
83///
84/// ## Orientation
85///
86/// Wrap panel can stack your widgets either in vertical or horizontal direction. Use `.with_orientation` while building
87/// the panel to switch orientation to desired.
88#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
89pub struct WrapPanel {
90    /// Base widget of the wrap panel.
91    pub widget: Widget,
92    /// Current orientation of the wrap panel.
93    pub orientation: InheritableVariable<Orientation>,
94    /// Internal lines storage.
95    #[visit(skip)]
96    #[reflect(hidden)]
97    pub lines: RefCell<Vec<Line>>,
98}
99
100impl ConstructorProvider<UiNode, UserInterface> for WrapPanel {
101    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
102        GraphNodeConstructor::new::<Self>()
103            .with_variant("Wrap Panel", |ui| {
104                WrapPanelBuilder::new(WidgetBuilder::new().with_name("Wrap Panel"))
105                    .build(&mut ui.build_ctx())
106                    .into()
107            })
108            .with_group("Layout")
109    }
110}
111
112crate::define_widget_deref!(WrapPanel);
113
114/// Represents a single line (either vertical or horizontal) with arranged widgets.
115#[derive(Clone, Debug)]
116pub struct Line {
117    /// Indices of the children widgets that belongs to this line.
118    pub children: Range<usize>,
119    /// Bounds of this line.
120    pub bounds: Rect<f32>,
121}
122
123impl Default for Line {
124    fn default() -> Self {
125        Self {
126            children: 0..0,
127            bounds: Default::default(),
128        }
129    }
130}
131
132uuid_provider!(WrapPanel = "f488ab8e-8f8b-473c-a450-5ac33f1afb39");
133
134impl Control for WrapPanel {
135    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
136        let mut measured_size: Vector2<f32> = Vector2::default();
137        let mut line_size = Vector2::default();
138        for child_handle in self.widget.children() {
139            let child = ui.node(*child_handle);
140            ui.measure_node(*child_handle, available_size);
141            let desired = child.desired_size();
142            match *self.orientation {
143                Orientation::Vertical => {
144                    if line_size.y + desired.y > available_size.y {
145                        // Commit column.
146                        measured_size.y = measured_size.y.max(line_size.y);
147                        measured_size.x += line_size.x;
148                        line_size = Vector2::default();
149                    }
150                    line_size.x = line_size.x.max(desired.x);
151                    line_size.y += desired.y;
152                }
153                Orientation::Horizontal => {
154                    if line_size.x + desired.x > available_size.x {
155                        // Commit row.
156                        measured_size.x = measured_size.x.max(line_size.x);
157                        measured_size.y += line_size.y;
158                        line_size = Vector2::default();
159                    }
160                    line_size.x += desired.x;
161                    line_size.y = line_size.y.max(desired.y);
162                }
163            }
164        }
165
166        // Commit rest.
167        match *self.orientation {
168            Orientation::Vertical => {
169                measured_size.y = measured_size.y.max(line_size.y);
170                measured_size.x += line_size.x;
171            }
172            Orientation::Horizontal => {
173                measured_size.x = measured_size.x.max(line_size.x);
174                measured_size.y += line_size.y;
175            }
176        }
177
178        measured_size
179    }
180
181    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
182        // First pass - arrange lines.
183        let mut lines = self.lines.borrow_mut();
184        lines.clear();
185        let mut line = Line::default();
186        for child_handle in self.widget.children() {
187            let child = ui.node(*child_handle);
188            let desired = child.desired_size();
189            match *self.orientation {
190                Orientation::Vertical => {
191                    if line.bounds.h() + desired.y > final_size.y {
192                        // Commit column.
193                        lines.push(line.clone());
194                        // Advance column.
195                        line.bounds.position.x += line.bounds.w();
196                        line.bounds.position.y = 0.0;
197                        line.bounds.size.x = desired.x;
198                        line.bounds.size.y = desired.y;
199                        // Reset children.
200                        line.children.start = line.children.end;
201                        line.children.end = line.children.start + 1;
202                    } else {
203                        line.bounds.size.y += desired.y;
204                        line.bounds.size.x = line.bounds.w().max(desired.x);
205                        line.children.end += 1;
206                    }
207                }
208                Orientation::Horizontal => {
209                    if line.bounds.w() + desired.x > final_size.x {
210                        // Commit row.
211                        lines.push(line.clone());
212                        // Advance row.
213                        line.bounds.position.x = 0.0;
214                        line.bounds.position.y += line.bounds.h();
215                        line.bounds.size.x = desired.x;
216                        line.bounds.size.y = desired.y;
217                        // Reset children.
218                        line.children.start = line.children.end;
219                        line.children.end = line.children.start + 1;
220                    } else {
221                        line.bounds.size.x += desired.x;
222                        line.bounds.size.y = line.bounds.h().max(desired.y);
223                        line.children.end += 1;
224                    }
225                }
226            }
227        }
228
229        // Commit rest.
230        lines.push(line);
231
232        // Second pass - arrange children of lines.
233        let mut full_size = Vector2::default();
234        for line in lines.iter() {
235            let mut cursor = line.bounds.position;
236            for child_index in line.children.clone() {
237                let child_handle = self.children()[child_index];
238                let child = ui.node(child_handle);
239                let desired = child.desired_size();
240                match *self.orientation {
241                    Orientation::Vertical => {
242                        let child_bounds =
243                            Rect::new(line.bounds.x(), cursor.y, line.bounds.w(), desired.y);
244                        ui.arrange_node(child_handle, &child_bounds);
245                        cursor.y += desired.y;
246                    }
247                    Orientation::Horizontal => {
248                        let child_bounds =
249                            Rect::new(cursor.x, line.bounds.y(), desired.x, line.bounds.h());
250                        ui.arrange_node(child_handle, &child_bounds);
251                        cursor.x += desired.x;
252                    }
253                }
254            }
255            match *self.orientation {
256                Orientation::Vertical => {
257                    full_size.x += line.bounds.w();
258                    full_size.y = final_size.y.max(line.bounds.h());
259                }
260                Orientation::Horizontal => {
261                    full_size.x = final_size.x.max(line.bounds.w());
262                    full_size.y += line.bounds.h();
263                }
264            }
265        }
266
267        full_size
268    }
269
270    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
271        self.widget.handle_routed_message(ui, message);
272
273        if message.destination() == self.handle && message.direction() == MessageDirection::ToWidget
274        {
275            if let Some(WrapPanelMessage::Orientation(orientation)) = message.data() {
276                if *orientation != *self.orientation {
277                    self.orientation.set_value_and_mark_modified(*orientation);
278                    self.invalidate_layout();
279                }
280            }
281        }
282    }
283}
284
285/// Wrap panel builder creates [`WrapPanel`] widget and adds it to the user interface.
286pub struct WrapPanelBuilder {
287    widget_builder: WidgetBuilder,
288    orientation: Option<Orientation>,
289}
290
291impl WrapPanelBuilder {
292    /// Creates a new wrap panel builder.
293    pub fn new(widget_builder: WidgetBuilder) -> Self {
294        Self {
295            widget_builder,
296            orientation: None,
297        }
298    }
299
300    /// Sets the desired orientation of the wrap panel.
301    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
302        self.orientation = Some(orientation);
303        self
304    }
305
306    /// Finishes wrap panel building and returns its instance.
307    pub fn build_node(self, ctx: &BuildContext) -> UiNode {
308        let stack_panel = WrapPanel {
309            widget: self.widget_builder.build(ctx),
310            orientation: self.orientation.unwrap_or(Orientation::Vertical).into(),
311            lines: Default::default(),
312        };
313
314        UiNode::new(stack_panel)
315    }
316
317    /// Finishes wrap panel building, adds it to the user interface and returns its handle.
318    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
319        ctx.add_node(self.build_node(ctx))
320    }
321}
322
323#[cfg(test)]
324mod test {
325    use crate::wrap_panel::WrapPanelBuilder;
326    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
327
328    #[test]
329    fn test_deletion() {
330        test_widget_deletion(|ctx| WrapPanelBuilder::new(WidgetBuilder::new()).build(ctx));
331    }
332}