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::{
28    border::BorderBuilder,
29    brush::Brush,
30    core::{
31        color::Color,
32        pool::{Handle, ObjectOrVariant},
33        reflect::prelude::*,
34        type_traits::prelude::*,
35        variable::InheritableVariable,
36        visitor::prelude::*,
37    },
38    grid::{Column, GridBuilder, Row},
39    image::ImageBuilder,
40    message::{KeyCode, MessageData, UiMessage},
41    resources,
42    style::{resource::StyleResourceExt, Style},
43    widget::{Widget, WidgetBuilder, WidgetMessage},
44    BuildContext, Control, MouseButton, Thickness, UiNode, UserInterface, VerticalAlignment,
45};
46use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
47
48/// A set of possible check box messages.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum CheckBoxMessage {
51    /// Emitted when the checkbox changed its state. Could also be used to modify the checkbox state.
52    Check(Option<bool>),
53}
54impl MessageData for CheckBoxMessage {}
55
56/// Checkbox is a UI widget that have three states - `Checked`, `Unchecked` and `Undefined`. In most cases, it is used
57/// only with two values which fit in `bool` type. Third, undefined, state is used for specific situations when your
58/// data have such state.
59///
60/// ## How to create
61///
62/// To create a checkbox, you should do something like this:
63///
64/// ```rust,no_run
65/// # use fyrox_ui::{
66/// #     core::pool::Handle,
67/// #     check_box::CheckBoxBuilder, widget::WidgetBuilder, UiNode, UserInterface
68/// # };
69/// # use fyrox_ui::check_box::CheckBox;
70///
71/// fn create_checkbox(ui: &mut UserInterface) -> Handle<CheckBox> {
72///     CheckBoxBuilder::new(WidgetBuilder::new())
73///         // A custom value can be set during initialization.
74///         .checked(Some(true))
75///         .build(&mut ui.build_ctx())
76/// }
77/// ```
78///
79/// The above code will create a checkbox without any textual info, but usually checkboxes have some useful info
80/// near them. To create such checkbox, you could use [`CheckBoxBuilder::with_content`] method which accepts any widget handle.
81/// For checkbox with text, you could use [`crate::text::TextBuilder`] to create textual content, for checkbox with image - use
82/// [`crate::image::ImageBuilder`]. As already said, you're free to use any widget handle there.
83///
84/// Here's an example of checkbox with textual content.
85///
86/// ```rust,no_run
87/// # use fyrox_ui::{
88/// #     core::pool::Handle,
89/// #     check_box::CheckBoxBuilder, text::TextBuilder, widget::WidgetBuilder, UiNode,
90/// #     UserInterface,
91/// # };
92/// # use fyrox_ui::check_box::CheckBox;
93///
94/// fn create_checkbox(ui: &mut UserInterface) -> Handle<CheckBox> {
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 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 checkbox.
157    pub widget: Widget,
158    /// Current state of the checkbox.
159    pub checked: InheritableVariable<Option<bool>>,
160    /// Check mark widget that is used when the state is `Some(true)`.
161    pub check_mark: InheritableVariable<Handle<UiNode>>,
162    /// Check mark widget that is used when the state is `Some(false)`.
163    pub uncheck_mark: InheritableVariable<Handle<UiNode>>,
164    /// Check mark widget 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, 12.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                    .to_base()
192                    .into()
193            })
194            .with_group("Input")
195    }
196}
197
198crate::define_widget_deref!(CheckBox);
199
200impl Control for CheckBox {
201    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
202        self.widget.handle_routed_message(ui, message);
203
204        if let Some(msg) = message.data::<WidgetMessage>() {
205            match msg {
206                WidgetMessage::MouseDown { button, .. } => {
207                    if *button == MouseButton::Left
208                        && (message.destination() == self.handle()
209                            || self.widget.has_descendant(message.destination(), ui))
210                    {
211                        ui.capture_mouse(self.handle());
212                    }
213                }
214                WidgetMessage::MouseUp { button, .. } => {
215                    if *button == MouseButton::Left
216                        && (message.destination() == self.handle()
217                            || self.widget.has_descendant(message.destination(), ui))
218                    {
219                        ui.release_mouse_capture();
220
221                        if let Some(value) = *self.checked {
222                            // Invert state if it is defined.
223                            ui.send(self.handle(), CheckBoxMessage::Check(Some(!value)));
224                        } else {
225                            // Switch from undefined state to checked.
226                            ui.send(self.handle(), CheckBoxMessage::Check(Some(true)));
227                        }
228                    }
229                }
230                WidgetMessage::KeyDown(key_code) => {
231                    if !message.handled() && *key_code == KeyCode::Space {
232                        let checked = self.checked.map(|checked| !checked);
233                        ui.send(self.handle, CheckBoxMessage::Check(checked));
234                        message.set_handled(true);
235                    }
236                }
237                _ => (),
238            }
239        } else if let Some(&CheckBoxMessage::Check(value)) = message.data_for(self.handle) {
240            if *self.checked != value {
241                self.checked.set_value_and_mark_modified(value);
242
243                ui.try_send_response(message);
244
245                if self.check_mark.is_some() {
246                    match value {
247                        None => {
248                            ui.send(*self.check_mark, WidgetMessage::Visibility(false));
249                            ui.send(*self.uncheck_mark, WidgetMessage::Visibility(false));
250                            ui.send(*self.undefined_mark, WidgetMessage::Visibility(true));
251                        }
252                        Some(value) => {
253                            ui.send(*self.check_mark, WidgetMessage::Visibility(value));
254                            ui.send(*self.uncheck_mark, WidgetMessage::Visibility(!value));
255                            ui.send(*self.undefined_mark, WidgetMessage::Visibility(false));
256                        }
257                    }
258                }
259            }
260        }
261    }
262}
263
264/// Check box builder creates [`CheckBox`] instances and adds them to the user interface.
265pub struct CheckBoxBuilder {
266    widget_builder: WidgetBuilder,
267    checked: Option<bool>,
268    check_mark: Option<Handle<UiNode>>,
269    uncheck_mark: Option<Handle<UiNode>>,
270    undefined_mark: Option<Handle<UiNode>>,
271    background: Option<Handle<UiNode>>,
272    content: Handle<UiNode>,
273}
274
275impl CheckBoxBuilder {
276    /// Creates new check box builder instance.
277    pub fn new(widget_builder: WidgetBuilder) -> Self {
278        Self {
279            widget_builder,
280            checked: Some(false),
281            check_mark: None,
282            uncheck_mark: None,
283            undefined_mark: None,
284            content: Handle::NONE,
285            background: None,
286        }
287    }
288
289    /// Sets the desired state of the checkbox.
290    pub fn checked(mut self, value: Option<bool>) -> Self {
291        self.checked = value;
292        self
293    }
294
295    /// Sets the desired check mark when the state is `Some(true)`.
296    pub fn with_check_mark(mut self, check_mark: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
297        self.check_mark = Some(check_mark.to_base());
298        self
299    }
300
301    /// Sets the desired check mark when the state is `Some(false)`.
302    pub fn with_uncheck_mark(mut self, uncheck_mark: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
303        self.uncheck_mark = Some(uncheck_mark.to_base());
304        self
305    }
306
307    /// Sets the desired check mark when the state is `None`.
308    pub fn with_undefined_mark(
309        mut self,
310        undefined_mark: Handle<impl ObjectOrVariant<UiNode>>,
311    ) -> Self {
312        self.undefined_mark = Some(undefined_mark.to_base());
313        self
314    }
315
316    /// Sets the new content of the checkbox.
317    pub fn with_content(mut self, content: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
318        self.content = content.to_base();
319        self
320    }
321
322    /// Sets the desired background widget that will be used a container for check box contents. By
323    /// default, it is a simple border.
324    pub fn with_background(mut self, background: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
325        self.background = Some(background.to_base());
326        self
327    }
328
329    /// Finishes check box building and adds it to the user interface.
330    pub fn build(self, ctx: &mut BuildContext) -> Handle<CheckBox> {
331        let check_mark = self.check_mark.unwrap_or_else(|| {
332            let size = *ctx.style.property(CheckBox::CHECK_MARK_SIZE);
333            BorderBuilder::new(
334                WidgetBuilder::new()
335                    .with_background(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
336                    .with_child(
337                        ImageBuilder::new(WidgetBuilder::new().with_width(size).with_height(size))
338                            .with_opt_texture(resources::CHECK.clone())
339                            .build(ctx),
340                    ),
341            )
342            .with_pad_by_corner_radius(false)
343            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
344            .with_stroke_thickness(Thickness::uniform(0.0).into())
345            .build(ctx)
346            .to_base()
347        });
348        ctx[check_mark].set_visibility(self.checked.unwrap_or(false));
349
350        let uncheck_mark = self.uncheck_mark.unwrap_or_else(|| {
351            BorderBuilder::new(
352                WidgetBuilder::new()
353                    .with_margin(Thickness::uniform(3.0))
354                    .with_width(10.0)
355                    .with_height(9.0)
356                    .with_background(Brush::Solid(Color::TRANSPARENT).into())
357                    .with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
358            )
359            .with_pad_by_corner_radius(false)
360            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
361            .with_stroke_thickness(Thickness::uniform(0.0).into())
362            .build(ctx)
363            .to_base()
364        });
365        ctx[uncheck_mark].set_visibility(!self.checked.unwrap_or(true));
366
367        let undefined_mark = self.undefined_mark.unwrap_or_else(|| {
368            BorderBuilder::new(
369                WidgetBuilder::new()
370                    .with_margin(Thickness::uniform(4.0))
371                    .with_background(ctx.style.property(Style::BRUSH_BRIGHT))
372                    .with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
373            )
374            .with_pad_by_corner_radius(false)
375            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
376            .build(ctx)
377            .to_base()
378        });
379        ctx[undefined_mark].set_visibility(self.checked.is_none());
380
381        if self.content.is_some() {
382            ctx[self.content].set_row(0).set_column(1);
383        }
384
385        let background = self.background.unwrap_or_else(|| {
386            BorderBuilder::new(
387                WidgetBuilder::new()
388                    .with_vertical_alignment(VerticalAlignment::Center)
389                    .with_background(ctx.style.property(Style::BRUSH_DARKEST))
390                    .with_foreground(ctx.style.property(Style::BRUSH_LIGHT)),
391            )
392            .with_pad_by_corner_radius(false)
393            .with_corner_radius(ctx.style.property(CheckBox::CORNER_RADIUS))
394            .with_stroke_thickness(ctx.style.property(CheckBox::BORDER_THICKNESS))
395            .build(ctx)
396            .to_base()
397        });
398
399        let background_ref = &mut ctx[background];
400        background_ref.set_row(0).set_column(0);
401        if background_ref.min_width() < 0.01 {
402            background_ref.set_min_width(18.0);
403        }
404        if background_ref.min_height() < 0.01 {
405            background_ref.set_min_height(18.0);
406        }
407
408        ctx.link(check_mark, background);
409        ctx.link(uncheck_mark, background);
410        ctx.link(undefined_mark, background);
411
412        let grid = GridBuilder::new(
413            WidgetBuilder::new()
414                .with_child(background)
415                .with_child(self.content),
416        )
417        .add_row(Row::stretch())
418        .add_column(Column::auto())
419        .add_column(Column::stretch())
420        .build(ctx);
421
422        let cb = CheckBox {
423            widget: self
424                .widget_builder
425                .with_accepts_input(true)
426                .with_child(grid)
427                .build(ctx),
428            checked: self.checked.into(),
429            check_mark: check_mark.into(),
430            uncheck_mark: uncheck_mark.into(),
431            undefined_mark: undefined_mark.into(),
432        };
433        ctx.add(cb)
434    }
435}
436
437#[cfg(test)]
438mod test {
439    use crate::message::UiMessage;
440    use crate::{
441        check_box::{CheckBoxBuilder, CheckBoxMessage},
442        widget::WidgetBuilder,
443        UserInterface,
444    };
445    use fyrox_core::algebra::Vector2;
446
447    #[test]
448    fn check_box() {
449        let mut ui = UserInterface::new(Vector2::new(100.0, 100.0));
450
451        assert_eq!(ui.poll_message(), None);
452
453        let check_box = CheckBoxBuilder::new(WidgetBuilder::new()).build(&mut ui.build_ctx());
454
455        assert_eq!(ui.poll_message(), None);
456
457        // Check messages
458        let input_message = UiMessage::for_widget(check_box, CheckBoxMessage::Check(Some(true)));
459
460        ui.send_message(input_message.clone());
461
462        // This message that we just send.
463        assert_eq!(ui.poll_message(), Some(input_message.clone()));
464        // We must get response from check box.
465        assert_eq!(ui.poll_message(), Some(input_message.reverse()));
466    }
467
468    use crate::test::test_widget_deletion;
469
470    #[test]
471    fn test_deletion() {
472        test_widget_deletion(|ctx| CheckBoxBuilder::new(WidgetBuilder::new()).build(ctx));
473    }
474}