fyrox_ui/
check_box.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//! Checkbox is a UI widget that have three states - `Checked`, `Unchecked` and `Undefined`. In most cases it is used
22//! only with two values which fits in `bool` type. Third, undefined, state is used for specific situations when your
23//! data have such state. See [`CheckBox`] docs for more info and usage examples.
24
25#![warn(missing_docs)]
26
27use crate::{
28    border::BorderBuilder,
29    brush::Brush,
30    core::{
31        algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
32        variable::InheritableVariable, visitor::prelude::*,
33    },
34    define_constructor,
35    grid::{Column, GridBuilder, Row},
36    message::{KeyCode, MessageDirection, UiMessage},
37    style::{resource::StyleResourceExt, Style},
38    vector_image::{Primitive, VectorImageBuilder},
39    widget::{Widget, WidgetBuilder, WidgetMessage},
40    BuildContext, Control, HorizontalAlignment, MouseButton, Thickness, UiNode, UserInterface,
41    VerticalAlignment,
42};
43use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
44use std::ops::{Deref, DerefMut};
45
46/// A set of possible check box messages.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum CheckBoxMessage {
49    /// Emitted when the check box changed its state. Could also be used to modify check box state.
50    Check(Option<bool>),
51}
52
53impl CheckBoxMessage {
54    define_constructor!(
55        /// Creates [`CheckBoxMessage::checked`] message.
56        CheckBoxMessage:Check => fn checked(Option<bool>), layout: false
57    );
58}
59
60/// Checkbox is a UI widget that have three states - `Checked`, `Unchecked` and `Undefined`. In most cases it is used
61/// only with two values which fits in `bool` type. Third, undefined, state is used for specific situations when your
62/// data have such state.
63///
64/// ## How to create
65///
66/// To create a checkbox you should do something like this:
67///
68/// ```rust,no_run
69/// # use fyrox_ui::{
70/// #     core::pool::Handle,
71/// #     check_box::CheckBoxBuilder, widget::WidgetBuilder, UiNode, UserInterface
72/// # };
73/// fn create_checkbox(ui: &mut UserInterface) -> Handle<UiNode> {
74///     CheckBoxBuilder::new(WidgetBuilder::new())
75///         // A custom value can be set during initialization.
76///         .checked(Some(true))
77///         .build(&mut ui.build_ctx())
78/// }
79/// ```
80///
81/// The above code will create a checkbox without any textual info, but usually checkboxes have some useful info
82/// near them. To create such checkbox, you could use [`CheckBoxBuilder::with_content`] method which accepts any widget handle.
83/// For checkbox with text, you could use [`crate::text::TextBuilder`] to create textual content, for checkbox with image - use
84/// [`crate::image::ImageBuilder`]. As already said, you're free to use any widget handle there.
85///
86/// Here's an example of checkbox with textual content.
87///
88/// ```rust,no_run
89/// # use fyrox_ui::{
90/// #     core::pool::Handle,
91/// #     check_box::CheckBoxBuilder, text::TextBuilder, widget::WidgetBuilder, UiNode,
92/// #     UserInterface,
93/// # };
94/// fn create_checkbox(ui: &mut UserInterface) -> Handle<UiNode> {
95///     let ctx = &mut ui.build_ctx();
96///
97///     CheckBoxBuilder::new(WidgetBuilder::new())
98///         // A custom value can be set during initialization.
99///         .checked(Some(true))
100///         .with_content(
101///             TextBuilder::new(WidgetBuilder::new())
102///                 .with_text("This is a checkbox")
103///                 .build(ctx),
104///         )
105///         .build(ctx)
106/// }
107/// ```
108///
109/// ## Message handling
110///
111/// Checkboxes are not static widget and have multiple states. To handle a message from a checkbox, you need to handle
112/// the [`CheckBoxMessage::Check`] message. To do so, you can do something like this:
113///
114/// ```rust,no_run
115/// # use fyrox_ui::{
116/// #     core::pool::Handle,
117/// #     check_box::CheckBoxMessage, message::UiMessage, UiNode
118/// # };
119/// #
120/// # struct Foo {
121/// #     checkbox: Handle<UiNode>,
122/// # }
123/// #
124/// # impl Foo {
125/// fn on_ui_message(
126///     &mut self,
127///     message: &UiMessage,
128/// ) {
129///     if let Some(CheckBoxMessage::Check(value)) = message.data() {
130///         if message.destination() == self.checkbox {
131///             //
132///             // Insert your clicking handling code here.
133///             //
134///         }
135///     }
136/// }
137/// # }
138/// ```
139///
140/// Keep in mind that checkbox (as any other widget) generates [`WidgetMessage`] instances. You can catch them too and
141/// do a custom handling if you need.
142///
143/// ## Theme
144///
145/// Checkbox can be fully customized to have any look you want, there are few methods that will help you with
146/// customization:
147///
148/// 1) [`CheckBoxBuilder::with_content`] - sets the content that will be shown near the checkbox.
149/// 2) [`CheckBoxBuilder::with_check_mark`] - sets the widget that will be used as checked icon.
150/// 3) [`CheckBoxBuilder::with_uncheck_mark`] - sets the widget that will be used as unchecked icon.
151/// 4) [`CheckBoxBuilder::with_undefined_mark`] - sets the widget that will be used as undefined icon.
152#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
153#[type_uuid(id = "3a866ba8-7682-4ce7-954a-46360f5837dc")]
154#[reflect(derived_type = "UiNode")]
155pub struct CheckBox {
156    /// Base widget of the check box.
157    pub widget: Widget,
158    /// Current state of the check box.
159    pub checked: InheritableVariable<Option<bool>>,
160    /// Check mark that is used when the state is `Some(true)`.
161    pub check_mark: InheritableVariable<Handle<UiNode>>,
162    /// Check mark that is used when the state is `Some(false)`.
163    pub uncheck_mark: InheritableVariable<Handle<UiNode>>,
164    /// Check mark that is used when the state is `None`.
165    pub undefined_mark: InheritableVariable<Handle<UiNode>>,
166}
167
168impl CheckBox {
169    /// A name of style property, that defines corner radius of a checkbox.
170    pub const CORNER_RADIUS: &'static str = "CheckBox.CornerRadius";
171    /// A name of style property, that defines border thickness of a checkbox.
172    pub const BORDER_THICKNESS: &'static str = "CheckBox.BorderThickness";
173    /// A name of style property, that defines border thickness of a checkbox.
174    pub const CHECK_MARK_SIZE: &'static str = "CheckBox.CheckMarkSize";
175
176    /// Returns a style of the widget. This style contains only widget-specific properties.
177    pub fn style() -> Style {
178        Style::default()
179            .with(Self::CORNER_RADIUS, 4.0f32)
180            .with(Self::BORDER_THICKNESS, Thickness::uniform(1.0))
181            .with(Self::CHECK_MARK_SIZE, 7.0f32)
182    }
183}
184
185impl ConstructorProvider<UiNode, UserInterface> for CheckBox {
186    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
187        GraphNodeConstructor::new::<Self>()
188            .with_variant("CheckBox", |ui| {
189                CheckBoxBuilder::new(WidgetBuilder::new().with_name("CheckBox"))
190                    .build(&mut ui.build_ctx())
191                    .into()
192            })
193            .with_group("Input")
194    }
195}
196
197crate::define_widget_deref!(CheckBox);
198
199impl Control for CheckBox {
200    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
201        self.widget.handle_routed_message(ui, message);
202
203        if let Some(msg) = message.data::<WidgetMessage>() {
204            match msg {
205                WidgetMessage::MouseDown { button, .. } => {
206                    if *button == MouseButton::Left
207                        && (message.destination() == self.handle()
208                            || self.widget.has_descendant(message.destination(), ui))
209                    {
210                        ui.capture_mouse(self.handle());
211                    }
212                }
213                WidgetMessage::MouseUp { button, .. } => {
214                    if *button == MouseButton::Left
215                        && (message.destination() == self.handle()
216                            || self.widget.has_descendant(message.destination(), ui))
217                    {
218                        ui.release_mouse_capture();
219
220                        if let Some(value) = *self.checked {
221                            // Invert state if it is defined.
222                            ui.send_message(CheckBoxMessage::checked(
223                                self.handle(),
224                                MessageDirection::ToWidget,
225                                Some(!value),
226                            ));
227                        } else {
228                            // Switch from undefined state to checked.
229                            ui.send_message(CheckBoxMessage::checked(
230                                self.handle(),
231                                MessageDirection::ToWidget,
232                                Some(true),
233                            ));
234                        }
235                    }
236                }
237                WidgetMessage::KeyDown(key_code) => {
238                    if !message.handled() && *key_code == KeyCode::Space {
239                        ui.send_message(CheckBoxMessage::checked(
240                            self.handle,
241                            MessageDirection::ToWidget,
242                            self.checked.map(|checked| !checked),
243                        ));
244                        message.set_handled(true);
245                    }
246                }
247                _ => (),
248            }
249        } else if let Some(&CheckBoxMessage::Check(value)) = message.data::<CheckBoxMessage>() {
250            if message.direction() == MessageDirection::ToWidget
251                && message.destination() == self.handle()
252                && *self.checked != value
253            {
254                self.checked.set_value_and_mark_modified(value);
255
256                ui.send_message(message.reverse());
257
258                if self.check_mark.is_some() {
259                    match value {
260                        None => {
261                            ui.send_message(WidgetMessage::visibility(
262                                *self.check_mark,
263                                MessageDirection::ToWidget,
264                                false,
265                            ));
266                            ui.send_message(WidgetMessage::visibility(
267                                *self.uncheck_mark,
268                                MessageDirection::ToWidget,
269                                false,
270                            ));
271                            ui.send_message(WidgetMessage::visibility(
272                                *self.undefined_mark,
273                                MessageDirection::ToWidget,
274                                true,
275                            ));
276                        }
277                        Some(value) => {
278                            ui.send_message(WidgetMessage::visibility(
279                                *self.check_mark,
280                                MessageDirection::ToWidget,
281                                value,
282                            ));
283                            ui.send_message(WidgetMessage::visibility(
284                                *self.uncheck_mark,
285                                MessageDirection::ToWidget,
286                                !value,
287                            ));
288                            ui.send_message(WidgetMessage::visibility(
289                                *self.undefined_mark,
290                                MessageDirection::ToWidget,
291                                false,
292                            ));
293                        }
294                    }
295                }
296            }
297        }
298    }
299}
300
301/// Check box builder creates [`CheckBox`] instances and adds them to the user interface.
302pub struct CheckBoxBuilder {
303    widget_builder: WidgetBuilder,
304    checked: Option<bool>,
305    check_mark: Option<Handle<UiNode>>,
306    uncheck_mark: Option<Handle<UiNode>>,
307    undefined_mark: Option<Handle<UiNode>>,
308    background: Option<Handle<UiNode>>,
309    content: Handle<UiNode>,
310}
311
312impl CheckBoxBuilder {
313    /// Creates new check box builder instance.
314    pub fn new(widget_builder: WidgetBuilder) -> Self {
315        Self {
316            widget_builder,
317            checked: Some(false),
318            check_mark: None,
319            uncheck_mark: None,
320            undefined_mark: None,
321            content: Handle::NONE,
322            background: None,
323        }
324    }
325
326    /// Sets the desired state of the check box.
327    pub fn checked(mut self, value: Option<bool>) -> Self {
328        self.checked = value;
329        self
330    }
331
332    /// Sets the desired check mark when the state is `Some(true)`.
333    pub fn with_check_mark(mut self, check_mark: Handle<UiNode>) -> Self {
334        self.check_mark = Some(check_mark);
335        self
336    }
337
338    /// Sets the desired check mark when the state is `Some(false)`.
339    pub fn with_uncheck_mark(mut self, uncheck_mark: Handle<UiNode>) -> Self {
340        self.uncheck_mark = Some(uncheck_mark);
341        self
342    }
343
344    /// Sets the desired check mark when the state is `None`.
345    pub fn with_undefined_mark(mut self, undefined_mark: Handle<UiNode>) -> Self {
346        self.undefined_mark = Some(undefined_mark);
347        self
348    }
349
350    /// Sets the new content of the check box.
351    pub fn with_content(mut self, content: Handle<UiNode>) -> Self {
352        self.content = content;
353        self
354    }
355
356    /// Sets the desired background widget that will be used a container for check box contents. By
357    /// default, it is a simple border.
358    pub fn with_background(mut self, background: Handle<UiNode>) -> Self {
359        self.background = Some(background);
360        self
361    }
362
363    /// Finishes check box building and adds it to the user interface.
364    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
365        let check_mark = self.check_mark.unwrap_or_else(|| {
366            let size = *ctx.style.property(CheckBox::CHECK_MARK_SIZE);
367            let half_size = size * 0.5;
368
369            BorderBuilder::new(
370                WidgetBuilder::new()
371                    .with_background(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
372                    .with_child(
373                        VectorImageBuilder::new(
374                            WidgetBuilder::new()
375                                .with_vertical_alignment(VerticalAlignment::Center)
376                                .with_horizontal_alignment(HorizontalAlignment::Center)
377                                // Give some padding to ensure primitives don't get too cut off
378                                .with_width(size + 1.0)
379                                .with_height(size + 1.0)
380                                .with_foreground(ctx.style.property(Style::BRUSH_TEXT)),
381                        )
382                        .with_primitives({
383                            vec![
384                                Primitive::Line {
385                                    begin: Vector2::new(0.0, half_size),
386                                    end: Vector2::new(half_size, size),
387                                    thickness: 2.0,
388                                },
389                                Primitive::Line {
390                                    begin: Vector2::new(half_size, size),
391                                    end: Vector2::new(size, 0.0),
392                                    thickness: 2.0,
393                                },
394                            ]
395                        })
396                        .build(ctx),
397                    ),
398            )
399            .with_pad_by_corner_radius(false)
400            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
401            .with_stroke_thickness(Thickness::uniform(0.0).into())
402            .build(ctx)
403        });
404        ctx[check_mark].set_visibility(self.checked.unwrap_or(false));
405
406        let uncheck_mark = self.uncheck_mark.unwrap_or_else(|| {
407            BorderBuilder::new(
408                WidgetBuilder::new()
409                    .with_margin(Thickness::uniform(3.0))
410                    .with_width(10.0)
411                    .with_height(9.0)
412                    .with_background(Brush::Solid(Color::TRANSPARENT).into())
413                    .with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
414            )
415            .with_pad_by_corner_radius(false)
416            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
417            .with_stroke_thickness(Thickness::uniform(0.0).into())
418            .build(ctx)
419        });
420        ctx[uncheck_mark].set_visibility(!self.checked.unwrap_or(true));
421
422        let undefined_mark = self.undefined_mark.unwrap_or_else(|| {
423            BorderBuilder::new(
424                WidgetBuilder::new()
425                    .with_margin(Thickness::uniform(4.0))
426                    .with_background(ctx.style.property(Style::BRUSH_BRIGHT))
427                    .with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
428            )
429            .with_pad_by_corner_radius(false)
430            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
431            .build(ctx)
432        });
433        ctx[undefined_mark].set_visibility(self.checked.is_none());
434
435        if self.content.is_some() {
436            ctx[self.content].set_row(0).set_column(1);
437        }
438
439        let background = self.background.unwrap_or_else(|| {
440            BorderBuilder::new(
441                WidgetBuilder::new()
442                    .with_vertical_alignment(VerticalAlignment::Center)
443                    .with_background(ctx.style.property(Style::BRUSH_DARKEST))
444                    .with_foreground(ctx.style.property(Style::BRUSH_LIGHT)),
445            )
446            .with_pad_by_corner_radius(false)
447            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
448            .with_stroke_thickness(ctx.style.property(CheckBox::BORDER_THICKNESS))
449            .build(ctx)
450        });
451
452        let background_ref = &mut ctx[background];
453        background_ref.set_row(0).set_column(0);
454        if background_ref.min_width() < 0.01 {
455            background_ref.set_min_width(16.0);
456        }
457        if background_ref.min_height() < 0.01 {
458            background_ref.set_min_height(16.0);
459        }
460
461        ctx.link(check_mark, background);
462        ctx.link(uncheck_mark, background);
463        ctx.link(undefined_mark, background);
464
465        let grid = GridBuilder::new(
466            WidgetBuilder::new()
467                .with_child(background)
468                .with_child(self.content),
469        )
470        .add_row(Row::stretch())
471        .add_column(Column::auto())
472        .add_column(Column::auto())
473        .build(ctx);
474
475        let cb = CheckBox {
476            widget: self
477                .widget_builder
478                .with_accepts_input(true)
479                .with_child(grid)
480                .build(ctx),
481            checked: self.checked.into(),
482            check_mark: check_mark.into(),
483            uncheck_mark: uncheck_mark.into(),
484            undefined_mark: undefined_mark.into(),
485        };
486        ctx.add_node(UiNode::new(cb))
487    }
488}
489
490#[cfg(test)]
491mod test {
492    use crate::{
493        check_box::{CheckBoxBuilder, CheckBoxMessage},
494        message::MessageDirection,
495        widget::WidgetBuilder,
496        UserInterface,
497    };
498    use fyrox_core::algebra::Vector2;
499
500    #[test]
501    fn check_box() {
502        let mut ui = UserInterface::new(Vector2::new(100.0, 100.0));
503
504        assert_eq!(ui.poll_message(), None);
505
506        let check_box = CheckBoxBuilder::new(WidgetBuilder::new()).build(&mut ui.build_ctx());
507
508        assert_eq!(ui.poll_message(), None);
509
510        // Check messages
511        let input_message =
512            CheckBoxMessage::checked(check_box, MessageDirection::ToWidget, Some(true));
513
514        ui.send_message(input_message.clone());
515
516        // This message that we just send.
517        assert_eq!(ui.poll_message(), Some(input_message.clone()));
518        // We must get response from check box.
519        assert_eq!(ui.poll_message(), Some(input_message.reverse()));
520    }
521
522    use crate::test::test_widget_deletion;
523
524    #[test]
525    fn test_deletion() {
526        test_widget_deletion(|ctx| CheckBoxBuilder::new(WidgetBuilder::new()).build(ctx));
527    }
528}