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}