1
//! Checkbox widget with toggle callback support and default native-like styling.
2
//!
3
//! Key types: [`CheckBox`], [`CheckBoxState`], [`CheckBoxOnToggle`].
4

            
5
use azul_core::{
6
    callbacks::{CoreCallbackData, Update},
7
    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
8
    refany::RefAny,
9
};
10
use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
11
#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
12
use azul_css::{
13
    props::{
14
        basic::{color::ColorU, *},
15
        layout::*,
16
        property::{CssProperty, *},
17
        style::*,
18
    },
19
    *,
20
};
21

            
22
use crate::callbacks::{Callback, CallbackInfo};
23

            
24
static CHECKBOX_CONTAINER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
25
    "__azul-native-checkbox-container",
26
))];
27
static 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.
32
pub type CheckBoxOnToggleCallbackType =
33
    extern "C" fn(RefAny, CallbackInfo, CheckBoxState) -> Update;
34
impl_widget_callback!(
35
    CheckBoxOnToggle,
36
    OptionCheckBoxOnToggle,
37
    CheckBoxOnToggleCallback,
38
    CheckBoxOnToggleCallbackType
39
);
40

            
41
azul_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)]
57
pub 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)]
67
pub 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)]
77
pub struct CheckBoxState {
78
    pub checked: bool,
79
}
80

            
81
const BACKGROUND_COLOR: ColorU = ColorU {
82
    r: 255,
83
    g: 255,
84
    b: 255,
85
    a: 255,
86
}; // white
87
const BACKGROUND_THEME_LIGHT: &[StyleBackgroundContent] =
88
    &[StyleBackgroundContent::Color(BACKGROUND_COLOR)];
89
const BACKGROUND_COLOR_LIGHT: StyleBackgroundContentVec =
90
    StyleBackgroundContentVec::from_const_slice(BACKGROUND_THEME_LIGHT);
91
const COLOR_9B9B9B: ColorU = ColorU {
92
    r: 155,
93
    g: 155,
94
    b: 155,
95
    a: 255,
96
}; // #9b9b9b
97

            
98
const FILL_THEME: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(COLOR_9B9B9B)];
99
const FILL_COLOR_BACKGROUND: StyleBackgroundContentVec =
100
    StyleBackgroundContentVec::from_const_slice(FILL_THEME);
101

            
102
static 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

            
170
static 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

            
177
static 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

            
184
impl CheckBox {
185
188
    #[must_use] pub fn create(checked: bool) -> Self {
186
        Self {
187
188
            check_box_state: CheckBoxStateWrapper {
188
188
                inner: CheckBoxState { checked },
189
188
                ..Default::default()
190
188
            },
191
188
            container_style: CssPropertyWithConditionsVec::from_const_slice(
192
188
                DEFAULT_CHECKBOX_CONTAINER_STYLE,
193
            ),
194
188
            content_style: if checked {
195
35
                CssPropertyWithConditionsVec::from_const_slice(
196
35
                    DEFAULT_CHECKBOX_CONTENT_STYLE_CHECKED,
197
                )
198
            } else {
199
153
                CssPropertyWithConditionsVec::from_const_slice(
200
153
                    DEFAULT_CHECKBOX_CONTENT_STYLE_UNCHECKED,
201
                )
202
            },
203
        }
204
188
    }
205

            
206
    #[inline]
207
    #[must_use]
208
5
    pub fn swap_with_default(&mut self) -> Self {
209
5
        let mut s = Self::create(false);
210
5
        core::mem::swap(&mut s, self);
211
5
        s
212
5
    }
213

            
214
    #[inline]
215
28
    pub fn set_on_toggle<C: Into<CheckBoxOnToggleCallback>>(&mut self, data: RefAny, on_toggle: C) {
216
28
        self.check_box_state.on_toggle = Some(CheckBoxOnToggle {
217
28
            callback: on_toggle.into(),
218
28
            refany: data,
219
28
        })
220
28
        .into();
221
28
    }
222

            
223
    #[inline]
224
    #[must_use]
225
22
    pub fn with_on_toggle<C: Into<CheckBoxOnToggleCallback>>(
226
22
        mut self,
227
22
        data: RefAny,
228
22
        on_toggle: C,
229
22
    ) -> Self {
230
22
        self.set_on_toggle(data, on_toggle);
231
22
        self
232
22
    }
233

            
234
    #[inline]
235
145
    #[must_use] pub fn dom(self) -> Dom {
236
        use azul_core::{
237
            callbacks::{CoreCallback, CoreCallbackData},
238
            dom::{Dom, EventFilter, HoverEventFilter},
239
        };
