1
//! Segmented control / button-group widget — a joined row of mutually-exclusive
2
//! buttons where exactly one is selected. A blend of the `tabs::TabHeader` row of
3
//! clickable labels and `button.rs`'s styling, with the stateful 3-type split
4
//! (state / state-wrapper / widget) of the other interactive widgets.
5
//!
6
//! Clicking a segment selects it: the internal handler computes the clicked
7
//! segment's index from its position among its siblings, updates the
8
//! `selected_index`, invokes the user's `on_change(index)`, and live-restyles
9
//! every segment (selected vs unselected) via `set_css_property`.
10
//!
11
//! Key types: [`Segmented`], [`SegmentedState`], [`SegmentedOnChange`].
12

            
13
use std::vec::Vec;
14

            
15
use azul_core::{
16
    callbacks::{CoreCallbackData, Update},
17
    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
18
    refany::RefAny,
19
};
20
use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
21
use azul_css::{
22
    props::{
23
        basic::{color::ColorU, StyleFontSize},
24
        layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutJustifyContent, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight},
25
        property::{CssProperty, *},
26
        style::{StyleBackgroundContent, StyleBackgroundContentVec, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderRightColor, StyleCursor, StyleTextAlign, StyleUserSelect, StyleTextColor, LayoutBorderLeftWidth, StyleBorderLeftStyle, StyleBorderLeftColor, StyleBorderTopLeftRadius, StyleBorderBottomLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomRightRadius},
27
    },
28
    impl_option_inner, AzString, StringVec,
29
};
30

            
31
use crate::callbacks::{Callback, CallbackInfo};
32

            
33
static SEGMENTED_CLASS: &[IdOrClass] =
34
    &[Class(AzString::from_const_str("__azul-native-segmented"))];
35
static SEGMENT_ITEM_CLASS: &[IdOrClass] =
36
    &[Class(AzString::from_const_str("__azul-native-segmented-item"))];
37

            
38
/// Callback function type invoked when the selected segment changes.
39
pub type SegmentedOnChangeCallbackType =
40
    extern "C" fn(RefAny, CallbackInfo, SegmentedState) -> Update;
41
impl_widget_callback!(
42
    SegmentedOnChange,
43
    OptionSegmentedOnChange,
44
    SegmentedOnChangeCallback,
45
    SegmentedOnChangeCallbackType
46
);
47

            
48
azul_core::impl_managed_callback! {
49
    wrapper:        SegmentedOnChangeCallback,
50
    info_ty:        CallbackInfo,
51
    return_ty:      Update,
52
    default_ret:    Update::DoNothing,
53
    invoker_static: SEGMENTED_ON_CHANGE_INVOKER,
54
    invoker_ty:     AzSegmentedOnChangeCallbackInvoker,
55
    thunk_fn:       az_segmented_on_change_callback_thunk,
56
    setter_fn:      AzApp_setSegmentedOnChangeCallbackInvoker,
57
    from_handle_fn: AzSegmentedOnChangeCallback_createFromHostHandle,
58
    extra_args:     [ state: SegmentedState ],
59
}
60

            
61
/// A joined row of mutually-exclusive segments with a selection callback.
62
#[derive(Debug, Clone, PartialEq, Eq)]
63
#[repr(C)]
64
pub struct Segmented {
65
    pub segmented_state: SegmentedStateWrapper,
66
    /// The label of each segment, in order.
67
    pub labels: StringVec,
68
    /// Style for the row container.
69
    pub container_style: CssPropertyWithConditionsVec,
70
}
71

            
72
#[derive(Debug, Default, Clone, PartialEq, Eq)]
73
#[repr(C)]
74
pub struct SegmentedStateWrapper {
75
    /// The current selection.
76
    pub inner: SegmentedState,
77
    /// Optional: function to call when the selection changes.
78
    pub on_change: OptionSegmentedOnChange,
79
}
80

            
81
/// State of a [`Segmented`]: the index of the currently selected segment.
82
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
83
#[repr(C)]
84
pub struct SegmentedState {
85
    /// Zero-based index of the selected segment.
86
    pub selected_index: usize,
87
}
88

            
89
// ---- colours ----
90
/// Segment border colour (#ced4da).
91
const SEG_BORDER_COLOR: ColorU = ColorU {
92
    r: 206,
93
    g: 212,
94
    b: 218,
95
    a: 255,
96
};
97
/// Selected-segment background (#0d6efd, accent blue).
98
const SEG_SELECTED_BG_COLOR: ColorU = ColorU {
99
    r: 13,
100
    g: 110,
101
    b: 253,
102
    a: 255,
103
};
104
/// Unselected-segment background (white).
105
const SEG_UNSELECTED_BG_COLOR: ColorU = ColorU {
106
    r: 255,
107
    g: 255,
108
    b: 255,
109
    a: 255,
110
};
111
/// Selected-segment text colour (white).
112
const SEG_SELECTED_TEXT: ColorU = ColorU {
113
    r: 255,
114
    g: 255,
115
    b: 255,
116
    a: 255,
117
};
118
/// Unselected-segment text colour (#212529, dark).
119
const SEG_UNSELECTED_TEXT: ColorU = ColorU {
120
    r: 33,
121
    g: 37,
122
    b: 41,
123
    a: 255,
124
};
125

            
126
const SEG_SELECTED_BG_ITEMS: &[StyleBackgroundContent] =
127
    &[StyleBackgroundContent::Color(SEG_SELECTED_BG_COLOR)];
128
const SEG_SELECTED_BG: StyleBackgroundContentVec =
129
    StyleBackgroundContentVec::from_const_slice(SEG_SELECTED_BG_ITEMS);
130
const SEG_UNSELECTED_BG_ITEMS: &[StyleBackgroundContent] =
131
    &[StyleBackgroundContent::Color(SEG_UNSELECTED_BG_COLOR)];
132
const SEG_UNSELECTED_BG: StyleBackgroundContentVec =
133
    StyleBackgroundContentVec::from_const_slice(SEG_UNSELECTED_BG_ITEMS);
134

            
135
const SEG_RADIUS: isize = 6;
136

            
137
/// Row container: a horizontal flex row that hugs its content.
138
static SEGMENTED_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
139
    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
140
    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
141
    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
142
    CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
143
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
144
];
145

            
146
/// Builds the style for one segment. The selected/unselected colours and the
147
/// rounding of the outer corners (only the first segment is rounded on the left,
148
/// only the last on the right) are the position-dependent properties, so the
149
/// style is built at runtime.
150
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
151
1320
fn build_segment_style(selected: bool, is_first: bool, is_last: bool) -> CssPropertyWithConditionsVec {
152
1320
    let (bg, text) = if selected {
153
134
        (SEG_SELECTED_BG, SEG_SELECTED_TEXT)
154
    } else {
155
1186
        (SEG_UNSELECTED_BG, SEG_UNSELECTED_TEXT)
156
    };
157

            
158
1320
    let mut v: Vec<CssPropertyWithConditions> = alloc::vec![
159
1320
        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
160
1320
        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
161
1320
            LayoutFlexDirection::Row,
162
        )),
163
1320
        CssPropertyWithConditions::simple(CssProperty::const_justify_content(
164
1320
            LayoutJustifyContent::Center,
165
        )),
166
1320
        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
167
1320
        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
168
            0,
169
        ))),
170
        // padding: 6px 12px
171
1320
        CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
172
            6,
173
        ))),
174
1320
        CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
175
1320
            LayoutPaddingBottom::const_px(6),
176
        )),
177
1320
        CssPropertyWithConditions::simple(CssProperty::const_padding_left(
178
1320
            LayoutPaddingLeft::const_px(12),
179
        )),
180
1320
        CssPropertyWithConditions::simple(CssProperty::const_padding_right(
181
1320
            LayoutPaddingRight::const_px(12),
182
        )),
183
        // top/bottom/right borders (the left border is added only for the first segment,
184
        // so adjacent segments share a single 1px separator)
185
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
186
1320
            LayoutBorderTopWidth::const_px(1),
187
        )),
188
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
189
1320
            LayoutBorderBottomWidth::const_px(1),
190
        )),
191
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
192
1320
            LayoutBorderRightWidth::const_px(1),
193
        )),
194
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
195
1320
            inner: BorderStyle::Solid,
196
1320
        })),
197
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
198
1320
            StyleBorderBottomStyle {
199
1320
                inner: BorderStyle::Solid,
200
1320
            },
201
        )),
202
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
203
1320
            StyleBorderRightStyle {
204
1320
                inner: BorderStyle::Solid,
205
1320
            },
206
        )),
207
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
208
1320
            inner: SEG_BORDER_COLOR,
209
1320
        })),
210
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
211
1320
            StyleBorderBottomColor {
212
1320
                inner: SEG_BORDER_COLOR,
213
1320
            },
214
        )),
215
1320
        CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
216
1320
            StyleBorderRightColor {
217
1320
                inner: SEG_BORDER_COLOR,
218
1320
            },
219
        )),
220
1320
        CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
221
1320
        CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
222
1320
        CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
223
1320
        CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
224
1320
        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
225
1320
        CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
226
1320
            inner: text,
227
1320
        })),
228
    ];
229

            
230
1320
    if is_first {
231
139
        v.push(CssPropertyWithConditions::simple(
232
139
            CssProperty::const_border_left_width(LayoutBorderLeftWidth::const_px(1)),
233
139
        ));
234
139
        v.push(CssPropertyWithConditions::simple(
235
139
            CssProperty::const_border_left_style(StyleBorderLeftStyle {
236
139
                inner: BorderStyle::Solid,
237
139
            }),
238
139
        ));
239
139
        v.push(CssPropertyWithConditions::simple(
240
139
            CssProperty::const_border_left_color(StyleBorderLeftColor {
241
139
                inner: SEG_BORDER_COLOR,
242
139
            }),
243
139
        ));
244
139
        v.push(CssPropertyWithConditions::simple(
245
139
            CssProperty::const_border_top_left_radius(StyleBorderTopLeftRadius::const_px(SEG_RADIUS)),
246
139
        ));
247
139
        v.push(CssPropertyWithConditions::simple(
248
139
            CssProperty::const_border_bottom_left_radius(StyleBorderBottomLeftRadius::const_px(
249
139
                SEG_RADIUS,
250
139
            )),
251
139
        ));
252
1181
    }
