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}