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}