240

            
241
145
        Dom::create_div()
242
145
            .with_ids_and_classes(IdOrClassVec::from(CHECKBOX_CONTAINER_CLASS))
243
145
            .with_css_props(self.container_style)
244
145
            .with_callbacks(
245
145
                vec![CoreCallbackData {
246
145
                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
247
145
                    callback: CoreCallback {
248
145
                        cb: input::default_on_checkbox_clicked as usize,
249
145
                        ctx: azul_core::refany::OptionRefAny::None,
250
145
                    },
251
145
                    refany: RefAny::new(self.check_box_state),
252
145
                }]
253
145
                .into(),
254
            )
255
145
            .with_tab_index(TabIndex::Auto)
256
145
            .with_children(
257
145
                vec![Dom::create_div()
258
145
                    .with_ids_and_classes(IdOrClassVec::from(CHECKBOX_CONTENT_CLASS))
259
145
                    .with_css_props(self.content_style)]
260
145
                .into(),
261
            )
262
145
    }
263
}
264

            
265
// handle input events for the checkbox
266
mod 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
114
    pub(super) extern "C" fn default_on_checkbox_clicked(
275
114
        mut check_box: RefAny,
276
114
        mut info: CallbackInfo,
277
114
    ) -> Update {
278
114
        let Some(mut check_box) = check_box.downcast_mut::<CheckBoxStateWrapper>() else {
279
1
            return Update::DoNothing;
280
        };
281

            
282
113
        let Some(checkbox_content_id) = info.get_first_child(info.get_hit_node()) else {
283
5
            return Update::DoNothing;
284
        };
285

            
286
108
        check_box.inner.checked = !check_box.inner.checked;
287

            
288
108
        let result = {
289
            // rustc doesn't understand the borrowing lifetime here
290
108
            let check_box = &mut *check_box;
291
108
            let ontoggle = &mut check_box.on_toggle;
292
108
            let inner = check_box.inner;
293

            
294
108
            match ontoggle.as_mut() {
295
                Some(CheckBoxOnToggle {
296
3
                    callback,
297
3
                    refany: data,
298
3
                }) => (callback.cb)(data.clone(), info, inner),
299
105
                None => Update::DoNothing,
300
            }
301
        };
302

            
303
108
        if check_box.inner.checked {
304
55
            info.set_css_property(
305
55
                checkbox_content_id,
306
55
                CssProperty::const_opacity(StyleOpacity::const_new(100)),
307
55
            );
308
55
        } else {
309
53
            info.set_css_property(
310
53
                checkbox_content_id,
311
53
                CssProperty::const_opacity(StyleOpacity::const_new(0)),
312
53
            );
313
53
        }
314

            
315
108
        result
316
114
    }
