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