fyrox_ui/stack_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//! Stack panel orders its children widgets linearly; either top-to-bottom or left-to-right. See [`StackPanel`] docs for
22//! more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::{
27 core::{
28 algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
29 visitor::prelude::*,
30 },
31 define_constructor,
32 message::{MessageDirection, UiMessage},
33 widget::{Widget, WidgetBuilder},
34 BuildContext, Control, Orientation, UiNode, UserInterface,
35};
36
37use fyrox_core::uuid_provider;
38use fyrox_core::variable::InheritableVariable;
39use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
40use fyrox_graph::BaseSceneGraph;
41use std::ops::{Deref, DerefMut};
42
43/// A set of possible [`StackPanel`] widget messages.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum StackPanelMessage {
46 /// The message is used to change orientation of the stack panel.
47 Orientation(Orientation),
48}
49
50impl StackPanelMessage {
51 define_constructor!(
52 /// Creates [`StackPanelMessage::Orientation`] message.
53 StackPanelMessage:Orientation => fn orientation(Orientation), layout: false
54 );
55}
56
57/// Stack Panels are one of several methods to position multiple widgets in relation to each other. A Stack Panel Widget
58/// orders its children widgets linearly, aka in a stack of widgets, based on the order the widgets were added as children.
59/// So the first widget added will be at the top or left most position, while each additional widget will descend from top to
60/// bottom or continue from left most to right most. The below example code places 3 text widgets into a vertical stack:
61///
62/// ```rust,no_run
63/// # use fyrox_ui::{
64/// # UiNode, core::pool::Handle,
65/// # BuildContext,
66/// # widget::WidgetBuilder,
67/// # text::TextBuilder,
68/// # stack_panel::StackPanelBuilder,
69/// # };
70/// fn create_stack_panel(ctx: &mut BuildContext) -> Handle<UiNode> {
71/// StackPanelBuilder::new(
72/// WidgetBuilder::new()
73/// .with_child(
74/// TextBuilder::new(WidgetBuilder::new())
75/// .with_text("Top")
76/// .build(ctx)
77/// )
78/// .with_child(
79/// TextBuilder::new(WidgetBuilder::new())
80/// .with_text("Middle")
81/// .build(ctx)
82/// )
83/// .with_child(
84/// TextBuilder::new(WidgetBuilder::new())
85/// .with_text("Bottom")
86/// .build(ctx)
87/// )
88/// )
89/// .build(ctx)
90///
91/// }
92/// ```
93///
94/// As you can see from the example, creating a Stack Panel uses the standard method for creating widgets. Create a new
95/// [`StackPanelBuilder`] and provide it with a new [`WidgetBuilder`]. Adding widgets to the stack is done by adding children to
96/// the StackBuilder's [`WidgetBuilder`].
97///
98/// ## Stack Panel Orientation
99///
100/// As has been indicated, stack panels can be oriented to order its children either Vertical (from top to bottom), or
101/// Horizontal (left to most). This is done using the [`StackPanelBuilder::with_orientation`] function providing it
102/// with a [`Orientation`] enum value. **By default** all stack panels has [`Orientation::Vertical`].
103///
104/// ```rust,no_run
105/// # use fyrox_ui::{
106/// # Orientation,
107/// # BuildContext,
108/// # widget::WidgetBuilder,
109/// # stack_panel::StackPanelBuilder,
110/// # };
111///
112/// # fn build(ctx: &mut BuildContext) {
113/// StackPanelBuilder::new(
114/// WidgetBuilder::new()
115/// )
116/// .with_orientation(Orientation::Horizontal)
117/// .build(ctx);
118/// # }
119/// ```
120#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
121#[reflect(derived_type = "UiNode")]
122pub struct StackPanel {
123 /// Base widget of the stack panel.
124 pub widget: Widget,
125 /// Current orientation of the stack panel.
126 pub orientation: InheritableVariable<Orientation>,
127}
128
129impl ConstructorProvider<UiNode, UserInterface> for StackPanel {
130 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
131 GraphNodeConstructor::new::<Self>()
132 .with_variant("Stack Panel", |ui| {
133 StackPanelBuilder::new(WidgetBuilder::new().with_name("Stack Panel"))
134 .build(&mut ui.build_ctx())
135 .into()
136 })
137 .with_group("Layout")
138 }
139}
140
141crate::define_widget_deref!(StackPanel);
142
143uuid_provider!(StackPanel = "d868f554-a2c5-4280-abfc-396d10a0e1ed");
144
145impl Control for StackPanel {
146 fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
147 let mut child_constraint = Vector2::new(f32::INFINITY, f32::INFINITY);
148
149 match *self.orientation {
150 Orientation::Vertical => {
151 child_constraint.x = available_size.x;
152
153 if !self.widget.width().is_nan() {
154 child_constraint.x = self.widget.width();
155 }
156
157 child_constraint.x = child_constraint.x.clamp(self.min_width(), self.max_width());
158 }
159 Orientation::Horizontal => {
160 child_constraint.y = available_size.y;
161
162 if !self.widget.height().is_nan() {
163 child_constraint.y = self.widget.height();
164 }
165
166 child_constraint.y = child_constraint
167 .y
168 .clamp(self.min_height(), self.max_height());
169 }
170 }
171
172 let mut measured_size = Vector2::default();
173
174 for child_handle in self.widget.children() {
175 ui.measure_node(*child_handle, child_constraint);
176
177 let child = ui.node(*child_handle);
178 let desired = child.desired_size();
179 match *self.orientation {
180 Orientation::Vertical => {
181 if desired.x > measured_size.x {
182 measured_size.x = desired.x;
183 }
184 measured_size.y += desired.y;
185 }
186 Orientation::Horizontal => {
187 measured_size.x += desired.x;
188 if desired.y > measured_size.y {
189 measured_size.y = desired.y;
190 }
191 }
192 }
193 }
194
195 measured_size
196 }
197
198 fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
199 let mut width = final_size.x;
200 let mut height = final_size.y;
201
202 match *self.orientation {
203 Orientation::Vertical => height = 0.0,
204 Orientation::Horizontal => width = 0.0,
205 }
206
207 for child_handle in self.widget.children() {
208 let child = ui.node(*child_handle);
209 match *self.orientation {
210 Orientation::Vertical => {
211 let child_bounds = Rect::new(
212 0.0,
213 height,
214 width.max(child.desired_size().x),
215 child.desired_size().y,
216 );
217 ui.arrange_node(*child_handle, &child_bounds);
218 width = width.max(child.desired_size().x);
219 height += child.desired_size().y;
220 }
221 Orientation::Horizontal => {
222 let child_bounds = Rect::new(
223 width,
224 0.0,
225 child.desired_size().x,
226 height.max(child.desired_size().y),
227 );
228 ui.arrange_node(*child_handle, &child_bounds);
229 width += child.desired_size().x;
230 height = height.max(child.desired_size().y);
231 }
232 }
233 }
234
235 match *self.orientation {
236 Orientation::Vertical => {
237 height = height.max(final_size.y);
238 }
239 Orientation::Horizontal => {
240 width = width.max(final_size.x);
241 }
242 }
243
244 Vector2::new(width, height)
245 }
246
247 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
248 self.widget.handle_routed_message(ui, message);
249
250 if message.destination() == self.handle && message.direction() == MessageDirection::ToWidget
251 {
252 if let Some(StackPanelMessage::Orientation(orientation)) = message.data() {
253 if *orientation != *self.orientation {
254 self.orientation.set_value_and_mark_modified(*orientation);
255 self.invalidate_layout();
256 }
257 }
258 }
259 }
260}
261
262/// Stack panel builders creates [`StackPanel`] widgets and registers them in the user interface.
263pub struct StackPanelBuilder {
264 widget_builder: WidgetBuilder,
265 orientation: Option<Orientation>,
266}
267
268impl StackPanelBuilder {
269 /// Creates new stack panel builder with the base widget builder.
270 pub fn new(widget_builder: WidgetBuilder) -> Self {
271 Self {
272 widget_builder,
273 orientation: None,
274 }
275 }
276
277 /// Sets the desired orientation of the stack panel.
278 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
279 self.orientation = Some(orientation);
280 self
281 }
282
283 /// Finishes stack panel building.
284 pub fn build_stack_panel(self, ctx: &BuildContext) -> StackPanel {
285 StackPanel {
286 widget: self.widget_builder.build(ctx),
287 orientation: self.orientation.unwrap_or(Orientation::Vertical).into(),
288 }
289 }
290
291 /// Finishes stack panel building and wraps the result in a UI node.
292 pub fn build_node(self, ctx: &BuildContext) -> UiNode {
293 UiNode::new(self.build_stack_panel(ctx))
294 }
295
296 /// Finishes stack panel building and adds the new stack panel widget instance to the user interface and
297 /// returns its handle.
298 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
299 ctx.add_node(self.build_node(ctx))
300 }
301}
302
303#[cfg(test)]
304mod test {
305 use crate::stack_panel::StackPanelBuilder;
306 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
307
308 #[test]
309 fn test_deletion() {
310 test_widget_deletion(|ctx| StackPanelBuilder::new(WidgetBuilder::new()).build(ctx));
311 }
312}