253
1320
    if is_last {
254
139
        v.push(CssPropertyWithConditions::simple(
255
139
            CssProperty::const_border_top_right_radius(StyleBorderTopRightRadius::const_px(
256
139
                SEG_RADIUS,
257
139
            )),
258
139
        ));
259
139
        v.push(CssPropertyWithConditions::simple(
260
139
            CssProperty::const_border_bottom_right_radius(StyleBorderBottomRightRadius::const_px(
261
139
                SEG_RADIUS,
262
139
            )),
263
139
        ));
264
1181
    }
265

            
266
1320
    CssPropertyWithConditionsVec::from_vec(v)
267
1320
}
268

            
269
impl Segmented {
270
    /// Creates a segmented control from the given labels, with the first segment selected.
271
154
    #[must_use] pub fn create(labels: StringVec) -> Self {
272
154
        Self {
273
154
            segmented_state: SegmentedStateWrapper {
274
154
                inner: SegmentedState { selected_index: 0 },
275
154
                ..Default::default()
276
154
            },
277
154
            labels,
278
154
            container_style: CssPropertyWithConditionsVec::from_const_slice(
279
154
                SEGMENTED_CONTAINER_STYLE,
280
154
            ),
281
154
        }
282
154
    }
283

            
284
    /// Sets the currently selected segment index.
285
    #[inline]
286
78
    pub const fn set_selected_index(&mut self, selected_index: usize) {
287
78
        self.segmented_state.inner.selected_index = selected_index;
288
78
    }
289

            
290
    /// Builder-style setter for the selected segment index.
291
    #[inline]
292
46
    #[must_use] pub const fn with_selected_index(mut self, selected_index: usize) -> Self {
293
46
        self.set_selected_index(selected_index);
294
46
        self
295
46
    }
296

            
297
    #[inline]
298
6
    #[must_use] pub fn swap_with_default(&mut self) -> Self {
299
6
        let mut s = Self::create(StringVec::from_const_slice(&[]));
300
6
        core::mem::swap(&mut s, self);
301
6
        s
302
6
    }
303

            
304
    #[inline]
305
15
    pub fn set_on_change<C: Into<SegmentedOnChangeCallback>>(
306
15
        &mut self,
307
15
        data: RefAny,
308
15
        on_change: C,
309
15
    ) {
310
15
        self.segmented_state.on_change = Some(SegmentedOnChange {
311
15
            callback: on_change.into(),
312
15
            refany: data,
313
15
        })
314
15
        .into();
315
15
    }
316

            
317
    #[inline]
318
10
    #[must_use] pub fn with_on_change<C: Into<SegmentedOnChangeCallback>>(
319
10
        mut self,
320
10
        data: RefAny,
321
10
        on_change: C,
322
10
    ) -> Self {
323
10
        self.set_on_change(data, on_change);
324
10
        self
325
10
    }
326

            
327
70
    #[must_use] pub fn dom(self) -> Dom {
328
        use azul_core::{
329
            callbacks::CoreCallback,
330
            dom::{EventFilter, HoverEventFilter},
331
            refany::OptionRefAny,
332
        };
333

            
334
70
        let selected = self.segmented_state.inner.selected_index;
335
70
        let count = self.labels.as_ref().len();
336

            
337
        // One shared RefAny across every segment's callback (RefAny::clone shares
338
        // the underlying state — same pattern as tabs/map).
339
70
        let state = RefAny::new(self.segmented_state);
340

            
341
70
        let mut children: Vec<Dom> = Vec::with_capacity(count);
342
1138
        for (i, label) in self.labels.as_ref().iter().enumerate() {
343
1138
            let is_first = i == 0;
344
1138
            let is_last = i + 1 == count;
345
1138
            let seg_style = build_segment_style(i == selected, is_first, is_last);
346
1138

            
347
1138
            children.push(
348
1138
                Dom::create_p_with_text(label.clone())
349
1138
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(SEGMENT_ITEM_CLASS))
350
1138
                    .with_css_props(seg_style)
351
1138
                    .with_callbacks(
352
1138
                        vec![CoreCallbackData {
353
1138
                            event: EventFilter::Hover(HoverEventFilter::MouseUp),
354
1138
                            callback: CoreCallback {
355
1138
                                cb: on_segment_click as usize,
356
1138
                                ctx: OptionRefAny::None,
357
1138
                            },
358
1138
                            refany: state.clone(),
359
1138
                        }]
360
1138
                        .into(),
361
1138
                    )
362
1138
                    .with_tab_index(TabIndex::Auto),
363
1138
            );
364
1138
        }
365

            
366
70
        Dom::create_div()
367
70
            .with_ids_and_classes(IdOrClassVec::from_const_slice(SEGMENTED_CLASS))
368
70
            .with_css_props(self.container_style)
369
70
            .with_children(children.into())
370
70
    }
371
}
372

            
373
impl Default for Segmented {
374
8
    fn default() -> Self {
375
8
        Self::create(StringVec::from_const_slice(&[]))
376
8
    }
377
}
378

            
379
/// Click handler shared by all segments. Determines the clicked segment's index
380
/// from its position among its siblings, updates the selection, invokes the user
381
/// callback, and live-restyles every segment.
382
26
extern "C" fn on_segment_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
383
    use azul_core::dom::DomNodeId;
384

            
385
26
    let clicked = info.get_hit_node();
386
26
    let Some(parent) = info.get_parent(clicked) else {
387
3
        return Update::DoNothing;
388
    };
389

            
390
    // Collect the segment siblings in document order.
391
23
    let mut segments: Vec<DomNodeId> = Vec::new();
392
23
    let mut cur = info.get_first_child(parent);
393
719
    while let Some(node) = cur {
394
696
        segments.push(node);
395
696
        cur = info.get_next_sibling(node);
396
696
    }
397

            
398
358
    let Some(selected) = segments.iter().position(|n| *n == clicked) else {
399
        return Update::DoNothing;
400
    };
401

            
402
21
    let result = {
403
23
        let Some(mut seg) = data.downcast_mut::<SegmentedStateWrapper>() else {
404
2
            return Update::DoNothing;
405
        };
406
21
        seg.inner.selected_index = selected;
407
21
        let inner = seg.inner;
408
21
        let seg = &mut *seg;
409
21
        match seg.on_change.as_mut() {
410
5
            Some(SegmentedOnChange { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
411
16
            None => Update::DoNothing,
412
        }
413
    };
414

            
415
    // Live-restyle: selected segment gets the accent fill + light text,
416
    // the rest get the neutral fill + dark text.
417
692
    for (i, node) in segments.iter().enumerate() {
418
692
        if i == selected {
419
21
            info.set_css_property(*node, CssProperty::const_background_content(SEG_SELECTED_BG));
420
21
            info.set_css_property(
421
21
                *node,
422
21
                CssProperty::const_text_color(StyleTextColor {
423
21
                    inner: SEG_SELECTED_TEXT,
424
21
                }),
425
21
            );
426
671
        } else {
427
671
            info.set_css_property(
428
671
                *node,
429
671
                CssProperty::const_background_content(SEG_UNSELECTED_BG),
430
671
            );
431
671
            info.set_css_property(
432
671
                *node,
433
671
                CssProperty::const_text_color(StyleTextColor {
434
671
                    inner: SEG_UNSELECTED_TEXT,
435
671
                }),
436
671
            );
437
671
        }
438
    }
439

            
440
21
    result
441
26
}
442

            
443
impl From<Segmented> for Dom {
444
1
    fn from(s: Segmented) -> Self {
445
1
        s.dom()
446
1
    }
447
}
448

            
449
#[cfg(test)]
450
mod autotest_generated {
451
    use std::{
452
        collections::{BTreeMap, HashMap, HashSet},
453
        sync::{Arc, Mutex},
454
    };
455

            
456
    use azul_core::{
457
        dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
458
        geom::{LogicalRect, OptionLogicalPosition},
459
        gl::OptionGlContextPtr,
460
        hit_test::ScrollPosition,
461
        refany::OptionRefAny,
462
        resources::RendererResources,
463
        styled_dom::{NodeHierarchyItemId, StyledDom},
464
        window::{MonitorVec, RawWindowHandle},
465
    };
466
    use azul_css::{
467
        props::basic::{length::SizeMetric, pixel::PixelValue},
468
        system::SystemStyle,
469
    };
470
    use rust_fontconfig::FcFontCache;
471

            
472
    use super::*;
473
    #[cfg(feature = "icu")]
474
    use crate::icu::IcuLocalizerHandle;
475
    use crate::{
476
        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
477
        solver3::{display_list::DisplayList, layout_tree::LayoutTree},
478
        window::{DomLayoutResult, LayoutWindow},
479
        window_state::FullWindowState,
480
    };
481

            
482
    // ------------------------------------------------------------------
483
    // Helpers
484
    // ------------------------------------------------------------------
485

            
486
    fn labels(v: &[&str]) -> StringVec {
487
        StringVec::from_vec(v.iter().map(|s| AzString::from(*s)).collect::<Vec<_>>())
488
    }
489

            
490
    /// `n` distinct labels: `s0, s1, … s{n-1}`.
491
    fn n_labels(n: usize) -> StringVec {
492
        StringVec::from_vec((0..n).map(|i| AzString::from(format!("s{i}"))).collect::<Vec<_>>())
493
    }
494

            
495
    /// The eight possible `(selected, is_first, is_last)` argument triples —
496
    /// the complete input domain of `build_segment_style`.
497
    const ALL_FLAGS: [(bool, bool, bool); 8] = [
498
        (false, false, false),
499
        (false, false, true),
500
        (false, true, false),
501
        (false, true, true),
502
        (true, false, false),
503
        (true, false, true),
504
        (true, true, false),
505
        (true, true, true),
506
    ];
507

            
508
    /// The declared properties of a style vec, in declaration order.
509
    fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
510
        v.as_ref().iter().map(|p| p.property.clone()).collect()
511
    }
512

            
513
    /// The *kind* of every declared property, in order (ignores the values).
514
    fn property_kinds(
515
        v: &CssPropertyWithConditionsVec,
516
    ) -> Vec<core::mem::Discriminant<CssProperty>> {
517
        v.as_ref().iter().map(|p| core::mem::discriminant(&p.property)).collect()
518
    }
519

            
520
    fn declares(v: &CssPropertyWithConditionsVec, pred: impl Fn(&CssProperty) -> bool) -> usize {
521
        v.as_ref().iter().filter(|p| pred(&p.property)).count()
522
    }
523

            
524
    /// The `f32` of a `PixelValue`, asserting it is an absolute `px` length — an
525
    /// `em`/`%` slipping into the segment geometry would resolve against the
526
    /// parent font/box instead of the intended fixed padding, border or radius.
527
    fn px(pv: &PixelValue) -> f32 {
528
        assert_eq!(
529
            pv.metric,
530
            SizeMetric::Px,
531
            "segment geometry must be absolute px, got {:?}",
532
            pv.metric
533
        );
534
        pv.number.get()
535
    }
536

            
537
    /// The four paddings in `(top, bottom, left, right)` order.
538
    fn padding_px(
539
        v: &CssPropertyWithConditionsVec,
540
    ) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
541
        let find =
542
            |f: &dyn Fn(&CssProperty) -> Option<f32>| v.as_ref().iter().find_map(|p| f(&p.property));
543
        (
544
            find(&|p| match p {
545
                CssProperty::PaddingTop(x) => x.get_property().map(|x| px(&x.inner)),
546
                _ => None,
547
            }),
548
            find(&|p| match p {
549
                CssProperty::PaddingBottom(x) => x.get_property().map(|x| px(&x.inner)),
550
                _ => None,
551
            }),
552
            find(&|p| match p {
553
                CssProperty::PaddingLeft(x) => x.get_property().map(|x| px(&x.inner)),
554
                _ => None,
555
            }),
556
            find(&|p| match p {
557
                CssProperty::PaddingRight(x) => x.get_property().map(|x| px(&x.inner)),
558
                _ => None,
559
            }),
560
        )
561
    }
