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}