Skip to main content

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