562

            
563
    /// The four corner radii as `(top_left, top_right, bottom_left, bottom_right)`.
564
    fn radii_px(
565
        v: &CssPropertyWithConditionsVec,
566
    ) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
567
        let find =
568
            |f: &dyn Fn(&CssProperty) -> Option<f32>| v.as_ref().iter().find_map(|p| f(&p.property));
569
        (
570
            find(&|p| match p {
571
                CssProperty::BorderTopLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
572
                _ => None,
573
            }),
574
            find(&|p| match p {
575
                CssProperty::BorderTopRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
576
                _ => None,
577
            }),
578
            find(&|p| match p {
579
                CssProperty::BorderBottomLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
580
                _ => None,
581
            }),
582
            find(&|p| match p {
583
                CssProperty::BorderBottomRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
584
                _ => None,
585
            }),
586
        )
587
    }
588

            
589
    /// The four border widths as `(top, bottom, left, right)`.
590
    fn border_widths_px(
591
        v: &CssPropertyWithConditionsVec,
592
    ) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
593
        let find =
594
            |f: &dyn Fn(&CssProperty) -> Option<f32>| v.as_ref().iter().find_map(|p| f(&p.property));
595
        (
596
            find(&|p| match p {
597
                CssProperty::BorderTopWidth(x) => x.get_property().map(|x| px(&x.inner)),
598
                _ => None,
599
            }),
600
            find(&|p| match p {
601
                CssProperty::BorderBottomWidth(x) => x.get_property().map(|x| px(&x.inner)),
602
                _ => None,
603
            }),
604
            find(&|p| match p {
605
                CssProperty::BorderLeftWidth(x) => x.get_property().map(|x| px(&x.inner)),
606
                _ => None,
607
            }),
608
            find(&|p| match p {
609
                CssProperty::BorderRightWidth(x) => x.get_property().map(|x| px(&x.inner)),
610
                _ => None,
611
            }),
612
        )
613
    }
614

            
615
    fn font_size_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
616
        v.as_ref().iter().find_map(|p| match &p.property {
617
            CssProperty::FontSize(f) => f.get_property().map(|f| px(&f.inner)),
618
            _ => None,
619
        })
620
    }
621

            
622
    fn text_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
623
        v.as_ref().iter().find_map(|p| match &p.property {
624
            CssProperty::TextColor(c) => c.get_property().map(|c| c.inner),
625
            _ => None,
626
        })
627
    }
628

            
629
    /// The single background layer of a style vec, asserting there is exactly one
630
    /// and that it is a flat colour (a gradient would not be a `Color`).
631
    fn background_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
632
        let bg = v.as_ref().iter().find_map(|p| match &p.property {
633
            CssProperty::BackgroundContent(b) => b.get_property(),
634
            _ => None,
635
        })?;
636
        assert_eq!(bg.as_ref().len(), 1, "a segment must declare exactly one background layer");
637
        match &bg.as_ref()[0] {
638
            StyleBackgroundContent::Color(c) => Some(*c),
639
            other => panic!("segment background is not a flat colour: {other:?}"),
640
        }
641
    }
642

            
643
    /// Perceived brightness (0..=255) of an sRGB colour, Rec.709 weights. Kept to
644
    /// plain `+`/`*` (no gamma expansion) so the readability assertions stay exact
645
    /// and toolchain-independent.
646
    fn luma(c: ColorU) -> f32 {
647
        0.2126 * f32::from(c.r) + 0.7152 * f32::from(c.g) + 0.0722 * f32::from(c.b)
648
    }
649

            
650
    /// The text of a text node, looking through the `<p>` block wrapper the
651
    /// label convention mandates (`p > text`).
652
    fn text_of(node: &Dom) -> Option<&str> {
653
        match node.root.get_node_type() {
654
            NodeType::Text(s) => Some(s.as_ref().as_str()),
655
            NodeType::P => match node.children.as_ref() {
656
                [only] => match only.root.get_node_type() {
657
                    NodeType::Text(s) => Some(s.as_ref().as_str()),
658
                    _ => None,
659
                },
660
                _ => None,
661
            },
662
            _ => None,
663
        }
664
    }
665

            
666
    /// The properties of a rendered node's *inline* style, in declaration order.
667
    fn inline_properties(node: &Dom) -> Vec<CssProperty> {
668
        node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
669
    }
670

            
671
    /// The true recursive descendant count of a `Dom` — what
672
    /// `estimated_total_children` is documented to cache.
673
    fn recursive_descendants(node: &Dom) -> usize {
674
        node.children.as_ref().iter().map(|c| 1 + recursive_descendants(c)).sum()
675
    }
676

            
677
    /// Boundary + "negative" selection indices. `usize` has no negative values, so
678
    /// a `-1` handed in through FFI arrives here as `usize::MAX`; both wrapped
679
    /// forms are included so the setter is exercised at the two's-complement ends.
680
    fn boundary_indices() -> Vec<usize> {
681
        vec![
682
            0,
683
            1,
684
            2,
685
            usize::MAX / 2,
686
            usize::MAX / 2 + 1,
687
            usize::MAX - 1,
688
            usize::MAX,
689
            (-1i64) as usize,
690
            i64::MIN as usize,
691
            u32::MAX as usize,
692
        ]
693
    }
694

            
695
    /// Adversarial segment labels: empty, whitespace, combining marks, ZWJ emoji,
696
    /// RTL, embedded NULs (`AzString` is length-based, so a NUL must not
697
    /// truncate), control characters, and a string far longer than any plausible
698
    /// segment caption.
699
    fn adversarial_strings() -> Vec<String> {
700
        let mut v: Vec<String> = [
701
            "",
702
            "Day",
703
            " ",
704
            "e\u{0301}",                                   // e + combining acute
705
            "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}", // ZWJ family emoji
706
            "\u{5E9}\u{5DC}\u{5D5}\u{5DD}",                // RTL Hebrew
707
            "\0",                                          // a single NUL
708
            "a\0b",                                        // embedded NUL
709
            "\u{FFFD}\u{202E}\u{200B}",                    // replacement, RTL override, ZWSP
710
            "line\nbreak\ttab",                            // control characters
711
            "-9223372036854775808",                        // i64::MIN as a caption
712
        ]
713
        .iter()
714
        .map(|s| (*s).to_string())
715
        .collect();
716
        v.push("x".repeat(100_000));
717
        v
718
    }
719

            
720
    /// Forces the `fn`-item -> `fn`-pointer coercion the `Into` bound needs.
721
    fn change_cb(f: SegmentedOnChangeCallbackType) -> SegmentedOnChangeCallback {
722
        f.into()
723
    }
724

            
725
    /// A `RefAny` payload recording every index a user `on_change` sees.
726
    struct IndexLog {
727
        seen: Vec<usize>,
728
    }
729

            
730
    extern "C" fn record_index(mut data: RefAny, _: CallbackInfo, state: SegmentedState) -> Update {
731
        if let Some(mut log) = data.downcast_mut::<IndexLog>() {
732
            log.seen.push(state.selected_index);
733
        }
734
        Update::RefreshDom
735
    }
736

            
737
    extern "C" fn change_do_nothing(_: RefAny, _: CallbackInfo, _: SegmentedState) -> Update {
738
        Update::DoNothing
739
    }
740

            
741
    extern "C" fn change_refresh_all(_: RefAny, _: CallbackInfo, state: SegmentedState) -> Update {
742
        // `selected_index` is read (and discarded) purely so this body cannot be
743
        // identical-code-folded onto another handler; the tests below compare
744
        // callback function pointers for equality/inequality.
745
        let _ = state.selected_index;
746
        Update::RefreshDomAllWindows
747
    }
748

            
749
    /// A payload whose callback tries to read the *same* `SegmentedStateWrapper`
750
    /// `RefAny` that the handler is currently holding a mutable borrow on.
751
    struct ReentrantProbe {
752
        /// A clone of the state `RefAny` the handler was invoked with.
753
        state: RefAny,
754
        /// `Some(index)` if the re-entrant read succeeded, `None` if it was
755
        /// refused. Starts as `Some(usize::MAX)` so "never ran" is distinguishable.
756
        saw_index: Option<usize>,
757
        calls: usize,
758
    }
759

            
760
    extern "C" fn probe_state_reentrantly(
761
        mut data: RefAny,
762
        _: CallbackInfo,
763
        _: SegmentedState,
764
    ) -> Update {
765
        if let Some(mut probe) = data.downcast_mut::<ReentrantProbe>() {
766
            probe.calls += 1;
767
            let mut state = probe.state.clone();
768
            probe.saw_index =
769
                state.downcast_ref::<SegmentedStateWrapper>().map(|w| w.inner.selected_index);
770
        }
771
        Update::DoNothing
772
    }
773

            
774
    fn log_indices(data: &mut RefAny) -> Vec<usize> {
775
        data.downcast_ref::<IndexLog>().expect("payload must still be an IndexLog").seen.clone()
776
    }
