fyrox_ui/expander.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//! Expander is a simple container that has a header and collapsible/expandable content zone. It is used to
22//! create collapsible regions with headers. See [`Expander`] docs for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::{
27 check_box::{CheckBoxBuilder, CheckBoxMessage},
28 core::pool::Handle,
29 core::{reflect::prelude::*, type_traits::prelude::*, visitor::prelude::*},
30 define_constructor,
31 grid::{Column, GridBuilder, Row},
32 message::{MessageDirection, UiMessage},
33 utils::{make_arrow, ArrowDirection},
34 widget::{Widget, WidgetBuilder, WidgetMessage},
35 BuildContext, Control, UiNode, UserInterface, VerticalAlignment,
36};
37use fyrox_core::uuid_provider;
38use fyrox_core::variable::InheritableVariable;
39use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
40use std::ops::{Deref, DerefMut};
41
42/// A set messages that can be used to either alternate the state of an [`Expander`] widget, or to listen for
43/// state changes.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum ExpanderMessage {
46 /// A message, that could be used to either switch expander state (with [`MessageDirection::ToWidget`]) or
47 /// to get its new state [`MessageDirection::FromWidget`].
48 Expand(bool),
49}
50
51impl ExpanderMessage {
52 define_constructor!(
53 /// Creates [`ExpanderMessage::Expand`] message.
54 ExpanderMessage:Expand => fn expand(bool), layout: false
55 );
56}
57
58/// Expander is a simple container that has a header and collapsible/expandable content zone. It is used to
59/// create collapsible regions with headers.
60///
61/// ## Examples
62///
63/// The following example creates a simple expander with a textual header and a stack panel widget with few
64/// buttons a content:
65///
66/// ```rust
67/// # use fyrox_ui::{
68/// # button::ButtonBuilder, core::pool::Handle, expander::ExpanderBuilder,
69/// # stack_panel::StackPanelBuilder, text::TextBuilder, widget::WidgetBuilder, BuildContext,
70/// # UiNode,
71/// # };
72/// #
73/// fn create_expander(ctx: &mut BuildContext) -> Handle<UiNode> {
74/// ExpanderBuilder::new(WidgetBuilder::new())
75/// // Header is visible all the time.
76/// .with_header(
77/// TextBuilder::new(WidgetBuilder::new())
78/// .with_text("Foobar")
79/// .build(ctx),
80/// )
81/// // Define a content of collapsible area.
82/// .with_content(
83/// StackPanelBuilder::new(
84/// WidgetBuilder::new()
85/// .with_child(
86/// ButtonBuilder::new(WidgetBuilder::new())
87/// .with_text("Button 1")
88/// .build(ctx),
89/// )
90/// .with_child(
91/// ButtonBuilder::new(WidgetBuilder::new())
92/// .with_text("Button 2")
93/// .build(ctx),
94/// ),
95/// )
96/// .build(ctx),
97/// )
98/// .build(ctx)
99/// }
100/// ```
101///
102/// ## Customization
103///
104/// It is possible to completely change the arrow of the header of the expander. By default, the arrow consists
105/// of [`crate::check_box::CheckBox`] widget. By changing the arrow, you can customize the look of the header.
106/// For example, you can set the new check box with image check marks, which will use custom graphics:
107///
108/// ```rust
109/// # use fyrox_ui::{
110/// # check_box::CheckBoxBuilder, core::pool::Handle, expander::ExpanderBuilder,
111/// # image::ImageBuilder, widget::WidgetBuilder, BuildContext, UiNode,
112/// # };
113/// #
114/// fn create_expander(ctx: &mut BuildContext) -> Handle<UiNode> {
115/// ExpanderBuilder::new(WidgetBuilder::new())
116/// .with_checkbox(
117/// CheckBoxBuilder::new(WidgetBuilder::new())
118/// .with_check_mark(
119/// ImageBuilder::new(WidgetBuilder::new().with_height(16.0).with_height(16.0))
120/// .with_opt_texture(None) // Set this to required image.
121/// .build(ctx),
122/// )
123/// .with_uncheck_mark(
124/// ImageBuilder::new(WidgetBuilder::new().with_height(16.0).with_height(16.0))
125/// .with_opt_texture(None) // Set this to required image.
126/// .build(ctx),
127/// )
128/// .build(ctx),
129/// )
130/// // The rest is omitted.
131/// .build(ctx)
132/// }
133/// ```
134///
135/// ## Messages
136///
137/// Use [`ExpanderMessage::Expand`] message to catch the moment when its state changes:
138///
139/// ```rust
140/// # use fyrox_ui::{core::pool::Handle, expander::ExpanderMessage, message::{MessageDirection, UiMessage}};
141/// fn on_ui_message(message: &UiMessage) {
142/// let your_expander_handle = Handle::NONE;
143/// if let Some(ExpanderMessage::Expand(expanded)) = message.data() {
144/// if message.destination() == your_expander_handle && message.direction() == MessageDirection::FromWidget {
145/// println!(
146/// "{} expander has changed its state to {}!",
147/// message.destination(),
148/// expanded
149/// );
150/// }
151/// }
152/// }
153/// ```
154///
155/// To switch expander state at runtime, send [`ExpanderMessage::Expand`] to your Expander widget instance with
156/// [`MessageDirection::ToWidget`].
157#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
158pub struct Expander {
159 /// Base widget of the expander.
160 pub widget: Widget,
161 /// Current content of the expander.
162 pub content: InheritableVariable<Handle<UiNode>>,
163 /// Current expander check box of the expander.
164 pub expander: InheritableVariable<Handle<UiNode>>,
165 /// A flag, that indicates whether the expander is expanded or collapsed.
166 pub is_expanded: InheritableVariable<bool>,
167}
168
169impl ConstructorProvider<UiNode, UserInterface> for Expander {
170 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
171 GraphNodeConstructor::new::<Self>()
172 .with_variant("Expander", |ui| {
173 ExpanderBuilder::new(WidgetBuilder::new().with_name("Expander"))
174 .build(&mut ui.build_ctx())
175 .into()
176 })
177 .with_group("Visual")
178 }
179}
180
181crate::define_widget_deref!(Expander);
182
183uuid_provider!(Expander = "24976179-b338-4c55-84c3-72d21663efd2");
184
185impl Control for Expander {
186 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
187 if let Some(&ExpanderMessage::Expand(expand)) = message.data::<ExpanderMessage>() {
188 if message.destination() == self.handle()
189 && message.direction() == MessageDirection::ToWidget
190 && *self.is_expanded != expand
191 {
192 // Switch state of expander.
193 ui.send_message(CheckBoxMessage::checked(
194 *self.expander,
195 MessageDirection::ToWidget,
196 Some(expand),
197 ));
198 // Show or hide content.
199 ui.send_message(WidgetMessage::visibility(
200 *self.content,
201 MessageDirection::ToWidget,
202 expand,
203 ));
204 self.is_expanded.set_value_and_mark_modified(expand);
205 }
206 } else if let Some(CheckBoxMessage::Check(value)) = message.data::<CheckBoxMessage>() {
207 if message.destination() == *self.expander
208 && message.direction() == MessageDirection::FromWidget
209 {
210 ui.send_message(ExpanderMessage::expand(
211 self.handle,
212 MessageDirection::ToWidget,
213 value.unwrap_or(false),
214 ));
215 }
216 }
217 self.widget.handle_routed_message(ui, message);
218 }
219}
220
221/// Expander builder allows you to create [`Expander`] widgets and add them to user interface.
222pub struct ExpanderBuilder {
223 /// Base builder.
224 pub widget_builder: WidgetBuilder,
225 header: Handle<UiNode>,
226 content: Handle<UiNode>,
227 check_box: Handle<UiNode>,
228 is_expanded: bool,
229 expander_column: Option<Column>,
230}
231
232impl ExpanderBuilder {
233 /// Creates a new expander builder.
234 pub fn new(widget_builder: WidgetBuilder) -> Self {
235 Self {
236 widget_builder,
237 header: Handle::NONE,
238 content: Handle::NONE,
239 check_box: Default::default(),
240 is_expanded: true,
241 expander_column: None,
242 }
243 }
244
245 /// Sets the desired header of the expander.
246 pub fn with_header(mut self, header: Handle<UiNode>) -> Self {
247 self.header = header;
248 self
249 }
250
251 /// Sets the desired content of the expander.
252 pub fn with_content(mut self, content: Handle<UiNode>) -> Self {
253 self.content = content;
254 self
255 }
256
257 /// Sets the desired state of the expander.
258 pub fn with_expanded(mut self, expanded: bool) -> Self {
259 self.is_expanded = expanded;
260 self
261 }
262
263 /// Sets the desired check box (arrow part) of the expander.
264 pub fn with_checkbox(mut self, check_box: Handle<UiNode>) -> Self {
265 self.check_box = check_box;
266 self
267 }
268
269 /// Sets the desired expander column properties of the expander.
270 pub fn with_expander_column(mut self, expander_column: Column) -> Self {
271 self.expander_column = Some(expander_column);
272 self
273 }
274
275 /// Finishes widget building and adds it to the user interface, returning a handle to the new instance.
276 pub fn build(self, ctx: &mut BuildContext<'_>) -> Handle<UiNode> {
277 let expander = if self.check_box.is_some() {
278 self.check_box
279 } else {
280 CheckBoxBuilder::new(
281 WidgetBuilder::new().with_vertical_alignment(VerticalAlignment::Center),
282 )
283 .with_check_mark(make_arrow(ctx, ArrowDirection::Bottom, 8.0))
284 .with_uncheck_mark(make_arrow(ctx, ArrowDirection::Right, 8.0))
285 .checked(Some(self.is_expanded))
286 .build(ctx)
287 };
288
289 ctx[expander].set_row(0).set_column(0);
290
291 if self.header.is_some() {
292 ctx[self.header].set_row(0).set_column(1);
293 }
294
295 let grid = GridBuilder::new(
296 WidgetBuilder::new()
297 .with_child(expander)
298 .with_child(self.header),
299 )
300 .add_row(Row::auto())
301 .add_column(self.expander_column.unwrap_or_else(Column::auto))
302 .add_column(Column::stretch())
303 .build(ctx);
304
305 if self.content.is_some() {
306 ctx[self.content]
307 .set_row(1)
308 .set_column(0)
309 .set_visibility(self.is_expanded);
310 }
311
312 let e = UiNode::new(Expander {
313 widget: self
314 .widget_builder
315 .with_child(
316 GridBuilder::new(
317 WidgetBuilder::new()
318 .with_child(grid)
319 .with_child(self.content),
320 )
321 .add_column(Column::stretch())
322 .add_row(Row::auto())
323 .add_row(Row::stretch())
324 .build(ctx),
325 )
326 .build(ctx),
327 content: self.content.into(),
328 expander: expander.into(),
329 is_expanded: self.is_expanded.into(),
330 });
331 ctx.add_node(e)
332 }
333}
334
335#[cfg(test)]
336mod test {
337 use crate::expander::ExpanderBuilder;
338 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
339
340 #[test]
341 fn test_deletion() {
342 test_widget_deletion(|ctx| ExpanderBuilder::new(WidgetBuilder::new()).build(ctx));
343 }
344}