Skip to main content

azul_layout/widgets/
check_box.rs

1//! Checkbox widget with toggle callback support and default native-like styling.
2//!
3//! Key types: [`CheckBox`], [`CheckBoxState`], [`CheckBoxOnToggle`].
4
5use 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)] // widget/render module pulls in the css property/value types it builds with
12use 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
31/// Callback function type invoked when the checkbox is toggled.
32pub 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/// A toggleable checkbox widget with customizable styling and toggle callback.
55#[derive(Debug, Clone, PartialEq, Eq)]
56#[repr(C)]
57pub struct CheckBox {
58    pub check_box_state: CheckBoxStateWrapper,
59    /// Style for the checkbox container
60    pub container_style: CssPropertyWithConditionsVec,
61    /// Style for the checkbox content
62    pub content_style: CssPropertyWithConditionsVec,
63}
64
65#[derive(Debug, Default, Clone, PartialEq, Eq)]
66#[repr(C)]
67pub struct CheckBoxStateWrapper {
68    /// Content (image or text) of this `CheckBox`, centered by default
69    pub inner: CheckBoxState,
70    /// Optional: Function to call when the `CheckBox` is toggled
71    pub on_toggle: OptionCheckBoxOnToggle,
72}
73
74/// The checked/unchecked state of a [`CheckBox`].
75#[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}; // white
87const 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}; // #9b9b9b
97
98const 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    // padding: 2px
110    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    // border: 1px solid #484c52;
123    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
265// handle input events for the checkbox
266mod 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            // rustc doesn't understand the borrowing lifetime here
290            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    // ------------------------------------------------------------------
358    // Harness
359    // ------------------------------------------------------------------
360
361    /// The container is `14px` wide with `2px` padding and a `1px` border on each
362    /// side; the checkmark is `8px`. These four numbers are the entire geometry of
363    /// the widget, and `8 == 14 - 2*2 - 2*1` is the relation that makes the mark
364    /// fill the padding box exactly (see `content_exactly_fills_the_containers_padding_box`).
365    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    /// Flattened node ids of `CheckBox::dom()` (pre-order).
371    const CONTAINER: usize = 0;
372    const CONTENT: usize = 1;
373
374    /// A `DomNodeId` in the root DOM pointing at flattened node `idx`.
375    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    /// A `DomNodeId` whose node component is `None` — the "no concrete node was hit"
383    /// case. `CallbackInfo::set_css_property` *panics* on such an id, so the handler
384    /// must bail out before ever reaching it.
385    fn node_none() -> DomNodeId {
386        DomNodeId {
387            dom: DomId::ROOT_ID,
388            node: NodeHierarchyItemId::NONE,
389        }
390    }
391
392    /// A `DomLayoutResult` carrying only a `styled_dom`: the checkbox handler reaches
393    /// exactly two `CallbackInfo` queries (`get_hit_node`, `get_first_child`), and
394    /// both read the node hierarchy only — no real layout (and no font) is needed.
395    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    /// Runs `f` with a `CallbackInfo` whose window holds `styled_dom` as the root DOM
417    /// and whose hit node is `hit`. Returns `f`'s value plus every change the callback
418    /// pushed onto the transaction log.
419    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: &current_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    /// Renders `check_box`, then hands back both the laid-out DOM *and* the very
471    /// `RefAny` the widget registered on its own mouse-up callback. Driving the
472    /// handler with these two is the real wiring — nothing is re-created by hand,
473    /// so a mismatch between what `dom()` stores and what the handler expects
474    /// cannot hide behind the fixture.
475    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    /// One "mouse-up on `hit`" delivered to the widget's own registered handler.
482    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    /// The opacity overrides pushed onto the content node, in push order.
501    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    // ------------------------------------------------------------------
520    // Style-vec probes
521    // ------------------------------------------------------------------
522
523    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    /// The `f32` of a `PixelValue`, asserting it is an absolute `px` length. An `em`
532    /// or `%` slipping into the checkbox geometry would resolve against the parent
533    /// font/box, so a 14px box could render at any size at all.
534    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    /// The declared opacity, normalized to 0.0..=1.0. `StyleOpacity::const_new` takes
565    /// a *percentage*, so a `const_new(1)` typo yields 1% — a checkmark that is there
566    /// but invisible. This is the only property that distinguishes the two states.
567    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    /// The properties of a rendered node's *inline* style, in declaration order.
587    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    // ------------------------------------------------------------------
596    // Toggle callbacks
597    // ------------------------------------------------------------------
598
599    /// A payload the toggle callback writes into. It arrives as the `data: RefAny`
600    /// argument — a *shared* clone of what the test still holds — so the test can
601    /// read back exactly what the widget passed, without any global state.
602    #[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    /// A `Callback`-shaped (2-arg) function — the shape FFI bindings hand in, which the
636    /// `From<Callback>` arm *transmutes* into the 3-arg checkbox slot. Never called.
637    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    // ==================================================================
657    // CheckBox::create
658    // ==================================================================
659
660    #[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        // Same input -> same widget (the const style tables are shared, not rebuilt),
678        // and the checked/unchecked widgets must not compare equal — if they did, the
679        // two states would render identically.
680        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        // The container chrome must be byte-for-byte identical in both states: a box
692        // that changed size/colour when ticked would reflow its neighbours.
693        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        // ... and the *content* styles differ in opacity and nothing else.
702        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        // `StyleOpacity::const_new` takes a percentage: 100 -> 1.0, 0 -> 0.0. A swapped
721        // branch (or a `const_new(1)` typo) yields a checkbox that is permanently ticked
722        // or permanently blank — both of which still *type*-check.
723        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            // `px()` asserts SizeMetric::Px — an em/% here would scale with the parent.
740            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        // 8 == 14 - 2*2 - 2*1. The mark is sized to fill the box's padding box exactly
750        // under border-box sizing; if any of the four constants drifts, the checkmark
751        // either overflows its box or leaves a gap, and nothing else in the file would
752        // notice.
753        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        // A duplicate declaration means the later one silently wins — a latent
798        // "why is my override ignored" bug that never surfaces as an error.
799        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        // Without `cursor: pointer` the checkbox looks inert even though it is the
822        // node that carries the mouse-up handler.
823        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    // ==================================================================
837    // CheckBox::swap_with_default
838    // ==================================================================
839
840    #[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        // The callback (and its payload) left with the returned value ...
866        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        // ... and did NOT stay behind: a duplicated callback would fire twice, and a
878        // duplicated RefAny would double-free its payload.
879        assert!(
880            c.check_box_state.on_toggle.as_ref().is_none(),
881            "the toggle callback was copied instead of moved",
882        );
883
884        // The payload is still alive and unchanged after the move.
885        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(); // a = default, b = checked
892        let c = b.swap_with_default(); // b = default, c = checked
893
894        assert_eq!(c, CheckBox::create(true));
895        assert_eq!(a, CheckBox::create(false));
896        assert_eq!(b, CheckBox::create(false));
897    }
898
899    // ==================================================================
900    // CheckBox::set_on_toggle / with_on_toggle
901    // ==================================================================
902
903    #[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        // `OptionCheckBoxOnToggle` is a single slot; setting twice must leave the
934        // *second* callback installed (and must not leak the first one's RefAny).
935        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        // The displaced payload is still a valid, readable RefAny (not freed twice).
947        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        // The `From<Callback>` arm *transmutes* a 2-arg fn pointer into the 3-arg
1009        // checkbox slot — this is the FFI (Python/C) path. The pointer must come out
1010        // bit-identical; a mangled one would be called as a wild jump on the first click.
1011        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    // ==================================================================
1027    // CheckBox::dom
1028    // ==================================================================
1029
1030    #[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        // Swapping the two would style the 8px mark like a 14px bordered box (and vice
1059        // versa) — the widget would still render, just wrong.
1060        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            // The mark itself must stay inert — a second handler there would toggle twice
1098            // per click (the event bubbles).
1099            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        // `dom()` moves `check_box_state` (state + on_toggle + user RefAny) into the
1109        // callback's RefAny. If it stored the *user's* payload instead, the handler's
1110        // `downcast_mut::<CheckBoxStateWrapper>()` would fail and every click would be
1111        // a silent no-op.
1112        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        // Unlike Button (which registers nothing without an on_click), the checkbox must
1133        // always install its own handler: the box has to tick even with no user callback.
1134        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        // `Dom::estimated_total_children` is a *cached* count; if it under-reports, the
1162        // flatten under-allocates its arenas. Two nodes: container (0), content (1).
1163        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    // ==================================================================
1172    // input::default_on_checkbox_clicked
1173    // ==================================================================
1174
1175    #[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        // Two independent deliveries against the same widget state — the styled DOM is
1212        // rebuilt each time because the harness consumes it, but the RefAny is shared,
1213        // which is exactly how the real event loop drives it.
1214        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        // The handler downcasts blind; a foreign RefAny must bail out, not reinterpret
1226        // the bytes as a CheckBoxStateWrapper.
1227        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        // The handler needs the content node to sync the checkmark's opacity. If it is
1246        // not there, it must leave the *state* alone too — a flipped flag with no visual
1247        // update is a checkbox that renders the opposite of what it reports.
1248        let (styled, state) = laid_out(CheckBox::create(false));
1249
1250        // The checkmark (node 1) is a leaf: it has no first child.
1251        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        // Stale hit ids reach callbacks after a DOM mutation. `set_css_property` *panics*
1264        // on a None node id, so the handler has to bail out before that point.
1265        // usize::MAX is unencodable by NodeId's 1-based scheme and would overflow while
1266        // building this fixture, before `click()` is even called. usize::MAX - 1 is the
1267        // repo's MAX_ENCODABLE_NODE and still absent from the layout.
1268        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        // Order matters: the flag is flipped *before* the user callback runs, so the
1281        // callback observes the state the user just asked for — not the stale one.
1282        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        // ... and the opacity sync still happens *after* the user callback returns.
1300        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        // create(true) -> clicked once -> the callback must have seen `false`.
1319        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        // A user callback returning DoNothing must not suppress the widget's own visual
1325        // bookkeeping — otherwise the flag says "checked" and the mark stays invisible.
1326        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        // The bail-out happens before the flip *and* before the user callback: a click
1345        // that cannot be rendered must not be reported to the app either.
1346        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        // 101 clicks starting unchecked -> checked. Every push must agree with the flag
1366        // it accompanies; a drift between the two is exactly the class of bug that makes
1367        // a checkbox render inverted after a while.
1368        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}