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")]
154pub struct CheckBox {
155    /// Base widget of the check box.
156    pub widget: Widget,
157    /// Current state of the check box.
158    pub checked: InheritableVariable<Option<bool>>,
159    /// Check mark that is used when the state is `Some(true)`.
160    pub check_mark: InheritableVariable<Handle<UiNode>>,
161    /// Check mark that is used when the state is `Some(false)`.
162    pub uncheck_mark: InheritableVariable<Handle<UiNode>>,
163    /// Check mark that is used when the state is `None`.
164    pub undefined_mark: InheritableVariable<Handle<UiNode>>,
165}
166
167impl CheckBox {
168    /// A name of style property, that defines corner radius of a checkbox.
169    pub const CORNER_RADIUS: &'static str = "CheckBox.CornerRadius";
170    /// A name of style property, that defines border thickness of a checkbox.
171    pub const BORDER_THICKNESS: &'static str = "CheckBox.BorderThickness";
172    /// A name of style property, that defines border thickness of a checkbox.
173    pub const CHECK_MARK_SIZE: &'static str = "CheckBox.CheckMarkSize";
174
175    /// Returns a style of the widget. This style contains only widget-specific properties.
176    pub fn style() -> Style {
177        Style::default()
178            .with(Self::CORNER_RADIUS, 4.0f32)
179            .with(Self::BORDER_THICKNESS, Thickness::uniform(1.0))
180            .with(Self::CHECK_MARK_SIZE, 7.0f32)
181    }
182}
183
184impl ConstructorProvider<UiNode, UserInterface> for CheckBox {
185    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
186        GraphNodeConstructor::new::<Self>()
187            .with_variant("CheckBox", |ui| {
188                CheckBoxBuilder::new(WidgetBuilder::new().with_name("CheckBox"))
189                    .build(&mut ui.build_ctx())
190                    .into()
191            })
192            .with_group("Input")
193    }
194}
195
196crate::define_widget_deref!(CheckBox);
197
198impl Control for CheckBox {
199    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
200        self.widget.handle_routed_message(ui, message);
201
202        if let Some(msg) = message.data::<WidgetMessage>() {
203            match msg {
204                WidgetMessage::MouseDown { button, .. } => {
205                    if *button == MouseButton::Left
206                        && (message.destination() == self.handle()
207                            || self.widget.has_descendant(message.destination(), ui))
208                    {
209                        ui.capture_mouse(self.handle());
210                    }
211                }
212                WidgetMessage::MouseUp { button, .. } => {
213                    if *button == MouseButton::Left
214                        && (message.destination() == self.handle()
215                            || self.widget.has_descendant(message.destination(), ui))
216                    {
217                        ui.release_mouse_capture();
218
219                        if let Some(value) = *self.checked {
220                            // Invert state if it is defined.
221                            ui.send_message(CheckBoxMessage::checked(
222                                self.handle(),
223                                MessageDirection::ToWidget,
224                                Some(!value),
225                            ));
226                        } else {
227                            // Switch from undefined state to checked.
228                            ui.send_message(CheckBoxMessage::checked(
229                                self.handle(),
230                                MessageDirection::ToWidget,
231                                Some(true),
232                            ));
233                        }
234                    }
235                }
236                WidgetMessage::KeyDown(key_code) => {
237                    if !message.handled() && *key_code == KeyCode::Space {
238                        ui.send_message(CheckBoxMessage::checked(
239                            self.handle,
240                            MessageDirection::ToWidget,
241                            self.checked.map(|checked| !checked),
242                        ));
243                        message.set_handled(true);
244                    }
245                }
246                _ => (),
247            }
248        } else if let Some(&CheckBoxMessage::Check(value)) = message.data::<CheckBoxMessage>() {
249            if message.direction() == MessageDirection::ToWidget
250                && message.destination() == self.handle()
251                && *self.checked != value
252            {
253                self.checked.set_value_and_mark_modified(value);
254
255                ui.send_message(message.reverse());
256
257                if self.check_mark.is_some() {
258                    match value {
259                        None => {
260                            ui.send_message(WidgetMessage::visibility(
261                                *self.check_mark,
262                                MessageDirection::ToWidget,
263                                false,
264                            ));
265                            ui.send_message(WidgetMessage::visibility(
266                                *self.uncheck_mark,
267                                MessageDirection::ToWidget,
268                                false,
269                            ));
270                            ui.send_message(WidgetMessage::visibility(
271                                *self.undefined_mark,
272                                MessageDirection::ToWidget,
273                                true,
274                            ));
275                        }
276                        Some(value) => {
277                            ui.send_message(WidgetMessage::visibility(
278                                *self.check_mark,
279                                MessageDirection::ToWidget,
280                                value,
281                            ));
282                            ui.send_message(WidgetMessage::visibility(
283                                *self.uncheck_mark,
284                                MessageDirection::ToWidget,
285                                !value,
286                            ));
287                            ui.send_message(WidgetMessage::visibility(
288                                *self.undefined_mark,
289                                MessageDirection::ToWidget,
290                                false,
291                            ));
292                        }
293                    }
294                }
295            }
296        }
297    }
298}
299
300/// Check box builder creates [`CheckBox`] instances and adds them to the user interface.
301pub struct CheckBoxBuilder {
302    widget_builder: WidgetBuilder,
303    checked: Option<bool>,
304    check_mark: Option<Handle<UiNode>>,
305    uncheck_mark: Option<Handle<UiNode>>,
306    undefined_mark: Option<Handle<UiNode>>,
307    background: Option<Handle<UiNode>>,
308    content: Handle<UiNode>,
309}
310
311impl CheckBoxBuilder {
312    /// Creates new check box builder instance.
313    pub fn new(widget_builder: WidgetBuilder) -> Self {
314        Self {
315            widget_builder,
316            checked: Some(false),
317            check_mark: None,
318            uncheck_mark: None,
319            undefined_mark: None,
320            content: Handle::NONE,
321            background: None,
322        }
323    }
324
325    /// Sets the desired state of the check box.
326    pub fn checked(mut self, value: Option<bool>) -> Self {
327        self.checked = value;
328        self
329    }
330
331    /// Sets the desired check mark when the state is `Some(true)`.
332    pub fn with_check_mark(mut self, check_mark: Handle<UiNode>) -> Self {
333        self.check_mark = Some(check_mark);
334        self
335    }
336
337    /// Sets the desired check mark when the state is `Some(false)`.
338    pub fn with_uncheck_mark(mut self, uncheck_mark: Handle<UiNode>) -> Self {
339        self.uncheck_mark = Some(uncheck_mark);
340        self
341    }
342
343    /// Sets the desired check mark when the state is `None`.
344    pub fn with_undefined_mark(mut self, undefined_mark: Handle<UiNode>) -> Self {
345        self.undefined_mark = Some(undefined_mark);
346        self
347    }
348
349    /// Sets the new content of the check box.
350    pub fn with_content(mut self, content: Handle<UiNode>) -> Self {
351        self.content = content;
352        self
353    }
354
355    /// Sets the desired background widget that will be used a container for check box contents. By
356    /// default, it is a simple border.
357    pub fn with_background(mut self, background: Handle<UiNode>) -> Self {
358        self.background = Some(background);
359        self
360    }
361
362    /// Finishes check box building and adds it to the user interface.
363    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
364        let check_mark = self.check_mark.unwrap_or_else(|| {
365            let size = *ctx.style.property(CheckBox::CHECK_MARK_SIZE);
366            let half_size = size * 0.5;
367
368            BorderBuilder::new(
369                WidgetBuilder::new()
370                    .with_background(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
371                    .with_child(
372                        VectorImageBuilder::new(
373                            WidgetBuilder::new()
374                                .with_vertical_alignment(VerticalAlignment::Center)
375                                .with_horizontal_alignment(HorizontalAlignment::Center)
376                                // Give some padding to ensure primitives don't get too cut off
377                                .with_width(size + 1.0)
378                                .with_height(size + 1.0)
379                                .with_foreground(ctx.style.property(Style::BRUSH_TEXT)),
380                        )
381                        .with_primitives({
382                            vec![
383                                Primitive::Line {
384                                    begin: Vector2::new(0.0, half_size),
385                                    end: Vector2::new(half_size, size),
386                                    thickness: 2.0,
387                                },
388                                Primitive::Line {
389                                    begin: Vector2::new(half_size, size),
390                                    end: Vector2::new(size, 0.0),
391                                    thickness: 2.0,
392                                },
393                            ]
394                        })
395                        .build(ctx),
396                    ),
397            )
398            .with_pad_by_corner_radius(false)
399            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
400            .with_stroke_thickness(Thickness::uniform(0.0).into())
401            .build(ctx)
402        });
403        ctx[check_mark].set_visibility(self.checked.unwrap_or(false));
404
405        let uncheck_mark = self.uncheck_mark.unwrap_or_else(|| {
406            BorderBuilder::new(
407                WidgetBuilder::new()
408                    .with_margin(Thickness::uniform(3.0))
409                    .with_width(10.0)
410                    .with_height(9.0)
411                    .with_background(Brush::Solid(Color::TRANSPARENT).into())
412                    .with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
413            )
414            .with_pad_by_corner_radius(false)
415            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
416            .with_stroke_thickness(Thickness::uniform(0.0).into())
417            .build(ctx)
418        });
419        ctx[uncheck_mark].set_visibility(!self.checked.unwrap_or(true));
420
421        let undefined_mark = self.undefined_mark.unwrap_or_else(|| {
422            BorderBuilder::new(
423                WidgetBuilder::new()
424                    .with_margin(Thickness::uniform(4.0))
425                    .with_background(ctx.style.property(Style::BRUSH_BRIGHT))
426                    .with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
427            )
428            .with_pad_by_corner_radius(false)
429            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
430            .build(ctx)
431        });
432        ctx[undefined_mark].set_visibility(self.checked.is_none());
433
434        if self.content.is_some() {
435            ctx[self.content].set_row(0).set_column(1);
436        }
437
438        let background = self.background.unwrap_or_else(|| {
439            BorderBuilder::new(
440                WidgetBuilder::new()
441                    .with_vertical_alignment(VerticalAlignment::Center)
442                    .with_background(ctx.style.property(Style::BRUSH_DARKEST))
443                    .with_foreground(ctx.style.property(Style::BRUSH_LIGHT)),
444            )
445            .with_pad_by_corner_radius(false)
446            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
447            .with_stroke_thickness(ctx.style.property(CheckBox::BORDER_THICKNESS))
448            .build(ctx)
449        });
450
451        let background_ref = &mut ctx[background];
452        background_ref.set_row(0).set_column(0);
453        if background_ref.min_width() < 0.01 {
454            background_ref.set_min_width(16.0);
455        }
456        if background_ref.min_height() < 0.01 {
457            background_ref.set_min_height(16.0);
458        }
459
460        ctx.link(check_mark, background);
461        ctx.link(uncheck_mark, background);
462        ctx.link(undefined_mark, background);
463
464        let grid = GridBuilder::new(
465            WidgetBuilder::new()
466                .with_child(background)
467                .with_child(self.content),
468        )
469        .add_row(Row::stretch())
470        .add_column(Column::auto())
471        .add_column(Column::auto())
472        .build(ctx);
473
474        let cb = CheckBox {
475            widget: self
476                .widget_builder
477                .with_accepts_input(true)
478                .with_child(grid)
479                .build(ctx),
480            checked: self.checked.into(),
481            check_mark: check_mark.into(),
482            uncheck_mark: uncheck_mark.into(),
483            undefined_mark: undefined_mark.into(),
484        };
485        ctx.add_node(UiNode::new(cb))
486    }
487}
488
489#[cfg(test)]
490mod test {
491    use crate::{
492        check_box::{CheckBoxBuilder, CheckBoxMessage},
493        message::MessageDirection,
494        widget::WidgetBuilder,
495        UserInterface,
496    };
497    use fyrox_core::algebra::Vector2;
498
499    #[test]
500    fn check_box() {
501        let mut ui = UserInterface::new(Vector2::new(100.0, 100.0));
502
503        assert_eq!(ui.poll_message(), None);
504
505        let check_box = CheckBoxBuilder::new(WidgetBuilder::new()).build(&mut ui.build_ctx());
506
507        assert_eq!(ui.poll_message(), None);
508
509        // Check messages
510        let input_message =
511            CheckBoxMessage::checked(check_box, MessageDirection::ToWidget, Some(true));
512
513        ui.send_message(input_message.clone());
514
515        // This message that we just send.
516        assert_eq!(ui.poll_message(), Some(input_message.clone()));
517        // We must get response from check box.
518        assert_eq!(ui.poll_message(), Some(input_message.reverse()));
519    }
520
521    use crate::test::test_widget_deletion;
522
523    #[test]
524    fn test_deletion() {
525        test_widget_deletion(|ctx| CheckBoxBuilder::new(WidgetBuilder::new()).build(ctx));
526    }
527}