777

            
778
    fn selected_index_of(data: &mut RefAny) -> usize {
779
        data.downcast_ref::<SegmentedStateWrapper>()
780
            .expect("payload must still be a SegmentedStateWrapper")
781
            .inner
782
            .selected_index
783
    }
784

            
785
    /// The `RefAny` carried by segment `i`'s click callback.
786
    fn segment_state(dom: &Dom, i: usize) -> RefAny {
787
        let cbs = dom.children.as_ref()[i].root.get_callbacks();
788
        cbs.as_ref()
789
            .first()
790
            .expect("every segment must carry the click callback")
791
            .refany
792
            .clone()
793
    }
794

            
795
    /// A `DomLayoutResult` with an *empty* layout tree: `on_segment_click` only
796
    /// walks `styled_dom.node_hierarchy`, so no real layout (and no font) is needed.
797
    fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
798
        DomLayoutResult {
799
            styled_dom,
800
            layout_tree: LayoutTree {
801
                nodes: Vec::new(),
802
                warm: Vec::new(),
803
                cold: Vec::new(),
804
                root: 0,
805
                dom_to_layout: BTreeMap::new(),
806
                children_arena: Vec::new(),
807
                children_offsets: Vec::new(),
808
                subtree_needs_intrinsic: Vec::new(),
809
            },
810
            calculated_positions: Vec::new(),
811
            viewport: LogicalRect::zero(),
812
            display_list: Arc::new(DisplayList::default()),
813
            scroll_ids: HashMap::new(),
814
            scroll_id_to_node_id: HashMap::new(),
815
        }
816
    }
817

            
818
    /// Flattens `seg.dom()` and hands back the shared state `RefAny` the segment
819
    /// callbacks carry. Requires at least one label.
820
    fn flatten(seg: Segmented) -> (StyledDom, RefAny) {
821
        let dom = seg.dom();
822
        let state = segment_state(&dom, 0);
823
        (StyledDom::create_from_dom(dom), state)
824
    }
825

            
826
    /// Flattened (pre-order) node id of segment `i`. Every segment is a `<p>`
827
    /// wrapping one bare text node, so the tree is
828
    /// `0 root / 1 seg0 <p> / 2 seg0 text / 3 seg1 <p> / …` and the callback
829
    /// sits on the `<p>`.
830
    const fn seg_node(i: usize) -> usize {
831
        2 * i + 1
832
    }
833

            
834
    /// Invokes `on_segment_click` against a `LayoutWindow` holding `styled` (or
835
    /// nothing at all, when `styled` is `None`), with node `hit` as the hit node.
836
    /// Returns the `Update` plus every recorded `CallbackChange`.
837
    fn run_click(
838
        styled: Option<StyledDom>,
839
        hit: usize,
840
        data: RefAny,
841
    ) -> (Update, Vec<CallbackChange>) {
842
        let mut layout_window =
843
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
844
        if let Some(sd) = styled {
845
            layout_window.layout_results.insert(DomId::ROOT_ID, layout_result(sd));
846
        }
847

            
848
        let renderer_resources = RendererResources::default();
849
        let previous_window_state: Option<FullWindowState> = None;
850
        let current_window_state = FullWindowState::default();
851
        let gl_context = OptionGlContextPtr::None;
852
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
853
            BTreeMap::new();
854
        let window_handle = RawWindowHandle::Unsupported;
855
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
856

            
857
        let ref_data = CallbackInfoRefData {
858
            layout_window: &layout_window,
859
            renderer_resources: &renderer_resources,
860
            previous_window_state: &previous_window_state,
861
            current_window_state: &current_window_state,
862
            gl_context: &gl_context,
863
            current_scroll_manager: &scroll_states,
864
            current_window_handle: &window_handle,
865
            system_callbacks: &system_callbacks,
866
            system_style: Arc::new(SystemStyle::default()),
867
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
868
            #[cfg(feature = "icu")]
869
            icu_localizer: IcuLocalizerHandle::default(),
870
            ctx: OptionRefAny::None,
871
        };
872

            
873
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
874

            
875
        let info = CallbackInfo::new(
876
            &ref_data,
877
            &changes,
878
            DomNodeId {
879
                dom: DomId::ROOT_ID,
880
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
881
            },
882
            OptionLogicalPosition::None,
883
            OptionLogicalPosition::None,
884
        );
885

            
886
        let update = on_segment_click(data, info);
887
        let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
888
        (update, recorded)
889
    }
890

            
891
    /// Every colour the live restyle wrote, as `(node index, "bg" | "text", colour)`
892
    /// in emission order. Panics on any property other than the two the handler is
893
    /// documented to write.
894
    fn restyle_writes(changes: &[CallbackChange]) -> Vec<(usize, &'static str, ColorU)> {
895
        let mut out = Vec::new();
896
        for change in changes {
897
            let CallbackChange::ChangeNodeCssProperties { node_id, properties, .. } = change else {
898
                panic!("the restyle must only emit ChangeNodeCssProperties, got {change:?}");
899
            };
900
            for p in properties.as_ref() {
901
                match p {
902
                    CssProperty::BackgroundContent(v) => {
903
                        let layers =
904
                            v.get_property().expect("restyle must write an exact background");
905
                        assert_eq!(layers.as_ref().len(), 1, "a segment fill is a single layer");
906
                        match &layers.as_ref()[0] {
907
                            StyleBackgroundContent::Color(c) => {
908
                                out.push((node_id.index(), "bg", *c));
909
                            }
910
                            other => panic!("segment background is not a flat colour: {other:?}"),
911
                        }
912
                    }
913
                    CssProperty::TextColor(v) => {
914
                        let c = v.get_property().expect("restyle must write an exact text colour");
915
                        out.push((node_id.index(), "text", c.inner));
916
                    }
917
                    other => panic!("unexpected restyle property: {other:?}"),
918
                }
919
            }
920
        }
921
        out
922
    }
923

            
924
    // ------------------------------------------------------------------
925
    // build_segment_style
926
    // ------------------------------------------------------------------
927

            
928
    #[test]
929
    fn build_segment_style_handles_all_eight_flag_combinations() {
930
        // 24 shared declarations, +5 for the first segment (left border triple +
931
        // the two left radii), +2 for the last (the two right radii).
932
        for (selected, first, last) in ALL_FLAGS {
933
            let style = build_segment_style(selected, first, last);
934
            let expected = 24 + if first { 5 } else { 0 } + if last { 2 } else { 0 };
935
            assert_eq!(
936
                style.as_ref().len(),
937
                expected,
938
                "({selected}, {first}, {last}): unexpected declaration count"
939
            );
940
        }
941
    }
942

            
943
    #[test]
944
    fn build_segment_style_colours_depend_only_on_selected() {
945
        // Position must not leak into the palette: a "first" segment and a
946
        // "middle" segment with the same selection must paint identically.
947
        for selected in [false, true] {
948
            let reference = build_segment_style(selected, false, false);
949
            let bg = background_color(&reference).expect("a segment must declare a background");
950
            let fg = text_color(&reference).expect("a segment must declare a text colour");
951

            
952
            for (first, last) in [(false, false), (false, true), (true, false), (true, true)] {
953
                let style = build_segment_style(selected, first, last);
954
                assert_eq!(background_color(&style), Some(bg), "selected={selected}: background moved with position");
955
                assert_eq!(text_color(&style), Some(fg), "selected={selected}: text colour moved with position");
956
            }
957
        }
958

            
959
        assert_eq!(background_color(&build_segment_style(true, false, false)), Some(SEG_SELECTED_BG_COLOR));
960
        assert_eq!(text_color(&build_segment_style(true, false, false)), Some(SEG_SELECTED_TEXT));
961
        assert_eq!(background_color(&build_segment_style(false, false, false)), Some(SEG_UNSELECTED_BG_COLOR));
962
        assert_eq!(text_color(&build_segment_style(false, false, false)), Some(SEG_UNSELECTED_TEXT));
963
    }
964

            
965
    #[test]
966
    fn build_segment_style_adds_the_left_border_only_to_the_first_segment() {
967
        // Every segment paints its own right border, so a non-first segment that
968
        // also painted a left one would render a 2px seam between neighbours.
969
        for (selected, first, last) in ALL_FLAGS {
970
            let style = build_segment_style(selected, first, last);
971
            let want = usize::from(first);
972

            
973
            assert_eq!(
974
                declares(&style, |p| matches!(p, CssProperty::BorderLeftWidth(_))),
975
                want,
976
                "({selected}, {first}, {last}): left border width"
977
            );
978
            assert_eq!(
979
                declares(&style, |p| matches!(p, CssProperty::BorderLeftStyle(_))),
980
                want,
981
                "({selected}, {first}, {last}): left border style"
982
            );
983
            assert_eq!(
984
                declares(&style, |p| matches!(p, CssProperty::BorderLeftColor(_))),
985
                want,
986
                "({selected}, {first}, {last}): left border colour"
987
            );
988
        }
989
    }
990

            
991
    #[test]
