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}