317
}
318

            
319
impl From<CheckBox> for Dom {
320
2
    fn from(b: CheckBox) -> Self {
321
2
        b.dom()
322
2
    }
323
}
324

            
325
#[cfg(all(test, feature = "std"))]
326
#[allow(clippy::float_cmp, clippy::too_many_lines)]
327
mod 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: Arc::new(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(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
                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());
        assert_eq!(
            *a.downcast_ref::<u32>().expect("builder payload changed type"),
            *b.downcast_ref::<u32>().expect("setter payload changed type"),
        );
    }
    #[test]
    fn with_on_toggle_accepts_a_generic_callback_without_mangling_the_pointer() {
        // The `From<Callback>` arm *transmutes* a 2-arg fn pointer into the 3-arg
        // checkbox slot — this is the FFI (Python/C) path. The pointer must come out
        // bit-identical; a mangled one would be called as a wild jump on the first click.
        let generic = Callback {
            cb: generic_shaped,
            ctx: OptionRefAny::None,
        };
        let expected = generic_shaped as *const () as usize;
        let c = CheckBox::create(false).with_on_toggle(RefAny::new(0u8), generic);
        let t = c.check_box_state.on_toggle.as_ref().expect("the generic callback was dropped");
        assert_eq!(
            t.callback.cb as *const () as usize,
            expected,
            "the Callback -> CheckBoxOnToggleCallback transmute mangled the pointer",
        );
    }
    // ==================================================================
    // CheckBox::dom
    // ==================================================================
    #[test]
    fn dom_builds_a_focusable_container_with_exactly_one_content_child() {
        for checked in [false, true] {
            let dom = CheckBox::create(checked).dom();
            assert!(matches!(dom.root.get_node_type(), NodeType::Div));
            assert_eq!(
                dom.root.flags.get_tab_index(),
                Some(TabIndex::Auto),
                "checked={checked}: the checkbox is not keyboard-focusable",
            );
            assert_eq!(classes(&dom), vec!["__azul-native-checkbox-container".to_string()]);
            let children = dom.children.as_ref();
            assert_eq!(children.len(), 1, "checked={checked}: the checkbox must have exactly one child");
            assert_eq!(
                classes(&children[0]),
                vec!["__azul-native-checkbox-content".to_string()],
            );
            assert!(
                children[0].children.as_ref().is_empty(),
                "checked={checked}: the checkmark grew children",
            );
        }
    }
    #[test]
    fn dom_puts_the_container_style_on_the_box_and_the_content_style_on_the_mark() {
        // Swapping the two would style the 8px mark like a 14px bordered box (and vice
        // versa) — the widget would still render, just wrong.
        for checked in [false, true] {
            let c = CheckBox::create(checked);
            let container = properties(&c.container_style);
            let content = properties(&c.content_style);
            let dom = c.dom();
            assert_eq!(
                inline_properties(&dom),
                container,
                "checked={checked}: the container style did not land on the container",
            );
            assert_eq!(
                inline_properties(&dom.children.as_ref()[0]),
                content,
                "checked={checked}: the content style did not land on the checkmark",
            );
        }
    }
    #[test]
    fn dom_registers_exactly_one_mouse_up_handler_and_it_is_the_widgets_own() {
        for checked in [false, true] {
            let dom = CheckBox::create(checked).dom();
            let callbacks = dom.root.callbacks.as_ref();
            assert_eq!(callbacks.len(), 1, "checked={checked}: expected exactly one callback");
            assert_eq!(
                callbacks[0].event,
                EventFilter::Hover(HoverEventFilter::MouseUp),
                "checked={checked}: the checkbox must toggle on mouse-up",
            );
            assert_eq!(
                callbacks[0].callback.cb,
                input::default_on_checkbox_clicked as usize,
                "checked={checked}: the registered handler is not default_on_checkbox_clicked",
            );
            // The mark itself must stay inert — a second handler there would toggle twice
            // per click (the event bubbles).
            assert!(
                dom.children.as_ref()[0].root.callbacks.as_ref().is_empty(),
                "checked={checked}: the checkmark registered a handler of its own",
            );
        }
    }
    #[test]
    fn dom_hands_the_widget_state_to_the_handler_not_the_user_payload() {
        // `dom()` moves `check_box_state` (state + on_toggle + user RefAny) into the
        // callback's RefAny. If it stored the *user's* payload instead, the handler's
        // `downcast_mut::<CheckBoxStateWrapper>()` would fail and every click would be
        // a silent no-op.
        for checked in [false, true] {
            let dom = CheckBox::create(checked)
                .with_on_toggle(RefAny::new(9u32), toggle_do_nothing as CheckBoxOnToggleCallbackType)
                .dom();
            let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
            let wrapper = state
                .downcast_ref::<CheckBoxStateWrapper>()
                .expect("the handler's RefAny is not a CheckBoxStateWrapper");
            assert_eq!(wrapper.inner.checked, checked, "the checked flag was lost on the way into the DOM");
            assert!(
                wrapper.on_toggle.as_ref().is_some(),
                "the user's toggle callback was lost on the way into the DOM",
            );
        }
    }
    #[test]
    fn dom_of_a_callback_less_checkbox_still_registers_the_toggle_handler() {
        // Unlike Button (which registers nothing without an on_click), the checkbox must
        // always install its own handler: the box has to tick even with no user callback.
        let dom = CheckBox::create(false).dom();
        assert_eq!(dom.root.callbacks.as_ref().len(), 1);
        let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
        let wrapper = state.downcast_ref::<CheckBoxStateWrapper>().expect("wrong RefAny type");
        assert!(wrapper.on_toggle.as_ref().is_none());
    }
    #[test]
    fn from_checkbox_for_dom_is_the_same_as_calling_dom() {
        for checked in [false, true] {
            let via_from = Dom::from(CheckBox::create(checked));
            let via_dom = CheckBox::create(checked).dom();
            assert_eq!(classes(&via_from), classes(&via_dom));
            assert_eq!(inline_properties(&via_from), inline_properties(&via_dom));
            assert_eq!(via_from.children.as_ref().len(), via_dom.children.as_ref().len());
            assert_eq!(
                via_from.root.callbacks.as_ref().len(),
                via_dom.root.callbacks.as_ref().len(),
            );
            assert_eq!(via_from.root.flags.get_tab_index(), via_dom.root.flags.get_tab_index());
        }
    }
    #[test]
    fn the_rendered_dom_flattens_to_exactly_two_nodes() {
        // `Dom::estimated_total_children` is a *cached* count; if it under-reports, the
        // flatten under-allocates its arenas. Two nodes: container (0), content (1).
        let styled = StyledDom::create_from_dom(CheckBox::create(true).dom());
        assert_eq!(
            styled.node_data.as_ref().len(),
            2,
            "the checkbox no longer flattens to a container + a checkmark",
        );
    }
    // ==================================================================
    // input::default_on_checkbox_clicked
    // ==================================================================
    #[test]
    fn clicking_toggles_the_flag_and_pushes_the_matching_opacity_onto_the_checkmark() {
        let (styled, state) = laid_out(CheckBox::create(false));
        assert!(!is_checked(&state));
        let (update, changes) = click(styled, &state, node(CONTAINER));
        assert!(is_checked(&state), "the click did not tick the checkbox");
        assert!(
            matches!(update, Update::DoNothing),
            "with no user callback installed, the handler must report DoNothing",
        );
        assert_eq!(
            pushed_opacities(&changes),
            vec![(NodeId::new(CONTENT), 1.0)],
            "ticking the box did not make the checkmark opaque (on the *content* node)",
        );
    }
    #[test]
    fn clicking_a_checked_box_unticks_it_and_hides_the_checkmark() {
        let (styled, state) = laid_out(CheckBox::create(true));
        let (_, changes) = click(styled, &state, node(CONTAINER));
        assert!(!is_checked(&state), "the click did not untick the checkbox");
        assert_eq!(
            pushed_opacities(&changes),
            vec![(NodeId::new(CONTENT), 0.0)],
            "unticking the box left the checkmark visible",
        );
    }
    #[test]
    fn clicking_twice_returns_to_the_original_state() {
        let (styled, state) = laid_out(CheckBox::create(false));
        // Two independent deliveries against the same widget state — the styled DOM is
        // rebuilt each time because the harness consumes it, but the RefAny is shared,
        // which is exactly how the real event loop drives it.
        let (styled2, _) = laid_out(CheckBox::create(false));
        let (_, first) = click(styled, &state, node(CONTAINER));
        let (_, second) = click(styled2, &state, node(CONTAINER));
        assert!(!is_checked(&state), "two clicks did not return the checkbox to its original state");
        assert_eq!(pushed_opacities(&first), vec![(NodeId::new(CONTENT), 1.0)]);
        assert_eq!(pushed_opacities(&second), vec![(NodeId::new(CONTENT), 0.0)]);
    }
    #[test]
    fn clicking_with_a_refany_of_the_wrong_type_is_a_silent_no_op() {
        // The handler downcasts blind; a foreign RefAny must bail out, not reinterpret
        // the bytes as a CheckBoxStateWrapper.
        let (styled, _) = laid_out(CheckBox::create(false));
        let foreign = RefAny::new(0xDEAD_BEEF_u32);
        let (update, changes) = click(styled, &foreign, node(CONTAINER));
        assert!(matches!(update, Update::DoNothing));
        assert!(changes.is_empty(), "the handler wrote to the DOM through a foreign RefAny");
        let mut foreign = foreign;
        assert_eq!(
            *foreign.downcast_ref::<u32>().expect("the foreign payload was reinterpreted"),
            0xDEAD_BEEF,
            "the handler corrupted a RefAny it did not understand",
        );
    }
    #[test]
    fn clicking_a_childless_node_does_not_half_apply_the_toggle() {
        // The handler needs the content node to sync the checkmark's opacity. If it is
        // not there, it must leave the *state* alone too — a flipped flag with no visual
        // update is a checkbox that renders the opposite of what it reports.
        let (styled, state) = laid_out(CheckBox::create(false));
        // The checkmark (node 1) is a leaf: it has no first child.
        let (update, changes) = click(styled, &state, node(CONTENT));
        assert!(matches!(update, Update::DoNothing));
        assert!(changes.is_empty(), "a change was pushed for a node the handler could not find");
        assert!(
            !is_checked(&state),
            "the checked flag was flipped even though the checkmark could not be updated",
        );
    }
    #[test]
    fn clicking_a_node_that_is_not_in_the_layout_does_not_panic_or_toggle() {
        // Stale hit ids reach callbacks after a DOM mutation. `set_css_property` *panics*
        // on a None node id, so the handler has to bail out before that point.
        // usize::MAX is unencodable by NodeId's 1-based scheme and would overflow while
        // building this fixture, before `click()` is even called. usize::MAX - 1 is the
        // repo's MAX_ENCODABLE_NODE and still absent from the layout.
        for hit in [node(99), node(usize::MAX - 1), node_none()] {
            let (styled, state) = laid_out(CheckBox::create(true));
            let (update, changes) = click(styled, &state, hit);
            assert!(matches!(update, Update::DoNothing), "{hit:?}: a stale hit was acted on");
            assert!(changes.is_empty(), "{hit:?}: a stale hit pushed a DOM change");
            assert!(is_checked(&state), "{hit:?}: a stale hit toggled the checkbox");
        }
    }
    #[test]
    fn the_toggle_callback_sees_the_new_state_and_its_verdict_is_forwarded() {
        // Order matters: the flag is flipped *before* the user callback runs, so the
        // callback observes the state the user just asked for — not the stale one.
        let probe = log_refany();
        let (styled, state) = laid_out(
            CheckBox::create(false)
                .with_on_toggle(probe.clone(), record_toggle as CheckBoxOnToggleCallbackType),
        );
        let (update, changes) = click(styled, &state, node(CONTAINER));
        assert_eq!(
            read_log(&probe).seen,
            vec![true],
            "the toggle callback was not called exactly once with the NEW state",
        );
        assert!(
            matches!(update, Update::RefreshDom),
            "the user callback's Update was swallowed instead of forwarded",
        );
        // ... and the opacity sync still happens *after* the user callback returns.
        assert_eq!(pushed_opacities(&changes), vec![(NodeId::new(CONTENT), 1.0)]);
    }
    #[test]
    fn the_toggle_callback_receives_the_user_payload_not_the_widget_state() {
        let probe = log_refany();
        let (styled, state) = laid_out(
            CheckBox::create(true)
                .with_on_toggle(probe.clone(), record_toggle as CheckBoxOnToggleCallbackType),
        );
        click(styled, &state, node(CONTAINER));
        let log = read_log(&probe);
        assert_eq!(
            log.payload, 0xDEAD_BEEF,
            "the callback was handed something other than the user's own RefAny",
        );
        // create(true) -> clicked once -> the callback must have seen `false`.
        assert_eq!(log.seen, vec![false]);
    }
    #[test]
    fn a_toggle_callback_that_declines_the_update_still_gets_the_checkmark_synced() {
        // A user callback returning DoNothing must not suppress the widget's own visual
        // bookkeeping — otherwise the flag says "checked" and the mark stays invisible.
        let (styled, state) = laid_out(
            CheckBox::create(false)
                .with_on_toggle(RefAny::new(0u8), toggle_do_nothing as CheckBoxOnToggleCallbackType),
        );
        let (update, changes) = click(styled, &state, node(CONTAINER));
        assert!(matches!(update, Update::DoNothing));
        assert!(is_checked(&state));
        assert_eq!(
            pushed_opacities(&changes),
            vec![(NodeId::new(CONTENT), 1.0)],
            "a DoNothing user callback suppressed the checkmark update",
        );
    }
    #[test]
    fn a_toggle_callback_on_a_childless_node_is_never_called_at_all() {
        // The bail-out happens before the flip *and* before the user callback: a click
        // that cannot be rendered must not be reported to the app either.
        let probe = log_refany();
        let (styled, state) = laid_out(
            CheckBox::create(false)
                .with_on_toggle(probe.clone(), record_toggle as CheckBoxOnToggleCallbackType),
        );
        let (update, changes) = click(styled, &state, node(CONTENT));
        assert!(matches!(update, Update::DoNothing));
        assert!(changes.is_empty());
        assert!(!is_checked(&state));
        assert!(
            read_log(&probe).seen.is_empty(),
            "the user was notified of a toggle that never happened",
        );
    }
    #[test]
    fn many_clicks_leave_the_state_and_the_pushed_opacity_in_agreement() {
        // 101 clicks starting unchecked -> checked. Every push must agree with the flag
        // it accompanies; a drift between the two is exactly the class of bug that makes
        // a checkbox render inverted after a while.
        let mut state_after = false;
        let start = CheckBox::create(false);
        let (_, state) = laid_out(start);
        for i in 0..101u32 {
            let (styled, _) = laid_out(CheckBox::create(false));
            let (_, changes) = click(styled, &state, node(CONTAINER));
            state_after = !state_after;
            let expected = if state_after { 1.0 } else { 0.0 };
            assert_eq!(
                pushed_opacities(&changes),
                vec![(NodeId::new(CONTENT), expected)],
                "click #{i}: the pushed opacity disagrees with the checked flag",
            );
            assert_eq!(is_checked(&state), state_after, "click #{i}: the flag drifted");
        }
        assert!(is_checked(&state), "an odd number of clicks left the checkbox unchecked");
    }
}