992
    fn build_segment_style_rounds_only_the_outer_corners() {
993
        for (selected, first, last) in ALL_FLAGS {
994
            let style = build_segment_style(selected, first, last);
995
            let (tl, tr, bl, br) = radii_px(&style);
996
            let r = SEG_RADIUS as f32;
997

            
998
            assert_eq!(tl, first.then_some(r), "({selected}, {first}, {last}): top-left radius");
999
            assert_eq!(bl, first.then_some(r), "({selected}, {first}, {last}): bottom-left radius");
            assert_eq!(tr, last.then_some(r), "({selected}, {first}, {last}): top-right radius");
            assert_eq!(br, last.then_some(r), "({selected}, {first}, {last}): bottom-right radius");
        }
        // A lone segment is a fully rounded pill; an interior segment is square.
        let solo = build_segment_style(true, true, true);
        let r = SEG_RADIUS as f32;
        assert_eq!(radii_px(&solo), (Some(r), Some(r), Some(r), Some(r)));
        let middle = build_segment_style(true, false, false);
        assert_eq!(radii_px(&middle), (None, None, None, None));
    }
    #[test]
    fn build_segment_style_always_paints_the_shared_separator_edges() {
        // Top/bottom/right must be declared unconditionally — dropping the right
        // border on the last segment would leave the group open on one side.
        for (selected, first, last) in ALL_FLAGS {
            let style = build_segment_style(selected, first, last);
            let (top, bottom, left, right) = border_widths_px(&style);
            assert_eq!(top, Some(1.0), "({selected}, {first}, {last}): top border");
            assert_eq!(bottom, Some(1.0), "({selected}, {first}, {last}): bottom border");
            assert_eq!(right, Some(1.0), "({selected}, {first}, {last}): right border");
            assert_eq!(left, first.then_some(1.0), "({selected}, {first}, {last}): left border");
            // A width without a matching style/colour renders as no border at all.
            for count in [
                declares(&style, |p| matches!(p, CssProperty::BorderTopStyle(_))),
                declares(&style, |p| matches!(p, CssProperty::BorderBottomStyle(_))),
                declares(&style, |p| matches!(p, CssProperty::BorderRightStyle(_))),
                declares(&style, |p| matches!(p, CssProperty::BorderTopColor(_))),
                declares(&style, |p| matches!(p, CssProperty::BorderBottomColor(_))),
                declares(&style, |p| matches!(p, CssProperty::BorderRightColor(_))),
            ] {
                assert_eq!(count, 1, "({selected}, {first}, {last}): a shared edge lost its style/colour");
            }
        }
    }
    #[test]
    fn build_segment_style_border_colours_are_the_single_neutral_grey() {
        // A width without a matching colour (or a stray second grey) shows up as
        // an inconsistent seam between neighbouring segments.
        for (selected, first, last) in ALL_FLAGS {
            let style = build_segment_style(selected, first, last);
            for p in style.as_ref() {
                let found = match &p.property {
                    CssProperty::BorderTopColor(c) => c.get_property().map(|c| c.inner),
                    CssProperty::BorderBottomColor(c) => c.get_property().map(|c| c.inner),
                    CssProperty::BorderLeftColor(c) => c.get_property().map(|c| c.inner),
                    CssProperty::BorderRightColor(c) => c.get_property().map(|c| c.inner),
                    _ => None,
                };
                if let Some(c) = found {
                    assert_eq!(
                        c, SEG_BORDER_COLOR,
                        "({selected}, {first}, {last}): border colour {c:?} is not the shared grey"
                    );
                }
            }
        }
    }
    #[test]
    fn build_segment_style_geometry_is_absolute_and_symmetric() {
        for (selected, first, last) in ALL_FLAGS {
            let style = build_segment_style(selected, first, last);
            // padding: 6px 12px — `px()` asserts the metric on every value it reads.
            assert_eq!(
                padding_px(&style),
                (Some(6.0), Some(6.0), Some(12.0), Some(12.0)),
                "({selected}, {first}, {last}): padding is not 6px 12px"
            );
            assert_eq!(font_size_px(&style), Some(13.0), "({selected}, {first}, {last}): font size");
        }
    }
    #[test]
    fn build_segment_style_declares_every_property_unconditionally() {
        // `simple()` means an empty `apply_if`. A stray condition here would make
        // the segment silently unstyled until some selector state happened to match.
        for (selected, first, last) in ALL_FLAGS {
            let style = build_segment_style(selected, first, last);
            for p in style.as_ref() {
                assert!(
                    p.apply_if.as_ref().is_empty(),
                    "({selected}, {first}, {last}): {:?} is conditional",
                    p.property
                );
            }
        }
    }
    #[test]
    fn build_segment_style_never_declares_the_same_property_twice() {
        // Duplicates are silently last-wins, so a doubled declaration hides a
        // genuine value conflict instead of failing loudly.
        for (selected, first, last) in ALL_FLAGS {
            let style = build_segment_style(selected, first, last);
            let mut seen = HashSet::new();
            for kind in property_kinds(&style) {
                assert!(
                    seen.insert(kind),
                    "({selected}, {first}, {last}): duplicate property kind in the style vec"
                );
            }
            assert_eq!(seen.len(), style.as_ref().len());
        }
    }
    #[test]
    fn build_segment_style_is_pure() {
        // Called once per segment on every `dom()`; a hidden `static mut` cache or
        // an accumulating vec would show up as drift between two identical calls.
        for (selected, first, last) in ALL_FLAGS {
            let a = build_segment_style(selected, first, last);
            let b = build_segment_style(selected, first, last);
            assert_eq!(properties(&a), properties(&b), "({selected}, {first}, {last}): not pure");
        }
    }
    #[test]
    fn build_segment_style_keeps_the_label_readable_and_opaque() {
        for selected in [false, true] {
            let style = build_segment_style(selected, false, false);
            let bg = background_color(&style).expect("background");
            let fg = text_color(&style).expect("text colour");
            assert_eq!(bg.a, 255, "selected={selected}: a translucent fill lets the page bleed through");
            assert_eq!(fg.a, 255, "selected={selected}: translucent label text");
            assert_ne!(bg, fg, "selected={selected}: an invisible label is not a segment");
            assert!(
                (luma(bg) - luma(fg)).abs() >= 60.0,
                "selected={selected}: brightness gap {:.1} is too low to read",
                (luma(bg) - luma(fg)).abs()
            );
        }
        // The two states must be visually distinguishable — that is the entire
        // point of a segmented control.
        let sel = build_segment_style(true, false, false);
        let unsel = build_segment_style(false, false, false);
        assert_ne!(background_color(&sel), background_color(&unsel));
        assert_ne!(text_color(&sel), text_color(&unsel));
    }
    #[test]
    fn build_segment_style_declares_the_interaction_affordances() {
        for (selected, first, last) in ALL_FLAGS {
            let style = build_segment_style(selected, first, last);
            let ctx = format!("({selected}, {first}, {last})");
            assert!(
                style.as_ref().iter().any(|p| matches!(
                    &p.property,
                    CssProperty::Cursor(c) if c.get_property() == Some(&StyleCursor::Pointer)
                )),
                "{ctx}: a clickable segment must show the pointer cursor"
            );
            assert!(
                style.as_ref().iter().any(|p| matches!(
                    &p.property,
                    CssProperty::UserSelect(u) if u.get_property() == Some(&StyleUserSelect::None)
                )),
                "{ctx}: click-dragging a segment must not select its caption"
            );
            assert!(
                style.as_ref().iter().any(|p| matches!(
                    &p.property,
                    CssProperty::TextAlign(t) if t.get_property() == Some(&StyleTextAlign::Center)
                )),
                "{ctx}: captions are centred"
            );
            assert!(
                style.as_ref().iter().any(|p| matches!(
                    &p.property,
                    CssProperty::FlexGrow(f) if f.get_property().map(|f| f.inner.get()) == Some(0.0)
                )),
                "{ctx}: segments hug their caption, they do not stretch"
            );
        }
    }
    // ------------------------------------------------------------------
    // Segmented::create
    // ------------------------------------------------------------------
    #[test]
    fn create_preserves_labels_verbatim() {
        for case in [
            vec![],
            vec!["only"],
            vec!["Day", "Week"],
            vec!["Day", "Week", "Month", "Year"],
            vec!["dup", "dup", "dup"],
        ] {
            let seg = Segmented::create(labels(&case));
            let got: Vec<&str> = seg.labels.as_ref().iter().map(AzString::as_str).collect();
            assert_eq!(got, case, "create must not reorder/drop/dedupe/rewrite labels");
        }
    }
    #[test]
    fn create_preserves_adversarial_labels_byte_for_byte() {
        for s in adversarial_strings() {
            let seg = Segmented::create(labels(&[s.as_str()]));
            let stored = seg.labels.as_ref()[0].as_str();
            assert_eq!(stored, s.as_str(), "the caption changed on its way into the widget");
            assert_eq!(
                seg.labels.as_ref()[0].as_ref().len(),
                s.len(),
                "byte length changed (NUL truncation?)"
            );
        }
    }
    #[test]
    fn create_selects_the_first_segment_and_installs_no_callback() {
        for n in [0usize, 1, 2, 7] {
            let seg = Segmented::create(n_labels(n));
            assert_eq!(
                seg.segmented_state.inner.selected_index, 0,
                "n={n}: a fresh control starts on segment 0"
            );
            assert!(
                seg.segmented_state.on_change.as_ref().is_none(),
                "n={n}: create must not wire a callback"
            );
        }
    }
    #[test]
    fn create_installs_the_shared_container_style() {
        let seg = Segmented::create(labels(&["a", "b"]));
        assert_eq!(
            seg.container_style.as_ref(),
            SEGMENTED_CONTAINER_STYLE,
            "create must install the shared container style"
        );
        // Decode the semantics too, so a silent edit of the const is caught here
        // rather than only in a screenshot: a horizontal, content-hugging row.
        let style = &seg.container_style;
        assert_eq!(
            declares(style, |p| matches!(
                p, CssProperty::Display(d) if d.get_property() == Some(&LayoutDisplay::Flex))),
            1,
            "the row container must be a flex box"
        );
        assert_eq!(
            declares(style, |p| matches!(
                p, CssProperty::FlexDirection(d) if d.get_property() == Some(&LayoutFlexDirection::Row))),
            1,
            "segments are joined horizontally"
        );
        assert_eq!(
            declares(style, |p| matches!(
                p, CssProperty::AlignItems(a) if a.get_property() == Some(&LayoutAlignItems::Center))),
            1
        );
        assert_eq!(
            declares(style, |p| matches!(
                p, CssProperty::AlignSelf(a) if a.get_property() == Some(&LayoutAlignSelf::Start))),
            1
        );
        assert_eq!(
            declares(style, |p| matches!(
                p, CssProperty::FlexGrow(f) if f.get_property().map(|f| f.inner.get()) == Some(0.0))),
            1,
            "the group hugs its segments instead of filling the parent"
        );
        for p in seg.container_style.as_ref() {
            assert!(p.apply_if.as_ref().is_empty(), "{:?} is conditional", p.property);
        }
    }
    #[test]
    fn create_with_no_labels_equals_default() {
        let empty = Segmented::create(StringVec::from_const_slice(&[]));
        assert_eq!(empty, Segmented::default(), "Default must be the empty control");
        assert_eq!(empty.labels.as_ref().len(), 0);
        assert!(Segmented::default().segmented_state.on_change.as_ref().is_none());
    }
    #[test]
    fn create_scales_to_a_very_long_label_list() {
        let n = 4096;
        let seg = Segmented::create(n_labels(n));
        assert_eq!(seg.labels.as_ref().len(), n);
        assert_eq!(seg.labels.as_ref()[n - 1].as_str(), format!("s{}", n - 1));
        assert_eq!(seg.segmented_state.inner.selected_index, 0);
    }
    // ------------------------------------------------------------------
    // Segmented::set_selected_index  /  with_selected_index
    // ------------------------------------------------------------------
    #[test]
    fn set_selected_index_stores_every_boundary_value_verbatim() {
        // The setter is a plain field write: no clamping, no wrapping, no panic —
        // not even at `usize::MAX` or at a `-1` that arrived through FFI.
        for i in boundary_indices() {
            let mut seg = Segmented::create(labels(&["a", "b", "c"]));
            seg.set_selected_index(i);
            assert_eq!(seg.segmented_state.inner.selected_index, i, "index {i} was not stored as-is");
        }
    }
    #[test]
    fn set_selected_index_does_not_clamp_to_the_label_count() {
        // Documenting the actual contract: an out-of-range index is *accepted*
        // and simply selects nothing when rendered (see the `dom_` tests below).
        let mut seg = Segmented::create(labels(&["a", "b"]));
        for i in [2usize, 3, 1_000, usize::MAX] {
            seg.set_selected_index(i);
            assert_eq!(seg.segmented_state.inner.selected_index, i);
            assert_eq!(seg.labels.as_ref().len(), 2, "the setter must not touch the labels");
        }
    }
    #[test]
    fn set_selected_index_is_idempotent_and_last_write_wins() {
        let mut seg = Segmented::create(labels(&["a", "b", "c"]));
        for i in [1usize, 1, 1] {
            seg.set_selected_index(i);
        }
        assert_eq!(seg.segmented_state.inner.selected_index, 1);
        for i in [0usize, usize::MAX, 2, 0] {
            seg.set_selected_index(i);
        }
        assert_eq!(seg.segmented_state.inner.selected_index, 0, "the last write must win");
    }
    #[test]
    fn set_selected_index_leaves_every_other_field_alone() {
        let mut seg = Segmented::create(labels(&["a", "b"]))
            .with_on_change(RefAny::new(7u8), change_cb(change_do_nothing));
        let before = seg.clone();
        seg.set_selected_index(usize::MAX);
        assert_eq!(seg.labels, before.labels, "labels changed");
        assert_eq!(seg.container_style, before.container_style, "container style changed");
        assert_eq!(
            seg.segmented_state.on_change, before.segmented_state.on_change,
            "the callback was disturbed"
        );
    }
    #[test]
    fn with_selected_index_round_trips_through_the_setter() {
        for i in boundary_indices() {
            let via_builder = Segmented::create(labels(&["a", "b"])).with_selected_index(i);
            let mut via_setter = Segmented::create(labels(&["a", "b"]));
            via_setter.set_selected_index(i);
            assert_eq!(via_builder, via_setter, "index {i}: builder and setter diverge");
            assert_eq!(via_builder.segmented_state.inner.selected_index, i);
        }
    }
    #[test]
    fn with_selected_index_preserves_the_rest_of_the_widget() {
        let base = Segmented::create(labels(&["a", "b", "c"]));
        let built = base.clone().with_selected_index(2);
        assert_eq!(built.labels, base.labels);
        assert_eq!(built.container_style, base.container_style);
        assert_eq!(built.labels.as_ref().len(), 3, "len/contents must stay consistent");
        assert!(built.segmented_state.on_change.as_ref().is_none());
    }
    #[test]
    fn with_selected_index_chains_with_last_wins() {
        let seg = Segmented::create(labels(&["a", "b", "c"]))
            .with_selected_index(usize::MAX)
            .with_selected_index(0)
            .with_selected_index(2);
        assert_eq!(seg.segmented_state.inner.selected_index, 2);
    }
    // ------------------------------------------------------------------
    // Segmented::swap_with_default
    // ------------------------------------------------------------------
    #[test]
    fn swap_with_default_returns_the_original_and_leaves_a_default_behind() {
        let mut seg = Segmented::create(labels(&["Day", "Week", "Month"])).with_selected_index(2);
        let expected = seg.clone();
        let taken = seg.swap_with_default();
        assert_eq!(taken, expected, "the caller must get the original widget back");
        assert_eq!(seg, Segmented::default(), "a default must be left in its place");
        assert_eq!(seg.labels.as_ref().len(), 0);
        assert_eq!(seg.segmented_state.inner.selected_index, 0);
    }
    #[test]
    fn swap_with_default_moves_the_callback_out_with_the_widget() {
        let mut seg = Segmented::create(labels(&["a", "b"]))
            .with_on_change(RefAny::new(1u8), change_cb(record_index));
        let taken = seg.swap_with_default();
        assert!(
            taken.segmented_state.on_change.as_ref().is_some(),
            "the callback must travel with the taken widget"
        );
        assert!(
            seg.segmented_state.on_change.as_ref().is_none(),
            "the leftover default must not keep a handle on the callback"
        );
    }
    #[test]
    fn swap_with_default_on_a_default_is_a_no_op() {
        let mut seg = Segmented::default();
        let taken = seg.swap_with_default();
        assert_eq!(taken, Segmented::default());
        assert_eq!(seg, Segmented::default());
    }
    #[test]
    fn swap_with_default_twice_yields_a_default_the_second_time() {
        let mut seg = Segmented::create(labels(&["a", "b"])).with_selected_index(1);
        let first = seg.swap_with_default();
        let second = seg.swap_with_default();
        assert_eq!(first.labels.as_ref().len(), 2);
        assert_eq!(first.segmented_state.inner.selected_index, 1);
        assert_eq!(second, Segmented::default(), "the second take is the default we left behind");
        assert_eq!(seg, Segmented::default());
    }
    #[test]
    fn swap_with_default_does_not_truncate_a_large_label_list() {
        let n = 1024;
        let mut seg = Segmented::create(n_labels(n)).with_selected_index(n - 1);
        let taken = seg.swap_with_default();
        assert_eq!(taken.labels.as_ref().len(), n);
        assert_eq!(taken.labels.as_ref()[n - 1].as_str(), format!("s{}", n - 1));
        assert_eq!(taken.segmented_state.inner.selected_index, n - 1);
    }
    // ------------------------------------------------------------------
    // Segmented::set_on_change  /  with_on_change
    // ------------------------------------------------------------------
    #[test]
    fn set_on_change_installs_the_callback_and_its_payload() {
        let mut seg = Segmented::create(labels(&["a", "b"]));
        let mut payload = RefAny::new(IndexLog { seen: Vec::new() });
        seg.set_on_change(payload.clone(), change_cb(record_index));
        let installed =
            seg.segmented_state.on_change.as_ref().expect("set_on_change must install a callback");
        assert_eq!(installed.callback.cb as usize, record_index as usize, "wrong function installed");
        assert!(
            matches!(installed.callback.ctx, OptionRefAny::None),
            "a native Rust callback carries no FFI context"
        );
        // The stored `RefAny` must be a *share* of the caller's, not a copy:
        // writing through the widget's handle must be visible to the caller.
        let mut stored = installed.refany.clone();
        {
            let mut log = stored.downcast_mut::<IndexLog>().expect("payload type must survive");
            log.seen.push(42);
        }
        assert_eq!(log_indices(&mut payload), vec![42], "the payload was copied, not shared");
    }
    #[test]
    fn set_on_change_overwrites_a_previously_installed_callback() {
        let mut seg = Segmented::create(labels(&["a", "b"]));
        seg.set_on_change(RefAny::new(1u8), change_cb(record_index));
        seg.set_on_change(RefAny::new(2u8), change_cb(change_refresh_all));
        let installed = seg.segmented_state.on_change.as_ref().expect("callback");
        assert_eq!(installed.callback.cb as usize, change_refresh_all as usize, "the last setter must win");
        assert_ne!(installed.callback.cb as usize, record_index as usize);
    }
    #[test]
    fn set_on_change_does_not_disturb_labels_or_selection() {
        let mut seg = Segmented::create(labels(&["a", "b", "c"])).with_selected_index(2);
        seg.set_on_change(RefAny::new(0u8), change_cb(change_do_nothing));
        assert_eq!(seg.labels.as_ref().len(), 3);
        assert_eq!(seg.segmented_state.inner.selected_index, 2, "installing a callback moved the selection");
    }
    #[test]
    fn with_on_change_matches_the_setter_exactly() {
        let payload = RefAny::new(9u8);
        let via_builder = Segmented::create(labels(&["a", "b"]))
            .with_on_change(payload.clone(), change_cb(change_do_nothing));
        let mut via_setter = Segmented::create(labels(&["a", "b"]));
        via_setter.set_on_change(payload, change_cb(change_do_nothing));
        assert_eq!(via_builder, via_setter, "builder and setter must produce the same widget");
    }
    #[test]
    fn with_on_change_holds_its_invariants_after_construction() {
        let seg = Segmented::create(n_labels(5))
            .with_selected_index(3)
            .with_on_change(RefAny::new(0u8), change_cb(change_refresh_all));
        assert_eq!(seg.labels.as_ref().len(), 5, "label count must survive the builder chain");
        assert_eq!(seg.segmented_state.inner.selected_index, 3, "the selection must survive");
        assert_eq!(
            seg.container_style.as_ref(),
            SEGMENTED_CONTAINER_STYLE,
            "the container style must survive"
        );
        let installed = seg.segmented_state.on_change.as_ref().expect("callback");
        assert_eq!(installed.callback.cb as usize, change_refresh_all as usize);
    }
    #[test]
    fn with_on_change_chains_with_last_wins() {
        let seg = Segmented::create(labels(&["a"]))
            .with_on_change(RefAny::new(0u8), change_cb(record_index))
            .with_on_change(RefAny::new(0u8), change_cb(change_do_nothing));
        let installed = seg.segmented_state.on_change.as_ref().expect("callback");
        assert_eq!(installed.callback.cb as usize, change_do_nothing as usize);
    }
    // ------------------------------------------------------------------
    // Segmented::dom
    // ------------------------------------------------------------------
    #[test]
    fn dom_emits_one_text_child_per_label_in_order() {
        let case = ["Day", "Week", "Month", "Year"];
        let dom = Segmented::create(labels(&case)).dom();
        assert!(matches!(dom.root.get_node_type(), NodeType::Div), "the group is a div");
        assert!(dom.root.has_class("__azul-native-segmented"));
        assert!(dom.root.get_callbacks().as_ref().is_empty(), "the container itself is not clickable");
        let children = dom.children.as_ref();
        assert_eq!(children.len(), case.len());
        for (i, child) in children.iter().enumerate() {
            assert_eq!(text_of(child), Some(case[i]), "segment {i} shows the wrong caption");
            assert!(child.root.has_class("__azul-native-segmented-item"), "segment {i} lost its class");
        }
    }
    #[test]
    fn dom_of_an_empty_control_is_a_childless_container() {
        // `count == 0` must not underflow `i + 1 == count` or emit a stray child.
        let dom = Segmented::create(StringVec::from_const_slice(&[])).dom();
        assert_eq!(dom.children.as_ref().len(), 0);
        assert_eq!(dom.estimated_total_children, 0);
        assert!(dom.root.has_class("__azul-native-segmented"));
        let styled = StyledDom::create_from_dom(dom);
        assert_eq!(styled.node_hierarchy.as_ref().len(), 1, "just the container");
    }
    #[test]
    fn dom_styles_each_segment_by_its_position_and_selection() {
        for n in [1usize, 2, 3, 5] {
            for selected in 0..n {
                let dom = Segmented::create(n_labels(n)).with_selected_index(selected).dom();
                let children = dom.children.as_ref();
                assert_eq!(children.len(), n);
                for (i, child) in children.iter().enumerate() {
                    let expected =
                        properties(&build_segment_style(i == selected, i == 0, i + 1 == n));
                    assert_eq!(
                        inline_properties(child),
                        expected,
                        "n={n} selected={selected}: segment {i} carries the wrong style"
                    );
                }
            }
        }
    }
    #[test]
    fn dom_marks_exactly_one_segment_as_selected() {
        for n in [1usize, 2, 4] {
            for selected in 0..n {
                let dom = Segmented::create(n_labels(n)).with_selected_index(selected).dom();
                let marked: Vec<usize> = dom
                    .children
                    .as_ref()
                    .iter()
                    .enumerate()
                    .filter(|(_, c)| {
                        inline_properties(c).iter().any(|p| matches!(
                            p, CssProperty::TextColor(t)
                                if t.get_property().map(|t| t.inner) == Some(SEG_SELECTED_TEXT)))
                    })
                    .map(|(i, _)| i)
                    .collect();
                assert_eq!(marked, vec![selected], "n={n}: mutual exclusivity broken");
            }
        }
    }
    #[test]
    fn dom_with_an_out_of_range_selection_marks_nothing_and_does_not_panic() {
        // `set_selected_index` accepts any `usize`; rendering must degrade to
        // "nothing selected" rather than panicking or wrapping onto a real segment.
        let n = 3;
        for selected in [n, n + 1, 1_000, usize::MAX, usize::MAX - 1] {
            let dom = Segmented::create(n_labels(n)).with_selected_index(selected).dom();
            assert_eq!(dom.children.as_ref().len(), n, "selected={selected}: child count changed");
            for (i, child) in dom.children.as_ref().iter().enumerate() {
                let expected = properties(&build_segment_style(false, i == 0, i + 1 == n));
                assert_eq!(
                    inline_properties(child),
                    expected,
                    "selected={selected}: segment {i} must render unselected"
                );
            }
        }
    }
    #[test]
    fn dom_rounds_only_the_two_outer_segments() {
        let n = 4;
        let dom = Segmented::create(n_labels(n)).dom();
        let r = SEG_RADIUS as f32;
        let radii_of = |child: &Dom| -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
            let props = inline_properties(child);
            let find = |f: &dyn Fn(&CssProperty) -> Option<f32>| props.iter().find_map(f);
            (
                find(&|p| match p {
                    CssProperty::BorderTopLeftRadius(x) => x.get_property().map(|x| px(&x.inner)),
                    _ => None,
                }),
                find(&|p| match p {
                    CssProperty::BorderTopRightRadius(x) => x.get_property().map(|x| px(&x.inner)),
                    _ => None,
                }),
                find(&|p| match p {
                    CssProperty::BorderBottomLeftRadius(x) => x.get_property().map(|x| px(&x.inner)),
                    _ => None,
                }),
                find(&|p| match p {
                    CssProperty::BorderBottomRightRadius(x) => {
                        x.get_property().map(|x| px(&x.inner))
                    }
                    _ => None,
                }),
            )
        };
        let children = dom.children.as_ref();
        assert_eq!(radii_of(&children[0]), (Some(r), None, Some(r), None), "first: left corners only");
        assert_eq!(radii_of(&children[1]), (None, None, None, None), "interior segments are square");
        assert_eq!(radii_of(&children[2]), (None, None, None, None), "interior segments are square");
        assert_eq!(
            radii_of(&children[3]),
            (None, Some(r), None, Some(r)),
            "last: right corners only"
        );
    }
    #[test]
    fn dom_of_a_single_segment_is_rounded_on_both_ends() {
        let dom = Segmented::create(labels(&["only"])).dom();
        let children = dom.children.as_ref();
        assert_eq!(children.len(), 1);
        let expected = properties(&build_segment_style(true, true, true));
        assert_eq!(
            inline_properties(&children[0]),
            expected,
            "a lone segment is simultaneously first and last"
        );
    }
    #[test]
    fn dom_makes_every_segment_clickable_and_keyboard_reachable() {
        let n = 3;
        let dom = Segmented::create(n_labels(n)).dom();
        for (i, child) in dom.children.as_ref().iter().enumerate() {
            let cbs = child.root.get_callbacks();
            assert_eq!(cbs.as_ref().len(), 1, "segment {i}: exactly one handler");
            assert_eq!(cbs.as_ref()[0].event, EventFilter::Hover(HoverEventFilter::MouseUp));
            assert_eq!(cbs.as_ref()[0].callback.cb, on_segment_click as usize);
            assert!(matches!(cbs.as_ref()[0].callback.ctx, OptionRefAny::None));
            assert_eq!(
                child.root.get_tab_index(),
                Some(TabIndex::Auto),
                "segment {i} must be tab-reachable"
            );
        }
    }
    #[test]
    fn dom_shares_one_state_refany_across_every_segment() {
        // The handler resolves the clicked index from the DOM, so all segments
        // *must* observe the same state — a per-segment copy would let two
        // segments believe they are both selected.
        let dom = Segmented::create(n_labels(4)).dom();
        let mut first = segment_state(&dom, 0);
        {
            let mut w = first
                .downcast_mut::<SegmentedStateWrapper>()
                .expect("segment state must be a SegmentedStateWrapper");
            w.inner.selected_index = 3;
        }
        for i in 1..4 {
            let mut other = segment_state(&dom, i);
            assert_eq!(
                selected_index_of(&mut other),
                3,
                "segment {i} does not share segment 0's state"
            );
        }
    }
    #[test]
    fn dom_carries_the_installed_callback_into_the_shared_state() {
        let dom = Segmented::create(labels(&["a", "b"]))
            .with_on_change(RefAny::new(0u8), change_cb(change_refresh_all))
            .dom();
        let mut state = segment_state(&dom, 0);
        let wrapper =
            state.downcast_ref::<SegmentedStateWrapper>().expect("SegmentedStateWrapper");
        let installed = wrapper.on_change.as_ref().expect("the user callback must reach the DOM");
        assert_eq!(installed.callback.cb as usize, change_refresh_all as usize);
    }
    #[test]
    fn dom_preserves_adversarial_labels_verbatim() {
        for s in adversarial_strings() {
            let dom = Segmented::create(labels(&[s.as_str(), "other"])).dom();
            let children = dom.children.as_ref();
            assert_eq!(children.len(), 2);
            match children[0].children.as_ref() {
                [only] => match only.root.get_node_type() {
                    NodeType::Text(t) => {
                        assert_eq!(t.as_ref().as_str(), s.as_str(), "the caption changed inside dom()");
                        assert_eq!(t.as_ref().len(), s.len(), "byte length changed (NUL truncation?)");
                    }
                    other => panic!("expected a text node, got {other:?}"),
                },
                other => panic!("expected `p > text`, got {} children", other.len()),
            }
        }
    }
    #[test]
    fn dom_keeps_estimated_total_children_in_sync() {
        // `estimated_total_children` is a cached count; if it under-counts,
        // `convert_dom_into_compact_dom` under-allocates and panics.
        for n in [0usize, 1, 2, 3, 5, 64, 257] {
            let dom = Segmented::create(n_labels(n)).dom();
            assert_eq!(dom.children.as_ref().len(), n, "child count for n={n}");
            assert_eq!(
                dom.estimated_total_children,
                recursive_descendants(&dom),
                "cached descendant count desynced for n={n}"
            );
            assert_eq!(
                dom.estimated_total_children,
                2 * n,
                "for n={n} (each segment is a <p> wrapping one text node)"
            );
        }
    }
    #[test]
    fn dom_of_many_segments_flattens_without_panicking() {
        let n = 512;
        let styled = StyledDom::create_from_dom(Segmented::create(n_labels(n)).dom());
        assert_eq!(
            styled.node_hierarchy.as_ref().len(),
            2 * n + 1,
            "root + n segments, each a <p> wrapping one text node"
        );
    }
    #[test]
    fn dom_via_from_matches_dom_exactly() {
        let build = || Segmented::create(n_labels(3)).with_selected_index(1);
        let via_into: Dom = build().into();
        let via_dom = build().dom();
        assert_eq!(via_into.children.as_ref().len(), via_dom.children.as_ref().len());
        assert_eq!(
            via_into.estimated_total_children,
            via_dom.estimated_total_children
        );
        for i in 0..via_dom.children.as_ref().len() {
            assert_eq!(
                inline_properties(&via_into.children.as_ref()[i]),
                inline_properties(&via_dom.children.as_ref()[i]),
                "`From` diverges from `dom()` at segment {i}"
            );
            assert_eq!(
                text_of(&via_into.children.as_ref()[i]),
                text_of(&via_dom.children.as_ref()[i])
            );
        }
    }
    #[test]
    fn dom_with_duplicate_labels_still_produces_distinct_positional_segments() {
        // Selection is positional, not by caption: three identical captions must
        // still give exactly one selected segment, at the requested position.
        let dom = Segmented::create(labels(&["same", "same", "same"])).with_selected_index(1).dom();
        let children = dom.children.as_ref();
        for (i, child) in children.iter().enumerate() {
            assert_eq!(text_of(child), Some("same"));
            let expected = properties(&build_segment_style(i == 1, i == 0, i == 2));
            assert_eq!(inline_properties(child), expected, "segment {i}");
        }
    }
    // ------------------------------------------------------------------
    // on_segment_click
    // ------------------------------------------------------------------
    #[test]
    fn click_selects_the_segment_at_the_clicked_position() {
        let n = 4;
        let (styled, state) = flatten(Segmented::create(n_labels(n)));
        assert_eq!(
            styled.node_hierarchy.as_ref().len(),
            2 * n + 1,
            "fixture: root + n segments, each a <p> wrapping one text node"
        );
        for i in 0..n {
            let mut state = state.clone();
            let (update, changes) = run_click(Some(styled.clone()), seg_node(i), state.clone());
            assert_eq!(
                update,
                Update::DoNothing,
                "with no on_change installed the handler reports nothing to redraw"
            );
            assert_eq!(
                selected_index_of(&mut state),
                i,
                "node {} must select segment {i}",
                seg_node(i)
            );
            assert_eq!(restyle_writes(&changes).len(), 2 * n, "every segment must be restyled");
        }
    }
    #[test]
    fn click_restyle_agrees_with_a_freshly_built_style() {
        // The live restyle and a full rebuild must not drift apart, or a click
        // followed by a `RefreshDom` would visibly change the widget twice.
        let n = 4;
        let (styled, state) = flatten(Segmented::create(n_labels(n)));
        for clicked in 0..n {
            let (_, changes) = run_click(Some(styled.clone()), seg_node(clicked), state.clone());
            let writes = restyle_writes(&changes);
            assert_eq!(writes.len(), 2 * n);
            for i in 0..n {
                let fresh = build_segment_style(i == clicked, i == 0, i + 1 == n);
                assert_eq!(
                    writes[2 * i],
                    (seg_node(i), "bg", background_color(&fresh).expect("background")),
                    "clicked={clicked}: segment {i} background"
                );
                assert_eq!(
                    writes[2 * i + 1],
                    (seg_node(i), "text", text_color(&fresh).expect("text colour")),
                    "clicked={clicked}: segment {i} text colour"
                );
            }
        }
    }
    #[test]
    fn click_invokes_the_user_callback_with_the_updated_state() {
        let mut log = RefAny::new(IndexLog { seen: Vec::new() });
        let seg = Segmented::create(n_labels(4))
            .with_on_change(log.clone(), change_cb(record_index));
        let (styled, state) = flatten(seg);
        let (update, changes) = run_click(Some(styled.clone()), seg_node(2), state.clone());
        assert_eq!(update, Update::RefreshDom, "the user's Update must propagate");
        assert_eq!(log_indices(&mut log), vec![2], "the callback sees the *new* index");
        assert_eq!(restyle_writes(&changes).len(), 8, "the restyle must still run");
        // A second click updates the shared state again — the index is not sticky.
        let (_, _) = run_click(Some(styled), seg_node(0), state.clone());
        assert_eq!(log_indices(&mut log), vec![2, 0]);
        let mut state = state;
        assert_eq!(selected_index_of(&mut state), 0, "the state holds the *last* clicked index");
    }
    #[test]
    fn click_propagates_every_update_variant_unchanged() {
        for (cb, expected) in [
            (change_cb(change_do_nothing), Update::DoNothing),
            (change_cb(change_refresh_all), Update::RefreshDomAllWindows),
        ] {
            let seg =
                Segmented::create(labels(&["a", "b"])).with_on_change(RefAny::new(0u8), cb);
            let (styled, state) = flatten(seg);
            let (update, changes) = run_click(Some(styled), seg_node(1), state);
            assert_eq!(update, expected);
            assert_eq!(
                restyle_writes(&changes).len(),
                4,
                "the restyle runs regardless of what the user returns"
            );
        }
    }
    #[test]
    fn click_restyles_even_without_a_user_callback() {
        let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
        let (update, changes) = run_click(Some(styled), seg_node(0), state);
        assert_eq!(update, Update::DoNothing);
        assert_eq!(
            restyle_writes(&changes),
            vec![
                (seg_node(0), "bg", SEG_SELECTED_BG_COLOR),
                (seg_node(0), "text", SEG_SELECTED_TEXT),
                (seg_node(1), "bg", SEG_UNSELECTED_BG_COLOR),
                (seg_node(1), "text", SEG_UNSELECTED_TEXT),
            ],
            "selection feedback must not depend on the user wiring a callback"
        );
    }
    #[test]
    fn click_on_a_single_segment_control_stays_at_zero() {
        let (styled, state) = flatten(Segmented::create(labels(&["only"])));
        let mut probe = state.clone();
        let (update, changes) = run_click(Some(styled), seg_node(0), state);
        assert_eq!(update, Update::DoNothing);
        assert_eq!(selected_index_of(&mut probe), 0);
        assert_eq!(
            restyle_writes(&changes),
            vec![
                (seg_node(0), "bg", SEG_SELECTED_BG_COLOR),
                (seg_node(0), "text", SEG_SELECTED_TEXT)
            ]
        );
    }
    #[test]
    fn click_on_the_root_node_does_nothing() {
        // The container has no parent -> the handler must bail, not index into nothing.
        let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
        let mut probe = state.clone();
        let (update, changes) = run_click(Some(styled), 0, state);
        assert_eq!(update, Update::DoNothing);
        assert!(changes.is_empty(), "nothing may be restyled when the click is not on a segment");
        assert_eq!(selected_index_of(&mut probe), 0, "state must be untouched");
    }
    #[test]
    fn click_on_an_out_of_range_node_does_nothing() {
        let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
        let mut probe = state.clone();
        let (update, changes) = run_click(Some(styled), 9999, state);
        assert_eq!(update, Update::DoNothing, "a hit node outside the tree must not panic");
        assert!(changes.is_empty());
        assert_eq!(selected_index_of(&mut probe), 0);
    }
    #[test]
    fn click_with_no_layout_result_does_nothing() {
        let dom = Segmented::create(labels(&["a", "b"])).dom();
        let state = segment_state(&dom, 0);
        let (update, changes) = run_click(None, 1, state);
        assert_eq!(update, Update::DoNothing, "an empty LayoutWindow must be handled, not unwrapped");
        assert!(changes.is_empty());
    }
    #[test]
    fn click_with_a_foreign_payload_does_nothing() {
        // Wrong type in the RefAny: the downcast fails, so the handler must bail
        // *before* restyling — otherwise the DOM would show a selection the state
        // never recorded.
        let (styled, _) = flatten(Segmented::create(labels(&["a", "b"])));
        let (update, changes) = run_click(Some(styled), 1, RefAny::new(0u32));
        assert_eq!(update, Update::DoNothing);
        assert!(changes.is_empty(), "a failed downcast must not leave a half-applied restyle");
    }
    #[test]
    fn click_with_the_state_already_borrowed_does_nothing() {
        let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
        // A live mutable borrow on a sibling clone: `downcast_mut` inside the
        // handler must fail (returning DoNothing) instead of aliasing `&mut`.
        let mut held = state.clone();
        let guard = held.downcast_mut::<SegmentedStateWrapper>().expect("first borrow succeeds");
        let (update, changes) = run_click(Some(styled), 1, state);
        assert_eq!(update, Update::DoNothing);
        assert!(changes.is_empty());
        drop(guard);
    }
    #[test]
    fn click_holds_the_state_borrow_across_the_user_callback() {
        // The handler invokes the user callback while its own `downcast_mut` on
        // the state is still live. A user callback that re-enters the *same*
        // state `RefAny` is therefore refused — it must get `None` back rather
        // than a second aliasing borrow (or a panic).
        //
        // NOTE: probe <-> state form a RefAny reference cycle, so this fixture
        // leaks. That is deliberate and harmless for a single test.
        let mut probe = RefAny::new(ReentrantProbe {
            state: RefAny::new(0u8),
            saw_index: Some(usize::MAX),
            calls: 0,
        });
        let state = RefAny::new(SegmentedStateWrapper {
            inner: SegmentedState { selected_index: 0 },
            on_change: Some(SegmentedOnChange {
                callback: change_cb(probe_state_reentrantly),
                refany: probe.clone(),
            })
            .into(),
        });
        {
            let mut p = probe.downcast_mut::<ReentrantProbe>().expect("ReentrantProbe");
            p.state = state.clone();
        }
        let styled = StyledDom::create_from_dom(Segmented::create(labels(&["a", "b"])).dom());
        let (update, changes) = run_click(Some(styled), seg_node(1), state.clone());
        assert_eq!(update, Update::DoNothing);
        assert_eq!(restyle_writes(&changes).len(), 4, "the restyle must still run afterwards");
        let p = probe.downcast_ref::<ReentrantProbe>().expect("ReentrantProbe");
        assert_eq!(p.calls, 1, "the user callback must have run exactly once");
        assert_eq!(p.saw_index, None, "a re-entrant read of the state must be refused, not aliased");
    }
    #[test]
    fn click_indices_stay_within_the_label_count() {
        // The index is derived from the sibling position, so it can never address
        // past the last rendered segment however many there are.
        let n = 128;
        let (styled, state) = flatten(Segmented::create(n_labels(n)));
        for i in [0usize, 1, n / 2 - 1, n - 2, n - 1] {
            let mut probe = state.clone();
            let (_, changes) = run_click(Some(styled.clone()), seg_node(i), state.clone());
            let idx = selected_index_of(&mut probe);
            assert_eq!(idx, i, "node {} sits at sibling position {i}", seg_node(i));
            assert!(idx < n, "the reported index must always address a real label");
            assert_eq!(restyle_writes(&changes).len(), 2 * n);
        }
    }
    #[test]
    fn click_recovers_a_state_left_out_of_range_by_the_setter() {
        // `set_selected_index(usize::MAX)` renders nothing selected; the first
        // click must snap the state back to a real, in-range segment.
        let seg = Segmented::create(n_labels(3)).with_selected_index(usize::MAX);
        let (styled, state) = flatten(seg);
        let mut probe = state.clone();
        let (_, changes) = run_click(Some(styled), seg_node(1), state);
        assert_eq!(selected_index_of(&mut probe), 1);
        assert_eq!(
            restyle_writes(&changes),
            vec![
                (seg_node(0), "bg", SEG_UNSELECTED_BG_COLOR),
                (seg_node(0), "text", SEG_UNSELECTED_TEXT),
                (seg_node(1), "bg", SEG_SELECTED_BG_COLOR),
                (seg_node(1), "text", SEG_SELECTED_TEXT),
                (seg_node(2), "bg", SEG_UNSELECTED_BG_COLOR),
                (seg_node(2), "text", SEG_UNSELECTED_TEXT),
            ]
        );
    }
}