1use azul_core::{
6 callbacks::{CoreCallbackData, Update},
7 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
8 refany::RefAny,
9};
10use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
11#[allow(clippy::wildcard_imports)] use azul_css::{
13 props::{
14 basic::{color::ColorU, *},
15 layout::*,
16 property::{CssProperty, *},
17 style::*,
18 },
19 *,
20};
21
22use crate::callbacks::{Callback, CallbackInfo};
23
24static CHECKBOX_CONTAINER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
25 "__azul-native-checkbox-container",
26))];
27static CHECKBOX_CONTENT_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
28 "__azul-native-checkbox-content",
29))];
30
31pub type CheckBoxOnToggleCallbackType =
33 extern "C" fn(RefAny, CallbackInfo, CheckBoxState) -> Update;
34impl_widget_callback!(
35 CheckBoxOnToggle,
36 OptionCheckBoxOnToggle,
37 CheckBoxOnToggleCallback,
38 CheckBoxOnToggleCallbackType
39);
40
41azul_core::impl_managed_callback! {
42 wrapper: CheckBoxOnToggleCallback,
43 info_ty: CallbackInfo,
44 return_ty: Update,
45 default_ret: Update::DoNothing,
46 invoker_static: CHECK_BOX_ON_TOGGLE_INVOKER,
47 invoker_ty: AzCheckBoxOnToggleCallbackInvoker,
48 thunk_fn: az_check_box_on_toggle_callback_thunk,
49 setter_fn: AzApp_setCheckBoxOnToggleCallbackInvoker,
50 from_handle_fn: AzCheckBoxOnToggleCallback_createFromHostHandle,
51 extra_args: [ state: CheckBoxState ],
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56#[repr(C)]
57pub struct CheckBox {
58 pub check_box_state: CheckBoxStateWrapper,
59 pub container_style: CssPropertyWithConditionsVec,
61 pub content_style: CssPropertyWithConditionsVec,
63}
64
65#[derive(Debug, Default, Clone, PartialEq, Eq)]
66#[repr(C)]
67pub struct CheckBoxStateWrapper {
68 pub inner: CheckBoxState,
70 pub on_toggle: OptionCheckBoxOnToggle,
72}
73
74#[derive(Copy, Debug, Default, Clone, PartialEq, Eq)]
76#[repr(C)]
77pub struct CheckBoxState {
78 pub checked: bool,
79}
80
81const BACKGROUND_COLOR: ColorU = ColorU {
82 r: 255,
83 g: 255,
84 b: 255,
85 a: 255,
86}; const BACKGROUND_THEME_LIGHT: &[StyleBackgroundContent] =
88 &[StyleBackgroundContent::Color(BACKGROUND_COLOR)];
89const BACKGROUND_COLOR_LIGHT: StyleBackgroundContentVec =
90 StyleBackgroundContentVec::from_const_slice(BACKGROUND_THEME_LIGHT);
91const COLOR_9B9B9B: ColorU = ColorU {
92 r: 155,
93 g: 155,
94 b: 155,
95 a: 255,
96}; const FILL_THEME: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(COLOR_9B9B9B)];
99const FILL_COLOR_BACKGROUND: StyleBackgroundContentVec =
100 StyleBackgroundContentVec::from_const_slice(FILL_THEME);
101
102static DEFAULT_CHECKBOX_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
103 CssPropertyWithConditions::simple(CssProperty::const_background_content(
104 BACKGROUND_COLOR_LIGHT,
105 )),
106 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
107 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(14))),
108 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(14))),
109 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
111 LayoutPaddingLeft::const_px(2),
112 )),
113 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
114 LayoutPaddingRight::const_px(2),
115 )),
116 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
117 2,
118 ))),
119 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
120 LayoutPaddingBottom::const_px(2),
121 )),
122 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
124 LayoutBorderTopWidth::const_px(1),
125 )),
126 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
127 LayoutBorderBottomWidth::const_px(1),
128 )),
129 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
130 LayoutBorderLeftWidth::const_px(1),
131 )),
132 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
133 LayoutBorderRightWidth::const_px(1),
134 )),
135 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
136 inner: BorderStyle::Inset,
137 })),
138 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
139 StyleBorderBottomStyle {
140 inner: BorderStyle::Inset,
141 },
142 )),
143 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
144 inner: BorderStyle::Inset,
145 })),
146 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
147 StyleBorderRightStyle {
148 inner: BorderStyle::Inset,
149 },
150 )),
151 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
152 inner: COLOR_9B9B9B,
153 })),
154 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
155 StyleBorderBottomColor {
156 inner: COLOR_9B9B9B,
157 },
158 )),
159 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
160 inner: COLOR_9B9B9B,
161 })),
162 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
163 StyleBorderRightColor {
164 inner: COLOR_9B9B9B,
165 },
166 )),
167 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
168];
169
170static DEFAULT_CHECKBOX_CONTENT_STYLE_CHECKED: &[CssPropertyWithConditions] = &[
171 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(8))),
172 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(8))),
173 CssPropertyWithConditions::simple(CssProperty::const_background_content(FILL_COLOR_BACKGROUND)),
174 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
175];
176
177static DEFAULT_CHECKBOX_CONTENT_STYLE_UNCHECKED: &[CssPropertyWithConditions] = &[
178 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(8))),
179 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(8))),
180 CssPropertyWithConditions::simple(CssProperty::const_background_content(FILL_COLOR_BACKGROUND)),
181 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(0))),
182];
183
184impl CheckBox {
185 #[must_use] pub fn create(checked: bool) -> Self {
186 Self {
187 check_box_state: CheckBoxStateWrapper {
188 inner: CheckBoxState { checked },
189 ..Default::default()
190 },
191 container_style: CssPropertyWithConditionsVec::from_const_slice(
192 DEFAULT_CHECKBOX_CONTAINER_STYLE,
193 ),
194 content_style: if checked {
195 CssPropertyWithConditionsVec::from_const_slice(
196 DEFAULT_CHECKBOX_CONTENT_STYLE_CHECKED,
197 )
198 } else {
199 CssPropertyWithConditionsVec::from_const_slice(
200 DEFAULT_CHECKBOX_CONTENT_STYLE_UNCHECKED,
201 )
202 },
203 }
204 }
205
206 #[inline]
207 #[must_use]
208 pub fn swap_with_default(&mut self) -> Self {
209 let mut s = Self::create(false);
210 core::mem::swap(&mut s, self);
211 s
212 }
213
214 #[inline]
215 pub fn set_on_toggle<C: Into<CheckBoxOnToggleCallback>>(&mut self, data: RefAny, on_toggle: C) {
216 self.check_box_state.on_toggle = Some(CheckBoxOnToggle {
217 callback: on_toggle.into(),
218 refany: data,
219 })
220 .into();
221 }
222
223 #[inline]
224 #[must_use]
225 pub fn with_on_toggle<C: Into<CheckBoxOnToggleCallback>>(
226 mut self,
227 data: RefAny,
228 on_toggle: C,
229 ) -> Self {
230 self.set_on_toggle(data, on_toggle);
231 self
232 }
233
234 #[inline]
235 #[must_use] pub fn dom(self) -> Dom {
236 use azul_core::{
237 callbacks::{CoreCallback, CoreCallbackData},
238 dom::{Dom, EventFilter, HoverEventFilter},
239 };
240
241 Dom::create_div()
242 .with_ids_and_classes(IdOrClassVec::from(CHECKBOX_CONTAINER_CLASS))
243 .with_css_props(self.container_style)
244 .with_callbacks(
245 vec![CoreCallbackData {
246 event: EventFilter::Hover(HoverEventFilter::MouseUp),
247 callback: CoreCallback {
248 cb: input::default_on_checkbox_clicked as usize,
249 ctx: azul_core::refany::OptionRefAny::None,
250 },
251 refany: RefAny::new(self.check_box_state),
252 }]
253 .into(),
254 )
255 .with_tab_index(TabIndex::Auto)
256 .with_children(
257 vec![Dom::create_div()
258 .with_ids_and_classes(IdOrClassVec::from(CHECKBOX_CONTENT_CLASS))
259 .with_css_props(self.content_style)]
260 .into(),
261 )
262 }
263}
264
265mod input {
267
268 use azul_core::{callbacks::Update, refany::RefAny};
269 use azul_css::props::{property::CssProperty, style::effects::StyleOpacity};
270
271 use super::{CheckBoxOnToggle, CheckBoxStateWrapper};
272 use crate::callbacks::CallbackInfo;
273
274 pub(super) extern "C" fn default_on_checkbox_clicked(
275 mut check_box: RefAny,
276 mut info: CallbackInfo,
277 ) -> Update {
278 let Some(mut check_box) = check_box.downcast_mut::<CheckBoxStateWrapper>() else {
279 return Update::DoNothing;
280 };
281
282 let Some(checkbox_content_id) = info.get_first_child(info.get_hit_node()) else {
283 return Update::DoNothing;
284 };
285
286 check_box.inner.checked = !check_box.inner.checked;
287
288 let result = {
289 let check_box = &mut *check_box;
291 let ontoggle = &mut check_box.on_toggle;
292 let inner = check_box.inner;
293
294 match ontoggle.as_mut() {
295 Some(CheckBoxOnToggle {
296 callback,
297 refany: data,
298 }) => (callback.cb)(data.clone(), info, inner),
299 None => Update::DoNothing,
300 }
301 };
302
303 if check_box.inner.checked {
304 info.set_css_property(
305 checkbox_content_id,
306 CssProperty::const_opacity(StyleOpacity::const_new(100)),
307 );
308 } else {
309 info.set_css_property(
310 checkbox_content_id,
311 CssProperty::const_opacity(StyleOpacity::const_new(0)),
312 );
313 }
314
315 result
316 }
317}
318
319impl From<CheckBox> for Dom {
320 fn from(b: CheckBox) -> Self {
321 b.dom()
322 }
323}
324
325#[cfg(all(test, feature = "std"))]
326#[allow(clippy::float_cmp, clippy::too_many_lines)]
327mod autotest_generated {
328 use std::{
329 collections::{BTreeMap, HashMap},
330 mem::discriminant,
331 sync::{Arc, Mutex},
332 };
333
334 use azul_core::{
335 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
336 geom::{LogicalRect, OptionLogicalPosition},
337 gl::OptionGlContextPtr,
338 hit_test::ScrollPosition,
339 refany::OptionRefAny,
340 resources::RendererResources,
341 styled_dom::{NodeHierarchyItemId, StyledDom},
342 window::{MonitorVec, RawWindowHandle},
343 };
344 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
345 use rust_fontconfig::FcFontCache;
346
347 use super::*;
348 #[cfg(feature = "icu")]
349 use crate::icu::IcuLocalizerHandle;
350 use crate::{
351 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
352 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
353 window::{DomLayoutResult, LayoutWindow},
354 window_state::FullWindowState,
355 };
356
357 const CONTAINER_SIDE: f32 = 14.0;
366 const PADDING: f32 = 2.0;
367 const BORDER: f32 = 1.0;
368 const CONTENT_SIDE: f32 = 8.0;
369
370 const CONTAINER: usize = 0;
372 const CONTENT: usize = 1;
373
374 fn node(idx: usize) -> DomNodeId {
376 DomNodeId {
377 dom: DomId::ROOT_ID,
378 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
379 }
380 }
381
382 fn node_none() -> DomNodeId {
386 DomNodeId {
387 dom: DomId::ROOT_ID,
388 node: NodeHierarchyItemId::NONE,
389 }
390 }
391
392 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
396 DomLayoutResult {
397 styled_dom,
398 layout_tree: LayoutTree {
399 nodes: Vec::new(),
400 warm: Vec::new(),
401 cold: Vec::new(),
402 root: 0,
403 dom_to_layout: BTreeMap::new(),
404 children_arena: Vec::new(),
405 children_offsets: Vec::new(),
406 subtree_needs_intrinsic: Vec::new(),
407 },
408 calculated_positions: Vec::new(),
409 viewport: LogicalRect::zero(),
410 display_list: DisplayList::default(),
411 scroll_ids: HashMap::new(),
412 scroll_id_to_node_id: HashMap::new(),
413 }
414 }
415
416 fn with_info<R>(
420 styled_dom: StyledDom,
421 hit: DomNodeId,
422 f: impl FnOnce(&mut CallbackInfo) -> R,
423 ) -> (R, Vec<CallbackChange>) {
424 let mut layout_window =
425 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
426 layout_window
427 .layout_results
428 .insert(DomId::ROOT_ID, layout_result(styled_dom));
429
430 let renderer_resources = RendererResources::default();
431 let previous_window_state: Option<FullWindowState> = None;
432 let current_window_state = FullWindowState::default();
433 let gl_context = OptionGlContextPtr::None;
434 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
435 BTreeMap::new();
436 let window_handle = RawWindowHandle::Unsupported;
437 let system_callbacks = ExternalSystemCallbacks::rust_internal();
438
439 let ref_data = CallbackInfoRefData {
440 layout_window: &layout_window,
441 renderer_resources: &renderer_resources,
442 previous_window_state: &previous_window_state,
443 current_window_state: ¤t_window_state,
444 gl_context: &gl_context,
445 current_scroll_manager: &scroll_states,
446 current_window_handle: &window_handle,
447 system_callbacks: &system_callbacks,
448 system_style: Arc::new(azul_css::system::SystemStyle::default()),
449 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
450 #[cfg(feature = "icu")]
451 icu_localizer: IcuLocalizerHandle::default(),
452 ctx: OptionRefAny::None,
453 };
454
455 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
456
457 let mut info = CallbackInfo::new(
458 &ref_data,
459 &changes,
460 hit,
461 OptionLogicalPosition::None,
462 OptionLogicalPosition::None,
463 );
464
465 let r = f(&mut info);
466 let pushed = info.take_changes();
467 (r, pushed)
468 }
469
470 fn laid_out(check_box: CheckBox) -> (StyledDom, RefAny) {
476 let dom = check_box.dom();
477 let state = dom.root.callbacks.as_ref()[0].refany.clone();
478 (StyledDom::create_from_dom(dom), state)
479 }
480
481 fn click(
483 styled_dom: StyledDom,
484 state: &RefAny,
485 hit: DomNodeId,
486 ) -> (Update, Vec<CallbackChange>) {
487 with_info(styled_dom, hit, |info| {
488 input::default_on_checkbox_clicked(state.clone(), *info)
489 })
490 }
491
492 fn is_checked(state: &RefAny) -> bool {
493 let mut state = state.clone();
494 let wrapper = state
495 .downcast_ref::<CheckBoxStateWrapper>()
496 .expect("the widget state changed type");
497 wrapper.inner.checked
498 }
499
500 fn pushed_opacities(changes: &[CallbackChange]) -> Vec<(NodeId, f32)> {
502 changes
503 .iter()
504 .filter_map(|c| match c {
505 CallbackChange::ChangeNodeCssProperties {
506 node_id, properties, ..
507 } => {
508 let o = properties.as_ref().iter().find_map(|p| match p {
509 CssProperty::Opacity(o) => o.get_property().map(|o| o.inner.normalized()),
510 _ => None,
511 })?;
512 Some((*node_id, o))
513 }
514 _ => None,
515 })
516 .collect()
517 }
518
519 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
524 v.as_ref().iter().map(|p| p.property.clone()).collect()
525 }
526
527 fn find<T>(v: &CssPropertyWithConditionsVec, f: impl Fn(&CssProperty) -> Option<T>) -> Option<T> {
528 v.as_ref().iter().find_map(|p| f(&p.property))
529 }
530
531 fn px(pv: &PixelValue) -> f32 {
535 assert_eq!(
536 pv.metric,
537 SizeMetric::Px,
538 "checkbox geometry must be absolute px, got {:?}",
539 pv.metric,
540 );
541 pv.number.get()
542 }
543
544 fn width_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
545 find(v, |p| match p {
546 CssProperty::Width(w) => match w.get_property() {
547 Some(LayoutWidth::Px(pv)) => Some(px(pv)),
548 _ => None,
549 },
550 _ => None,
551 })
552 }
553
554 fn height_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
555 find(v, |p| match p {
556 CssProperty::Height(h) => match h.get_property() {
557 Some(LayoutHeight::Px(pv)) => Some(px(pv)),
558 _ => None,
559 },
560 _ => None,
561 })
562 }
563
564 fn opacity(v: &CssPropertyWithConditionsVec) -> Option<f32> {
568 find(v, |p| match p {
569 CssProperty::Opacity(o) => o.get_property().map(|o| o.inner.normalized()),
570 _ => None,
571 })
572 }
573
574 fn classes(dom: &Dom) -> Vec<String> {
575 dom.root
576 .get_ids_and_classes()
577 .as_ref()
578 .iter()
579 .filter_map(|c| match c {
580 IdOrClass::Class(s) => Some(s.as_str().to_string()),
581 IdOrClass::Id(_) => None,
582 })
583 .collect()
584 }
585
586 fn inline_properties(dom: &Dom) -> Vec<CssProperty> {
588 dom.root
589 .style
590 .iter_inline_properties()
591 .map(|(p, _)| p.clone())
592 .collect()
593 }
594
595 #[derive(Debug, Clone, PartialEq, Eq)]
603 struct ToggleLog {
604 seen: Vec<bool>,
605 payload: u32,
606 }
607
608 extern "C" fn record_toggle(
609 mut data: RefAny,
610 _info: CallbackInfo,
611 state: CheckBoxState,
612 ) -> Update {
613 if let Some(mut log) = data.downcast_mut::<ToggleLog>() {
614 log.seen.push(state.checked);
615 }
616 Update::RefreshDom
617 }
618
619 extern "C" fn toggle_do_nothing(
620 _data: RefAny,
621 _info: CallbackInfo,
622 _state: CheckBoxState,
623 ) -> Update {
624 Update::DoNothing
625 }
626
627 extern "C" fn toggle_refresh_all(
628 _data: RefAny,
629 _info: CallbackInfo,
630 _state: CheckBoxState,
631 ) -> Update {
632 Update::RefreshDomAllWindows
633 }
634
635 extern "C" fn generic_shaped(_data: RefAny, _info: CallbackInfo) -> Update {
638 Update::DoNothing
639 }
640
641 fn log_refany() -> RefAny {
642 RefAny::new(ToggleLog {
643 seen: Vec::new(),
644 payload: 0xDEAD_BEEF,
645 })
646 }
647
648 fn read_log(probe: &RefAny) -> ToggleLog {
649 let mut probe = probe.clone();
650 let log = probe
651 .downcast_ref::<ToggleLog>()
652 .expect("the user payload changed type");
653 log.clone()
654 }
655
656 #[test]
661 fn create_stores_the_checked_flag_and_installs_no_callback() {
662 for checked in [false, true] {
663 let c = CheckBox::create(checked);
664 assert_eq!(
665 c.check_box_state.inner.checked, checked,
666 "create({checked}) did not store the flag it was given",
667 );
668 assert!(
669 c.check_box_state.on_toggle.as_ref().is_none(),
670 "create({checked}) invented a toggle callback out of nowhere",
671 );
672 }
673 }
674
675 #[test]
676 fn create_is_pure_and_its_two_states_are_distinguishable() {
677 assert_eq!(CheckBox::create(true), CheckBox::create(true));
681 assert_eq!(CheckBox::create(false), CheckBox::create(false));
682 assert_ne!(
683 CheckBox::create(true),
684 CheckBox::create(false),
685 "a checked and an unchecked checkbox are indistinguishable",
686 );
687 }
688
689 #[test]
690 fn create_only_the_checkmark_opacity_depends_on_the_checked_flag() {
691 let checked = CheckBox::create(true);
694 let unchecked = CheckBox::create(false);
695 assert_eq!(
696 properties(&checked.container_style),
697 properties(&unchecked.container_style),
698 "the container style differs between the checked and unchecked state",
699 );
700
701 let a = properties(&checked.content_style);
703 let b = properties(&unchecked.content_style);
704 assert_eq!(a.len(), b.len(), "the two content styles declare a different number of properties");
705 let differing: Vec<_> = a
706 .iter()
707 .zip(b.iter())
708 .filter(|(x, y)| x != y)
709 .map(|(x, _)| discriminant(x))
710 .collect();
711 assert_eq!(
712 differing,
713 vec![discriminant(&CssProperty::const_opacity(StyleOpacity::const_new(0)))],
714 "the checked/unchecked content styles differ in something other than opacity",
715 );
716 }
717
718 #[test]
719 fn create_maps_checked_to_a_fully_opaque_mark_and_unchecked_to_a_fully_transparent_one() {
720 assert_eq!(
724 opacity(&CheckBox::create(true).content_style),
725 Some(1.0),
726 "a checked checkbox does not show its checkmark",
727 );
728 assert_eq!(
729 opacity(&CheckBox::create(false).content_style),
730 Some(0.0),
731 "an unchecked checkbox still shows its checkmark",
732 );
733 }
734
735 #[test]
736 fn create_geometry_is_absolute_px_in_both_states() {
737 for checked in [false, true] {
738 let c = CheckBox::create(checked);
739 assert_eq!(width_px(&c.container_style), Some(CONTAINER_SIDE));
741 assert_eq!(height_px(&c.container_style), Some(CONTAINER_SIDE));
742 assert_eq!(width_px(&c.content_style), Some(CONTENT_SIDE));
743 assert_eq!(height_px(&c.content_style), Some(CONTENT_SIDE));
744 }
745 }
746
747 #[test]
748 fn content_exactly_fills_the_containers_padding_box() {
749 assert_eq!(
754 CONTENT_SIDE,
755 CONTAINER_SIDE - 2.0 * PADDING - 2.0 * BORDER,
756 "the checkbox geometry constants no longer add up",
757 );
758
759 let c = CheckBox::create(true);
760 let padding = |f: fn(&CssProperty) -> Option<f32>| find(&c.container_style, f);
761 let pad_l = padding(|p| match p {
762 CssProperty::PaddingLeft(v) => v.get_property().map(|v| px(&v.inner)),
763 _ => None,
764 });
765 let pad_r = padding(|p| match p {
766 CssProperty::PaddingRight(v) => v.get_property().map(|v| px(&v.inner)),
767 _ => None,
768 });
769 let bor_l = padding(|p| match p {
770 CssProperty::BorderLeftWidth(v) => v.get_property().map(|v| px(&v.inner)),
771 _ => None,
772 });
773 let bor_r = padding(|p| match p {
774 CssProperty::BorderRightWidth(v) => v.get_property().map(|v| px(&v.inner)),
775 _ => None,
776 });
777
778 assert_eq!(pad_l, Some(PADDING));
779 assert_eq!(pad_r, Some(PADDING));
780 assert_eq!(bor_l, Some(BORDER));
781 assert_eq!(bor_r, Some(BORDER));
782
783 let inner = CONTAINER_SIDE
784 - pad_l.unwrap()
785 - pad_r.unwrap()
786 - bor_l.unwrap()
787 - bor_r.unwrap();
788 assert_eq!(
789 width_px(&c.content_style),
790 Some(inner),
791 "the checkmark no longer fills the container's padding box",
792 );
793 }
794
795 #[test]
796 fn create_declares_no_property_twice() {
797 for checked in [false, true] {
800 let c = CheckBox::create(checked);
801 for (name, v) in [
802 ("container", &c.container_style),
803 ("content", &c.content_style),
804 ] {
805 let props = properties(v);
806 let mut seen = Vec::new();
807 for p in &props {
808 let d = discriminant(p);
809 assert!(
810 !seen.contains(&d),
811 "checked={checked}: the {name} style declares {p:?} twice",
812 );
813 seen.push(d);
814 }
815 }
816 }
817 }
818
819 #[test]
820 fn create_marks_the_container_as_clickable() {
821 for checked in [false, true] {
824 let cursor = find(&CheckBox::create(checked).container_style, |p| match p {
825 CssProperty::Cursor(c) => c.get_property().copied(),
826 _ => None,
827 });
828 assert_eq!(
829 cursor,
830 Some(StyleCursor::Pointer),
831 "checked={checked}: the checkbox does not present as clickable",
832 );
833 }
834 }
835
836 #[test]
841 fn swap_with_default_returns_the_old_widget_and_leaves_an_unchecked_default_behind() {
842 let mut c = CheckBox::create(true);
843 let old = c.swap_with_default();
844
845 assert_eq!(old, CheckBox::create(true), "the old widget was not returned intact");
846 assert_eq!(c, CheckBox::create(false), "what was left behind is not a fresh unchecked checkbox");
847 }
848
849 #[test]
850 fn swap_with_default_on_an_already_default_widget_is_a_no_op() {
851 let mut c = CheckBox::create(false);
852 let old = c.swap_with_default();
853 assert_eq!(old, c, "swapping a default with a default produced two different widgets");
854 assert_eq!(old, CheckBox::create(false));
855 }
856
857 #[test]
858 fn swap_with_default_moves_the_toggle_callback_out_rather_than_copying_or_dropping_it() {
859 let probe = log_refany();
860 let mut c = CheckBox::create(true)
861 .with_on_toggle(probe.clone(), record_toggle as CheckBoxOnToggleCallbackType);
862
863 let old = c.swap_with_default();
864
865 let moved = old
867 .check_box_state
868 .on_toggle
869 .as_ref()
870 .expect("the toggle callback vanished during the swap");
871 assert_eq!(
872 moved.callback.cb as *const () as usize,
873 record_toggle as CheckBoxOnToggleCallbackType as *const () as usize,
874 "the fn pointer was mangled by the swap",
875 );
876
877 assert!(
880 c.check_box_state.on_toggle.as_ref().is_none(),
881 "the toggle callback was copied instead of moved",
882 );
883
884 assert_eq!(read_log(&probe).payload, 0xDEAD_BEEF);
886 }
887
888 #[test]
889 fn swapping_twice_round_trips_the_original_widget() {
890 let mut a = CheckBox::create(true);
891 let mut b = a.swap_with_default(); let c = b.swap_with_default(); assert_eq!(c, CheckBox::create(true));
895 assert_eq!(a, CheckBox::create(false));
896 assert_eq!(b, CheckBox::create(false));
897 }
898
899 #[test]
904 fn set_on_toggle_stores_the_function_pointer_and_the_payload_verbatim() {
905 let mut c = CheckBox::create(false);
906 c.set_on_toggle(RefAny::new(0xDEAD_BEEF_u32), toggle_do_nothing as CheckBoxOnToggleCallbackType);
907
908 let t = c
909 .check_box_state
910 .on_toggle
911 .as_ref()
912 .expect("set_on_toggle did not store anything");
913 assert_eq!(
914 t.callback.cb as *const () as usize,
915 toggle_do_nothing as CheckBoxOnToggleCallbackType as *const () as usize,
916 "the fn pointer was corrupted on the way in",
917 );
918
919 let mut data = t.refany.clone();
920 assert_eq!(
921 *data.downcast_ref::<u32>().expect("the payload changed type"),
922 0xDEAD_BEEF,
923 "the payload was corrupted",
924 );
925 assert!(
926 data.downcast_ref::<u64>().is_none(),
927 "downcasting to the wrong type must fail, not reinterpret the bytes",
928 );
929 }
930
931 #[test]
932 fn set_on_toggle_replaces_rather_than_accumulates() {
933 let first = log_refany();
936 let mut c = CheckBox::create(false);
937 c.set_on_toggle(first.clone(), toggle_do_nothing as CheckBoxOnToggleCallbackType);
938 c.set_on_toggle(RefAny::new(1u8), toggle_refresh_all as CheckBoxOnToggleCallbackType);
939
940 let t = c.check_box_state.on_toggle.as_ref().expect("the callback vanished");
941 assert_eq!(
942 t.callback.cb as *const () as usize,
943 toggle_refresh_all as CheckBoxOnToggleCallbackType as *const () as usize,
944 "the second set_on_toggle did not win",
945 );
946 assert_eq!(read_log(&first).payload, 0xDEAD_BEEF);
948 }
949
950 #[test]
951 fn set_on_toggle_does_not_disturb_the_state_or_the_styles() {
952 for checked in [false, true] {
953 let pristine = CheckBox::create(checked);
954 let mut c = CheckBox::create(checked);
955 c.set_on_toggle(RefAny::new(0u8), toggle_do_nothing as CheckBoxOnToggleCallbackType);
956
957 assert_eq!(
958 c.check_box_state.inner.checked, checked,
959 "installing a callback flipped the checked flag",
960 );
961 assert_eq!(
962 properties(&c.container_style),
963 properties(&pristine.container_style),
964 "installing a callback rewrote the container style",
965 );
966 assert_eq!(
967 properties(&c.content_style),
968 properties(&pristine.content_style),
969 "installing a callback rewrote the content style",
970 );
971 }
972 }
973
974 #[test]
975 fn with_on_toggle_is_exactly_set_on_toggle_in_builder_form() {
976 let by_builder = CheckBox::create(true)
977 .with_on_toggle(RefAny::new(7u32), toggle_do_nothing as CheckBoxOnToggleCallbackType);
978
979 let mut by_setter = CheckBox::create(true);
980 by_setter.set_on_toggle(RefAny::new(7u32), toggle_do_nothing as CheckBoxOnToggleCallbackType);
981
982 assert_eq!(
983 by_builder.check_box_state.inner,
984 by_setter.check_box_state.inner,
985 );
986 assert_eq!(
987 properties(&by_builder.container_style),
988 properties(&by_setter.container_style),
989 );
990 assert_eq!(
991 properties(&by_builder.content_style),
992 properties(&by_setter.content_style),
993 );
994
995 let a = by_builder.check_box_state.on_toggle.as_ref().expect("builder lost the callback");
996 let b = by_setter.check_box_state.on_toggle.as_ref().expect("setter lost the callback");
997 assert_eq!(a.callback.cb as *const () as usize, b.callback.cb as *const () as usize);
998
999 let (mut a, mut b) = (a.refany.clone(), b.refany.clone());
1000 assert_eq!(
1001 *a.downcast_ref::<u32>().expect("builder payload changed type"),
1002 *b.downcast_ref::<u32>().expect("setter payload changed type"),
1003 );
1004 }
1005
1006 #[test]
1007 fn with_on_toggle_accepts_a_generic_callback_without_mangling_the_pointer() {
1008 let generic = Callback {
1012 cb: generic_shaped,
1013 ctx: azul_core::refany::OptionRefAny::None,
1014 };
1015 let expected = generic_shaped as *const () as usize;
1016
1017 let c = CheckBox::create(false).with_on_toggle(RefAny::new(0u8), generic);
1018 let t = c.check_box_state.on_toggle.as_ref().expect("the generic callback was dropped");
1019 assert_eq!(
1020 t.callback.cb as *const () as usize,
1021 expected,
1022 "the Callback -> CheckBoxOnToggleCallback transmute mangled the pointer",
1023 );
1024 }
1025
1026 #[test]
1031 fn dom_builds_a_focusable_container_with_exactly_one_content_child() {
1032 for checked in [false, true] {
1033 let dom = CheckBox::create(checked).dom();
1034
1035 assert!(matches!(dom.root.get_node_type(), NodeType::Div));
1036 assert_eq!(
1037 dom.root.flags.get_tab_index(),
1038 Some(TabIndex::Auto),
1039 "checked={checked}: the checkbox is not keyboard-focusable",
1040 );
1041 assert_eq!(classes(&dom), vec!["__azul-native-checkbox-container".to_string()]);
1042
1043 let children = dom.children.as_ref();
1044 assert_eq!(children.len(), 1, "checked={checked}: the checkbox must have exactly one child");
1045 assert_eq!(
1046 classes(&children[0]),
1047 vec!["__azul-native-checkbox-content".to_string()],
1048 );
1049 assert!(
1050 children[0].children.as_ref().is_empty(),
1051 "checked={checked}: the checkmark grew children",
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn dom_puts_the_container_style_on_the_box_and_the_content_style_on_the_mark() {
1058 for checked in [false, true] {
1061 let c = CheckBox::create(checked);
1062 let container = properties(&c.container_style);
1063 let content = properties(&c.content_style);
1064
1065 let dom = c.dom();
1066 assert_eq!(
1067 inline_properties(&dom),
1068 container,
1069 "checked={checked}: the container style did not land on the container",
1070 );
1071 assert_eq!(
1072 inline_properties(&dom.children.as_ref()[0]),
1073 content,
1074 "checked={checked}: the content style did not land on the checkmark",
1075 );
1076 }
1077 }
1078
1079 #[test]
1080 fn dom_registers_exactly_one_mouse_up_handler_and_it_is_the_widgets_own() {
1081 for checked in [false, true] {
1082 let dom = CheckBox::create(checked).dom();
1083 let callbacks = dom.root.callbacks.as_ref();
1084
1085 assert_eq!(callbacks.len(), 1, "checked={checked}: expected exactly one callback");
1086 assert_eq!(
1087 callbacks[0].event,
1088 EventFilter::Hover(HoverEventFilter::MouseUp),
1089 "checked={checked}: the checkbox must toggle on mouse-up",
1090 );
1091 assert_eq!(
1092 callbacks[0].callback.cb,
1093 input::default_on_checkbox_clicked as usize,
1094 "checked={checked}: the registered handler is not default_on_checkbox_clicked",
1095 );
1096
1097 assert!(
1100 dom.children.as_ref()[0].root.callbacks.as_ref().is_empty(),
1101 "checked={checked}: the checkmark registered a handler of its own",
1102 );
1103 }
1104 }
1105
1106 #[test]
1107 fn dom_hands_the_widget_state_to_the_handler_not_the_user_payload() {
1108 for checked in [false, true] {
1113 let dom = CheckBox::create(checked)
1114 .with_on_toggle(RefAny::new(9u32), toggle_do_nothing as CheckBoxOnToggleCallbackType)
1115 .dom();
1116
1117 let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
1118 let wrapper = state
1119 .downcast_ref::<CheckBoxStateWrapper>()
1120 .expect("the handler's RefAny is not a CheckBoxStateWrapper");
1121
1122 assert_eq!(wrapper.inner.checked, checked, "the checked flag was lost on the way into the DOM");
1123 assert!(
1124 wrapper.on_toggle.as_ref().is_some(),
1125 "the user's toggle callback was lost on the way into the DOM",
1126 );
1127 }
1128 }
1129
1130 #[test]
1131 fn dom_of_a_callback_less_checkbox_still_registers_the_toggle_handler() {
1132 let dom = CheckBox::create(false).dom();
1135 assert_eq!(dom.root.callbacks.as_ref().len(), 1);
1136
1137 let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
1138 let wrapper = state.downcast_ref::<CheckBoxStateWrapper>().expect("wrong RefAny type");
1139 assert!(wrapper.on_toggle.as_ref().is_none());
1140 }
1141
1142 #[test]
1143 fn from_checkbox_for_dom_is_the_same_as_calling_dom() {
1144 for checked in [false, true] {
1145 let via_from = Dom::from(CheckBox::create(checked));
1146 let via_dom = CheckBox::create(checked).dom();
1147
1148 assert_eq!(classes(&via_from), classes(&via_dom));
1149 assert_eq!(inline_properties(&via_from), inline_properties(&via_dom));
1150 assert_eq!(via_from.children.as_ref().len(), via_dom.children.as_ref().len());
1151 assert_eq!(
1152 via_from.root.callbacks.as_ref().len(),
1153 via_dom.root.callbacks.as_ref().len(),
1154 );
1155 assert_eq!(via_from.root.flags.get_tab_index(), via_dom.root.flags.get_tab_index());
1156 }
1157 }
1158
1159 #[test]
1160 fn the_rendered_dom_flattens_to_exactly_two_nodes() {
1161 let styled = StyledDom::create_from_dom(CheckBox::create(true).dom());
1164 assert_eq!(
1165 styled.node_data.as_ref().len(),
1166 2,
1167 "the checkbox no longer flattens to a container + a checkmark",
1168 );
1169 }
1170
1171 #[test]
1176 fn clicking_toggles_the_flag_and_pushes_the_matching_opacity_onto_the_checkmark() {
1177 let (styled, state) = laid_out(CheckBox::create(false));
1178 assert!(!is_checked(&state));
1179
1180 let (update, changes) = click(styled, &state, node(CONTAINER));
1181
1182 assert!(is_checked(&state), "the click did not tick the checkbox");
1183 assert!(
1184 matches!(update, Update::DoNothing),
1185 "with no user callback installed, the handler must report DoNothing",
1186 );
1187 assert_eq!(
1188 pushed_opacities(&changes),
1189 vec![(NodeId::new(CONTENT), 1.0)],
1190 "ticking the box did not make the checkmark opaque (on the *content* node)",
1191 );
1192 }
1193
1194 #[test]
1195 fn clicking_a_checked_box_unticks_it_and_hides_the_checkmark() {
1196 let (styled, state) = laid_out(CheckBox::create(true));
1197 let (_, changes) = click(styled, &state, node(CONTAINER));
1198
1199 assert!(!is_checked(&state), "the click did not untick the checkbox");
1200 assert_eq!(
1201 pushed_opacities(&changes),
1202 vec![(NodeId::new(CONTENT), 0.0)],
1203 "unticking the box left the checkmark visible",
1204 );
1205 }
1206
1207 #[test]
1208 fn clicking_twice_returns_to_the_original_state() {
1209 let (styled, state) = laid_out(CheckBox::create(false));
1210
1211 let (styled2, _) = laid_out(CheckBox::create(false));
1215 let (_, first) = click(styled, &state, node(CONTAINER));
1216 let (_, second) = click(styled2, &state, node(CONTAINER));
1217
1218 assert!(!is_checked(&state), "two clicks did not return the checkbox to its original state");
1219 assert_eq!(pushed_opacities(&first), vec![(NodeId::new(CONTENT), 1.0)]);
1220 assert_eq!(pushed_opacities(&second), vec![(NodeId::new(CONTENT), 0.0)]);
1221 }
1222
1223 #[test]
1224 fn clicking_with_a_refany_of_the_wrong_type_is_a_silent_no_op() {
1225 let (styled, _) = laid_out(CheckBox::create(false));
1228 let foreign = RefAny::new(0xDEAD_BEEF_u32);
1229
1230 let (update, changes) = click(styled, &foreign, node(CONTAINER));
1231
1232 assert!(matches!(update, Update::DoNothing));
1233 assert!(changes.is_empty(), "the handler wrote to the DOM through a foreign RefAny");
1234
1235 let mut foreign = foreign;
1236 assert_eq!(
1237 *foreign.downcast_ref::<u32>().expect("the foreign payload was reinterpreted"),
1238 0xDEAD_BEEF,
1239 "the handler corrupted a RefAny it did not understand",
1240 );
1241 }
1242
1243 #[test]
1244 fn clicking_a_childless_node_does_not_half_apply_the_toggle() {
1245 let (styled, state) = laid_out(CheckBox::create(false));
1249
1250 let (update, changes) = click(styled, &state, node(CONTENT));
1252
1253 assert!(matches!(update, Update::DoNothing));
1254 assert!(changes.is_empty(), "a change was pushed for a node the handler could not find");
1255 assert!(
1256 !is_checked(&state),
1257 "the checked flag was flipped even though the checkmark could not be updated",
1258 );
1259 }
1260
1261 #[test]
1262 fn clicking_a_node_that_is_not_in_the_layout_does_not_panic_or_toggle() {
1263 for hit in [node(99), node(usize::MAX - 1), node_none()] {
1269 let (styled, state) = laid_out(CheckBox::create(true));
1270 let (update, changes) = click(styled, &state, hit);
1271
1272 assert!(matches!(update, Update::DoNothing), "{hit:?}: a stale hit was acted on");
1273 assert!(changes.is_empty(), "{hit:?}: a stale hit pushed a DOM change");
1274 assert!(is_checked(&state), "{hit:?}: a stale hit toggled the checkbox");
1275 }
1276 }
1277
1278 #[test]
1279 fn the_toggle_callback_sees_the_new_state_and_its_verdict_is_forwarded() {
1280 let probe = log_refany();
1283 let (styled, state) = laid_out(
1284 CheckBox::create(false)
1285 .with_on_toggle(probe.clone(), record_toggle as CheckBoxOnToggleCallbackType),
1286 );
1287
1288 let (update, changes) = click(styled, &state, node(CONTAINER));
1289
1290 assert_eq!(
1291 read_log(&probe).seen,
1292 vec![true],
1293 "the toggle callback was not called exactly once with the NEW state",
1294 );
1295 assert!(
1296 matches!(update, Update::RefreshDom),
1297 "the user callback's Update was swallowed instead of forwarded",
1298 );
1299 assert_eq!(pushed_opacities(&changes), vec![(NodeId::new(CONTENT), 1.0)]);
1301 }
1302
1303 #[test]
1304 fn the_toggle_callback_receives_the_user_payload_not_the_widget_state() {
1305 let probe = log_refany();
1306 let (styled, state) = laid_out(
1307 CheckBox::create(true)
1308 .with_on_toggle(probe.clone(), record_toggle as CheckBoxOnToggleCallbackType),
1309 );
1310
1311 click(styled, &state, node(CONTAINER));
1312
1313 let log = read_log(&probe);
1314 assert_eq!(
1315 log.payload, 0xDEAD_BEEF,
1316 "the callback was handed something other than the user's own RefAny",
1317 );
1318 assert_eq!(log.seen, vec![false]);
1320 }
1321
1322 #[test]
1323 fn a_toggle_callback_that_declines_the_update_still_gets_the_checkmark_synced() {
1324 let (styled, state) = laid_out(
1327 CheckBox::create(false)
1328 .with_on_toggle(RefAny::new(0u8), toggle_do_nothing as CheckBoxOnToggleCallbackType),
1329 );
1330
1331 let (update, changes) = click(styled, &state, node(CONTAINER));
1332
1333 assert!(matches!(update, Update::DoNothing));
1334 assert!(is_checked(&state));
1335 assert_eq!(
1336 pushed_opacities(&changes),
1337 vec![(NodeId::new(CONTENT), 1.0)],
1338 "a DoNothing user callback suppressed the checkmark update",
1339 );
1340 }
1341
1342 #[test]
1343 fn a_toggle_callback_on_a_childless_node_is_never_called_at_all() {
1344 let probe = log_refany();
1347 let (styled, state) = laid_out(
1348 CheckBox::create(false)
1349 .with_on_toggle(probe.clone(), record_toggle as CheckBoxOnToggleCallbackType),
1350 );
1351
1352 let (update, changes) = click(styled, &state, node(CONTENT));
1353
1354 assert!(matches!(update, Update::DoNothing));
1355 assert!(changes.is_empty());
1356 assert!(!is_checked(&state));
1357 assert!(
1358 read_log(&probe).seen.is_empty(),
1359 "the user was notified of a toggle that never happened",
1360 );
1361 }
1362
1363 #[test]
1364 fn many_clicks_leave_the_state_and_the_pushed_opacity_in_agreement() {
1365 let mut state_after = false;
1369 let start = CheckBox::create(false);
1370 let (_, state) = laid_out(start);
1371
1372 for i in 0..101u32 {
1373 let (styled, _) = laid_out(CheckBox::create(false));
1374 let (_, changes) = click(styled, &state, node(CONTAINER));
1375 state_after = !state_after;
1376
1377 let expected = if state_after { 1.0 } else { 0.0 };
1378 assert_eq!(
1379 pushed_opacities(&changes),
1380 vec![(NodeId::new(CONTENT), expected)],
1381 "click #{i}: the pushed opacity disagrees with the checked flag",
1382 );
1383 assert_eq!(is_checked(&state), state_after, "click #{i}: the flag drifted");
1384 }
1385
1386 assert!(is_checked(&state), "an odd number of clicks left the checkbox unchecked");
1387 }
1388}