Skip to main content

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