Skip to main content

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