1use std::vec::Vec;
14
15use azul_core::{
16 callbacks::{CoreCallbackData, Update},
17 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
18 refany::RefAny,
19};
20use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
21use 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
31use crate::callbacks::{Callback, CallbackInfo};
32
33static SEGMENTED_CLASS: &[IdOrClass] =
34 &[Class(AzString::from_const_str("__azul-native-segmented"))];
35static SEGMENT_ITEM_CLASS: &[IdOrClass] =
36 &[Class(AzString::from_const_str("__azul-native-segmented-item"))];
37
38pub type SegmentedOnChangeCallbackType =
40 extern "C" fn(RefAny, CallbackInfo, SegmentedState) -> Update;
41impl_widget_callback!(
42 SegmentedOnChange,
43 OptionSegmentedOnChange,
44 SegmentedOnChangeCallback,
45 SegmentedOnChangeCallbackType
46);
47
48azul_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#[derive(Debug, Clone, PartialEq, Eq)]
63#[repr(C)]
64pub struct Segmented {
65 pub segmented_state: SegmentedStateWrapper,
66 pub labels: StringVec,
68 pub container_style: CssPropertyWithConditionsVec,
70}
71
72#[derive(Debug, Default, Clone, PartialEq, Eq)]
73#[repr(C)]
74pub struct SegmentedStateWrapper {
75 pub inner: SegmentedState,
77 pub on_change: OptionSegmentedOnChange,
79}
80
81#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
83#[repr(C)]
84pub struct SegmentedState {
85 pub selected_index: usize,
87}
88
89const SEG_BORDER_COLOR: ColorU = ColorU {
92 r: 206,
93 g: 212,
94 b: 218,
95 a: 255,
96};
97const SEG_SELECTED_BG_COLOR: ColorU = ColorU {
99 r: 13,
100 g: 110,
101 b: 253,
102 a: 255,
103};
104const SEG_UNSELECTED_BG_COLOR: ColorU = ColorU {
106 r: 255,
107 g: 255,
108 b: 255,
109 a: 255,
110};
111const SEG_SELECTED_TEXT: ColorU = ColorU {
113 r: 255,
114 g: 255,
115 b: 255,
116 a: 255,
117};
118const SEG_UNSELECTED_TEXT: ColorU = ColorU {
120 r: 33,
121 g: 37,
122 b: 41,
123 a: 255,
124};
125
126const SEG_SELECTED_BG_ITEMS: &[StyleBackgroundContent] =
127 &[StyleBackgroundContent::Color(SEG_SELECTED_BG_COLOR)];
128const SEG_SELECTED_BG: StyleBackgroundContentVec =
129 StyleBackgroundContentVec::from_const_slice(SEG_SELECTED_BG_ITEMS);
130const SEG_UNSELECTED_BG_ITEMS: &[StyleBackgroundContent] =
131 &[StyleBackgroundContent::Color(SEG_UNSELECTED_BG_COLOR)];
132const SEG_UNSELECTED_BG: StyleBackgroundContentVec =
133 StyleBackgroundContentVec::from_const_slice(SEG_UNSELECTED_BG_ITEMS);
134
135const SEG_RADIUS: isize = 6;
136
137static 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#[allow(clippy::too_many_lines)] fn build_segment_style(selected: bool, is_first: bool, is_last: bool) -> CssPropertyWithConditionsVec {
152 let (bg, text) = if selected {
153 (SEG_SELECTED_BG, SEG_SELECTED_TEXT)
154 } else {
155 (SEG_UNSELECTED_BG, SEG_UNSELECTED_TEXT)
156 };
157
158 let mut v: Vec<CssPropertyWithConditions> = alloc::vec![
159 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
160 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
161 LayoutFlexDirection::Row,
162 )),
163 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
164 LayoutJustifyContent::Center,
165 )),
166 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
167 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
168 0,
169 ))),
170 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
172 6,
173 ))),
174 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
175 LayoutPaddingBottom::const_px(6),
176 )),
177 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
178 LayoutPaddingLeft::const_px(12),
179 )),
180 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
181 LayoutPaddingRight::const_px(12),
182 )),
183 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
186 LayoutBorderTopWidth::const_px(1),
187 )),
188 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
189 LayoutBorderBottomWidth::const_px(1),
190 )),
191 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
192 LayoutBorderRightWidth::const_px(1),
193 )),
194 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
195 inner: BorderStyle::Solid,
196 })),
197 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
198 StyleBorderBottomStyle {
199 inner: BorderStyle::Solid,
200 },
201 )),
202 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
203 StyleBorderRightStyle {
204 inner: BorderStyle::Solid,
205 },
206 )),
207 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
208 inner: SEG_BORDER_COLOR,
209 })),
210 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
211 StyleBorderBottomColor {
212 inner: SEG_BORDER_COLOR,
213 },
214 )),
215 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
216 StyleBorderRightColor {
217 inner: SEG_BORDER_COLOR,
218 },
219 )),
220 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
221 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
222 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
223 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
224 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
225 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
226 inner: text,
227 })),
228 ];
229
230 if is_first {
231 v.push(CssPropertyWithConditions::simple(
232 CssProperty::const_border_left_width(LayoutBorderLeftWidth::const_px(1)),
233 ));
234 v.push(CssPropertyWithConditions::simple(
235 CssProperty::const_border_left_style(StyleBorderLeftStyle {
236 inner: BorderStyle::Solid,
237 }),
238 ));
239 v.push(CssPropertyWithConditions::simple(
240 CssProperty::const_border_left_color(StyleBorderLeftColor {
241 inner: SEG_BORDER_COLOR,
242 }),
243 ));
244 v.push(CssPropertyWithConditions::simple(
245 CssProperty::const_border_top_left_radius(StyleBorderTopLeftRadius::const_px(SEG_RADIUS)),
246 ));
247 v.push(CssPropertyWithConditions::simple(
248 CssProperty::const_border_bottom_left_radius(StyleBorderBottomLeftRadius::const_px(
249 SEG_RADIUS,
250 )),
251 ));
252 }
253 if is_last {
254 v.push(CssPropertyWithConditions::simple(
255 CssProperty::const_border_top_right_radius(StyleBorderTopRightRadius::const_px(
256 SEG_RADIUS,
257 )),
258 ));
259 v.push(CssPropertyWithConditions::simple(
260 CssProperty::const_border_bottom_right_radius(StyleBorderBottomRightRadius::const_px(
261 SEG_RADIUS,
262 )),
263 ));
264 }
265
266 CssPropertyWithConditionsVec::from_vec(v)
267}
268
269impl Segmented {
270 #[must_use] pub fn create(labels: StringVec) -> Self {
272 Self {
273 segmented_state: SegmentedStateWrapper {
274 inner: SegmentedState { selected_index: 0 },
275 ..Default::default()
276 },
277 labels,
278 container_style: CssPropertyWithConditionsVec::from_const_slice(
279 SEGMENTED_CONTAINER_STYLE,
280 ),
281 }
282 }
283
284 #[inline]
286 pub const fn set_selected_index(&mut self, selected_index: usize) {
287 self.segmented_state.inner.selected_index = selected_index;
288 }
289
290 #[inline]
292 #[must_use] pub const fn with_selected_index(mut self, selected_index: usize) -> Self {
293 self.set_selected_index(selected_index);
294 self
295 }
296
297 #[inline]
298 #[must_use] pub fn swap_with_default(&mut self) -> Self {
299 let mut s = Self::create(StringVec::from_const_slice(&[]));
300 core::mem::swap(&mut s, self);
301 s
302 }
303
304 #[inline]
305 pub fn set_on_change<C: Into<SegmentedOnChangeCallback>>(
306 &mut self,
307 data: RefAny,
308 on_change: C,
309 ) {
310 self.segmented_state.on_change = Some(SegmentedOnChange {
311 callback: on_change.into(),
312 refany: data,
313 })
314 .into();
315 }
316
317 #[inline]
318 #[must_use] pub fn with_on_change<C: Into<SegmentedOnChangeCallback>>(
319 mut self,
320 data: RefAny,
321 on_change: C,
322 ) -> Self {
323 self.set_on_change(data, on_change);
324 self
325 }
326
327 #[must_use] pub fn dom(self) -> Dom {
328 use azul_core::{
329 callbacks::CoreCallback,
330 dom::{EventFilter, HoverEventFilter},
331 refany::OptionRefAny,
332 };
333
334 let selected = self.segmented_state.inner.selected_index;
335 let count = self.labels.as_ref().len();
336
337 let state = RefAny::new(self.segmented_state);
340
341 let mut children: Vec<Dom> = Vec::with_capacity(count);
342 for (i, label) in self.labels.as_ref().iter().enumerate() {
343 let is_first = i == 0;
344 let is_last = i + 1 == count;
345 let seg_style = build_segment_style(i == selected, is_first, is_last);
346
347 children.push(
348 Dom::create_text(label.clone())
349 .with_ids_and_classes(IdOrClassVec::from_const_slice(SEGMENT_ITEM_CLASS))
350 .with_css_props(seg_style)
351 .with_callbacks(
352 vec![CoreCallbackData {
353 event: EventFilter::Hover(HoverEventFilter::MouseUp),
354 callback: CoreCallback {
355 cb: on_segment_click as usize,
356 ctx: OptionRefAny::None,
357 },
358 refany: state.clone(),
359 }]
360 .into(),
361 )
362 .with_tab_index(TabIndex::Auto),
363 );
364 }
365
366 Dom::create_div()
367 .with_ids_and_classes(IdOrClassVec::from_const_slice(SEGMENTED_CLASS))
368 .with_css_props(self.container_style)
369 .with_children(children.into())
370 }
371}
372
373impl Default for Segmented {
374 fn default() -> Self {
375 Self::create(StringVec::from_const_slice(&[]))
376 }
377}
378
379extern "C" fn on_segment_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
383 use azul_core::dom::DomNodeId;
384
385 let clicked = info.get_hit_node();
386 let Some(parent) = info.get_parent(clicked) else {
387 return Update::DoNothing;
388 };
389
390 let mut segments: Vec<DomNodeId> = Vec::new();
392 let mut cur = info.get_first_child(parent);
393 while let Some(node) = cur {
394 segments.push(node);
395 cur = info.get_next_sibling(node);
396 }
397
398 let Some(selected) = segments.iter().position(|n| *n == clicked) else {
399 return Update::DoNothing;
400 };
401
402 let result = {
403 let Some(mut seg) = data.downcast_mut::<SegmentedStateWrapper>() else {
404 return Update::DoNothing;
405 };
406 seg.inner.selected_index = selected;
407 let inner = seg.inner;
408 let seg = &mut *seg;
409 match seg.on_change.as_mut() {
410 Some(SegmentedOnChange { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
411 None => Update::DoNothing,
412 }
413 };
414
415 for (i, node) in segments.iter().enumerate() {
418 if i == selected {
419 info.set_css_property(*node, CssProperty::const_background_content(SEG_SELECTED_BG));
420 info.set_css_property(
421 *node,
422 CssProperty::const_text_color(StyleTextColor {
423 inner: SEG_SELECTED_TEXT,
424 }),
425 );
426 } else {
427 info.set_css_property(
428 *node,
429 CssProperty::const_background_content(SEG_UNSELECTED_BG),
430 );
431 info.set_css_property(
432 *node,
433 CssProperty::const_text_color(StyleTextColor {
434 inner: SEG_UNSELECTED_TEXT,
435 }),
436 );
437 }
438 }
439
440 result
441}
442
443impl From<Segmented> for Dom {
444 fn from(s: Segmented) -> Self {
445 s.dom()
446 }
447}
448
449#[cfg(test)]
450mod 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 fn labels(v: &[&str]) -> StringVec {
487 StringVec::from_vec(v.iter().map(|s| AzString::from(*s)).collect::<Vec<_>>())
488 }
489
490 fn n_labels(n: usize) -> StringVec {
492 StringVec::from_vec((0..n).map(|i| AzString::from(format!("s{i}"))).collect::<Vec<_>>())
493 }
494
495 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 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
510 v.as_ref().iter().map(|p| p.property.clone()).collect()
511 }
512
513 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 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 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 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 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 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 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 fn text_of(node: &Dom) -> Option<&str> {
652 match node.root.get_node_type() {
653 NodeType::Text(s) => Some(s.as_ref().as_str()),
654 _ => None,
655 }
656 }
657
658 fn inline_properties(node: &Dom) -> Vec<CssProperty> {
660 node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
661 }
662
663 fn recursive_descendants(node: &Dom) -> usize {
666 node.children.as_ref().iter().map(|c| 1 + recursive_descendants(c)).sum()
667 }
668
669 fn boundary_indices() -> Vec<usize> {
673 vec![
674 0,
675 1,
676 2,
677 usize::MAX / 2,
678 usize::MAX / 2 + 1,
679 usize::MAX - 1,
680 usize::MAX,
681 (-1i64) as usize,
682 i64::MIN as usize,
683 u32::MAX as usize,
684 ]
685 }
686
687 fn adversarial_strings() -> Vec<String> {
692 let mut v: Vec<String> = [
693 "",
694 "Day",
695 " ",
696 "e\u{0301}", "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}", "\u{5E9}\u{5DC}\u{5D5}\u{5DD}", "\0", "a\0b", "\u{FFFD}\u{202E}\u{200B}", "line\nbreak\ttab", "-9223372036854775808", ]
705 .iter()
706 .map(|s| (*s).to_string())
707 .collect();
708 v.push("x".repeat(100_000));
709 v
710 }
711
712 fn change_cb(f: SegmentedOnChangeCallbackType) -> SegmentedOnChangeCallback {
714 f.into()
715 }
716
717 struct IndexLog {
719 seen: Vec<usize>,
720 }
721
722 extern "C" fn record_index(mut data: RefAny, _: CallbackInfo, state: SegmentedState) -> Update {
723 if let Some(mut log) = data.downcast_mut::<IndexLog>() {
724 log.seen.push(state.selected_index);
725 }
726 Update::RefreshDom
727 }
728
729 extern "C" fn change_do_nothing(_: RefAny, _: CallbackInfo, _: SegmentedState) -> Update {
730 Update::DoNothing
731 }
732
733 extern "C" fn change_refresh_all(_: RefAny, _: CallbackInfo, state: SegmentedState) -> Update {
734 let _ = state.selected_index;
738 Update::RefreshDomAllWindows
739 }
740
741 struct ReentrantProbe {
744 state: RefAny,
746 saw_index: Option<usize>,
749 calls: usize,
750 }
751
752 extern "C" fn probe_state_reentrantly(
753 mut data: RefAny,
754 _: CallbackInfo,
755 _: SegmentedState,
756 ) -> Update {
757 if let Some(mut probe) = data.downcast_mut::<ReentrantProbe>() {
758 probe.calls += 1;
759 let mut state = probe.state.clone();
760 probe.saw_index =
761 state.downcast_ref::<SegmentedStateWrapper>().map(|w| w.inner.selected_index);
762 }
763 Update::DoNothing
764 }
765
766 fn log_indices(data: &mut RefAny) -> Vec<usize> {
767 data.downcast_ref::<IndexLog>().expect("payload must still be an IndexLog").seen.clone()
768 }
769
770 fn selected_index_of(data: &mut RefAny) -> usize {
771 data.downcast_ref::<SegmentedStateWrapper>()
772 .expect("payload must still be a SegmentedStateWrapper")
773 .inner
774 .selected_index
775 }
776
777 fn segment_state(dom: &Dom, i: usize) -> RefAny {
779 let cbs = dom.children.as_ref()[i].root.get_callbacks();
780 cbs.as_ref()
781 .first()
782 .expect("every segment must carry the click callback")
783 .refany
784 .clone()
785 }
786
787 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
790 DomLayoutResult {
791 styled_dom,
792 layout_tree: LayoutTree {
793 nodes: Vec::new(),
794 warm: Vec::new(),
795 cold: Vec::new(),
796 root: 0,
797 dom_to_layout: BTreeMap::new(),
798 children_arena: Vec::new(),
799 children_offsets: Vec::new(),
800 subtree_needs_intrinsic: Vec::new(),
801 },
802 calculated_positions: Vec::new(),
803 viewport: LogicalRect::zero(),
804 display_list: DisplayList::default(),
805 scroll_ids: HashMap::new(),
806 scroll_id_to_node_id: HashMap::new(),
807 }
808 }
809
810 fn flatten(seg: Segmented) -> (StyledDom, RefAny) {
813 let dom = seg.dom();
814 let state = segment_state(&dom, 0);
815 (StyledDom::create_from_dom(dom), state)
816 }
817
818 fn run_click(
822 styled: Option<StyledDom>,
823 hit: usize,
824 data: RefAny,
825 ) -> (Update, Vec<CallbackChange>) {
826 let mut layout_window =
827 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
828 if let Some(sd) = styled {
829 layout_window.layout_results.insert(DomId::ROOT_ID, layout_result(sd));
830 }
831
832 let renderer_resources = RendererResources::default();
833 let previous_window_state: Option<FullWindowState> = None;
834 let current_window_state = FullWindowState::default();
835 let gl_context = OptionGlContextPtr::None;
836 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
837 BTreeMap::new();
838 let window_handle = RawWindowHandle::Unsupported;
839 let system_callbacks = ExternalSystemCallbacks::rust_internal();
840
841 let ref_data = CallbackInfoRefData {
842 layout_window: &layout_window,
843 renderer_resources: &renderer_resources,
844 previous_window_state: &previous_window_state,
845 current_window_state: ¤t_window_state,
846 gl_context: &gl_context,
847 current_scroll_manager: &scroll_states,
848 current_window_handle: &window_handle,
849 system_callbacks: &system_callbacks,
850 system_style: Arc::new(SystemStyle::default()),
851 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
852 #[cfg(feature = "icu")]
853 icu_localizer: IcuLocalizerHandle::default(),
854 ctx: OptionRefAny::None,
855 };
856
857 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
858
859 let info = CallbackInfo::new(
860 &ref_data,
861 &changes,
862 DomNodeId {
863 dom: DomId::ROOT_ID,
864 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
865 },
866 OptionLogicalPosition::None,
867 OptionLogicalPosition::None,
868 );
869
870 let update = on_segment_click(data, info);
871 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
872 (update, recorded)
873 }
874
875 fn restyle_writes(changes: &[CallbackChange]) -> Vec<(usize, &'static str, ColorU)> {
879 let mut out = Vec::new();
880 for change in changes {
881 let CallbackChange::ChangeNodeCssProperties { node_id, properties, .. } = change else {
882 panic!("the restyle must only emit ChangeNodeCssProperties, got {change:?}");
883 };
884 for p in properties.as_ref() {
885 match p {
886 CssProperty::BackgroundContent(v) => {
887 let layers =
888 v.get_property().expect("restyle must write an exact background");
889 assert_eq!(layers.as_ref().len(), 1, "a segment fill is a single layer");
890 match &layers.as_ref()[0] {
891 StyleBackgroundContent::Color(c) => {
892 out.push((node_id.index(), "bg", *c));
893 }
894 other => panic!("segment background is not a flat colour: {other:?}"),
895 }
896 }
897 CssProperty::TextColor(v) => {
898 let c = v.get_property().expect("restyle must write an exact text colour");
899 out.push((node_id.index(), "text", c.inner));
900 }
901 other => panic!("unexpected restyle property: {other:?}"),
902 }
903 }
904 }
905 out
906 }
907
908 #[test]
913 fn build_segment_style_handles_all_eight_flag_combinations() {
914 for (selected, first, last) in ALL_FLAGS {
917 let style = build_segment_style(selected, first, last);
918 let expected = 24 + if first { 5 } else { 0 } + if last { 2 } else { 0 };
919 assert_eq!(
920 style.as_ref().len(),
921 expected,
922 "({selected}, {first}, {last}): unexpected declaration count"
923 );
924 }
925 }
926
927 #[test]
928 fn build_segment_style_colours_depend_only_on_selected() {
929 for selected in [false, true] {
932 let reference = build_segment_style(selected, false, false);
933 let bg = background_color(&reference).expect("a segment must declare a background");
934 let fg = text_color(&reference).expect("a segment must declare a text colour");
935
936 for (first, last) in [(false, false), (false, true), (true, false), (true, true)] {
937 let style = build_segment_style(selected, first, last);
938 assert_eq!(background_color(&style), Some(bg), "selected={selected}: background moved with position");
939 assert_eq!(text_color(&style), Some(fg), "selected={selected}: text colour moved with position");
940 }
941 }
942
943 assert_eq!(background_color(&build_segment_style(true, false, false)), Some(SEG_SELECTED_BG_COLOR));
944 assert_eq!(text_color(&build_segment_style(true, false, false)), Some(SEG_SELECTED_TEXT));
945 assert_eq!(background_color(&build_segment_style(false, false, false)), Some(SEG_UNSELECTED_BG_COLOR));
946 assert_eq!(text_color(&build_segment_style(false, false, false)), Some(SEG_UNSELECTED_TEXT));
947 }
948
949 #[test]
950 fn build_segment_style_adds_the_left_border_only_to_the_first_segment() {
951 for (selected, first, last) in ALL_FLAGS {
954 let style = build_segment_style(selected, first, last);
955 let want = usize::from(first);
956
957 assert_eq!(
958 declares(&style, |p| matches!(p, CssProperty::BorderLeftWidth(_))),
959 want,
960 "({selected}, {first}, {last}): left border width"
961 );
962 assert_eq!(
963 declares(&style, |p| matches!(p, CssProperty::BorderLeftStyle(_))),
964 want,
965 "({selected}, {first}, {last}): left border style"
966 );
967 assert_eq!(
968 declares(&style, |p| matches!(p, CssProperty::BorderLeftColor(_))),
969 want,
970 "({selected}, {first}, {last}): left border colour"
971 );
972 }
973 }
974
975 #[test]
976 fn build_segment_style_rounds_only_the_outer_corners() {
977 for (selected, first, last) in ALL_FLAGS {
978 let style = build_segment_style(selected, first, last);
979 let (tl, tr, bl, br) = radii_px(&style);
980 let r = SEG_RADIUS as f32;
981
982 assert_eq!(tl, first.then_some(r), "({selected}, {first}, {last}): top-left radius");
983 assert_eq!(bl, first.then_some(r), "({selected}, {first}, {last}): bottom-left radius");
984 assert_eq!(tr, last.then_some(r), "({selected}, {first}, {last}): top-right radius");
985 assert_eq!(br, last.then_some(r), "({selected}, {first}, {last}): bottom-right radius");
986 }
987
988 let solo = build_segment_style(true, true, true);
990 let r = SEG_RADIUS as f32;
991 assert_eq!(radii_px(&solo), (Some(r), Some(r), Some(r), Some(r)));
992 let middle = build_segment_style(true, false, false);
993 assert_eq!(radii_px(&middle), (None, None, None, None));
994 }
995
996 #[test]
997 fn build_segment_style_always_paints_the_shared_separator_edges() {
998 for (selected, first, last) in ALL_FLAGS {
1001 let style = build_segment_style(selected, first, last);
1002 let (top, bottom, left, right) = border_widths_px(&style);
1003
1004 assert_eq!(top, Some(1.0), "({selected}, {first}, {last}): top border");
1005 assert_eq!(bottom, Some(1.0), "({selected}, {first}, {last}): bottom border");
1006 assert_eq!(right, Some(1.0), "({selected}, {first}, {last}): right border");
1007 assert_eq!(left, first.then_some(1.0), "({selected}, {first}, {last}): left border");
1008
1009 for count in [
1011 declares(&style, |p| matches!(p, CssProperty::BorderTopStyle(_))),
1012 declares(&style, |p| matches!(p, CssProperty::BorderBottomStyle(_))),
1013 declares(&style, |p| matches!(p, CssProperty::BorderRightStyle(_))),
1014 declares(&style, |p| matches!(p, CssProperty::BorderTopColor(_))),
1015 declares(&style, |p| matches!(p, CssProperty::BorderBottomColor(_))),
1016 declares(&style, |p| matches!(p, CssProperty::BorderRightColor(_))),
1017 ] {
1018 assert_eq!(count, 1, "({selected}, {first}, {last}): a shared edge lost its style/colour");
1019 }
1020 }
1021 }
1022
1023 #[test]
1024 fn build_segment_style_border_colours_are_the_single_neutral_grey() {
1025 for (selected, first, last) in ALL_FLAGS {
1028 let style = build_segment_style(selected, first, last);
1029 for p in style.as_ref() {
1030 let found = match &p.property {
1031 CssProperty::BorderTopColor(c) => c.get_property().map(|c| c.inner),
1032 CssProperty::BorderBottomColor(c) => c.get_property().map(|c| c.inner),
1033 CssProperty::BorderLeftColor(c) => c.get_property().map(|c| c.inner),
1034 CssProperty::BorderRightColor(c) => c.get_property().map(|c| c.inner),
1035 _ => None,
1036 };
1037 if let Some(c) = found {
1038 assert_eq!(
1039 c, SEG_BORDER_COLOR,
1040 "({selected}, {first}, {last}): border colour {c:?} is not the shared grey"
1041 );
1042 }
1043 }
1044 }
1045 }
1046
1047 #[test]
1048 fn build_segment_style_geometry_is_absolute_and_symmetric() {
1049 for (selected, first, last) in ALL_FLAGS {
1050 let style = build_segment_style(selected, first, last);
1051 assert_eq!(
1053 padding_px(&style),
1054 (Some(6.0), Some(6.0), Some(12.0), Some(12.0)),
1055 "({selected}, {first}, {last}): padding is not 6px 12px"
1056 );
1057 assert_eq!(font_size_px(&style), Some(13.0), "({selected}, {first}, {last}): font size");
1058 }
1059 }
1060
1061 #[test]
1062 fn build_segment_style_declares_every_property_unconditionally() {
1063 for (selected, first, last) in ALL_FLAGS {
1066 let style = build_segment_style(selected, first, last);
1067 for p in style.as_ref() {
1068 assert!(
1069 p.apply_if.as_ref().is_empty(),
1070 "({selected}, {first}, {last}): {:?} is conditional",
1071 p.property
1072 );
1073 }
1074 }
1075 }
1076
1077 #[test]
1078 fn build_segment_style_never_declares_the_same_property_twice() {
1079 for (selected, first, last) in ALL_FLAGS {
1082 let style = build_segment_style(selected, first, last);
1083 let mut seen = HashSet::new();
1084 for kind in property_kinds(&style) {
1085 assert!(
1086 seen.insert(kind),
1087 "({selected}, {first}, {last}): duplicate property kind in the style vec"
1088 );
1089 }
1090 assert_eq!(seen.len(), style.as_ref().len());
1091 }
1092 }
1093
1094 #[test]
1095 fn build_segment_style_is_pure() {
1096 for (selected, first, last) in ALL_FLAGS {
1099 let a = build_segment_style(selected, first, last);
1100 let b = build_segment_style(selected, first, last);
1101 assert_eq!(properties(&a), properties(&b), "({selected}, {first}, {last}): not pure");
1102 }
1103 }
1104
1105 #[test]
1106 fn build_segment_style_keeps_the_label_readable_and_opaque() {
1107 for selected in [false, true] {
1108 let style = build_segment_style(selected, false, false);
1109 let bg = background_color(&style).expect("background");
1110 let fg = text_color(&style).expect("text colour");
1111
1112 assert_eq!(bg.a, 255, "selected={selected}: a translucent fill lets the page bleed through");
1113 assert_eq!(fg.a, 255, "selected={selected}: translucent label text");
1114 assert_ne!(bg, fg, "selected={selected}: an invisible label is not a segment");
1115 assert!(
1116 (luma(bg) - luma(fg)).abs() >= 60.0,
1117 "selected={selected}: brightness gap {:.1} is too low to read",
1118 (luma(bg) - luma(fg)).abs()
1119 );
1120 }
1121
1122 let sel = build_segment_style(true, false, false);
1125 let unsel = build_segment_style(false, false, false);
1126 assert_ne!(background_color(&sel), background_color(&unsel));
1127 assert_ne!(text_color(&sel), text_color(&unsel));
1128 }
1129
1130 #[test]
1131 fn build_segment_style_declares_the_interaction_affordances() {
1132 for (selected, first, last) in ALL_FLAGS {
1133 let style = build_segment_style(selected, first, last);
1134 let ctx = format!("({selected}, {first}, {last})");
1135
1136 assert!(
1137 style.as_ref().iter().any(|p| matches!(
1138 &p.property,
1139 CssProperty::Cursor(c) if c.get_property() == Some(&StyleCursor::Pointer)
1140 )),
1141 "{ctx}: a clickable segment must show the pointer cursor"
1142 );
1143 assert!(
1144 style.as_ref().iter().any(|p| matches!(
1145 &p.property,
1146 CssProperty::UserSelect(u) if u.get_property() == Some(&StyleUserSelect::None)
1147 )),
1148 "{ctx}: click-dragging a segment must not select its caption"
1149 );
1150 assert!(
1151 style.as_ref().iter().any(|p| matches!(
1152 &p.property,
1153 CssProperty::TextAlign(t) if t.get_property() == Some(&StyleTextAlign::Center)
1154 )),
1155 "{ctx}: captions are centred"
1156 );
1157 assert!(
1158 style.as_ref().iter().any(|p| matches!(
1159 &p.property,
1160 CssProperty::FlexGrow(f) if f.get_property().map(|f| f.inner.get()) == Some(0.0)
1161 )),
1162 "{ctx}: segments hug their caption, they do not stretch"
1163 );
1164 }
1165 }
1166
1167 #[test]
1172 fn create_preserves_labels_verbatim() {
1173 for case in [
1174 vec![],
1175 vec!["only"],
1176 vec!["Day", "Week"],
1177 vec!["Day", "Week", "Month", "Year"],
1178 vec!["dup", "dup", "dup"],
1179 ] {
1180 let seg = Segmented::create(labels(&case));
1181 let got: Vec<&str> = seg.labels.as_ref().iter().map(AzString::as_str).collect();
1182 assert_eq!(got, case, "create must not reorder/drop/dedupe/rewrite labels");
1183 }
1184 }
1185
1186 #[test]
1187 fn create_preserves_adversarial_labels_byte_for_byte() {
1188 for s in adversarial_strings() {
1189 let seg = Segmented::create(labels(&[s.as_str()]));
1190 let stored = seg.labels.as_ref()[0].as_str();
1191 assert_eq!(stored, s.as_str(), "the caption changed on its way into the widget");
1192 assert_eq!(
1193 seg.labels.as_ref()[0].as_ref().len(),
1194 s.len(),
1195 "byte length changed (NUL truncation?)"
1196 );
1197 }
1198 }
1199
1200 #[test]
1201 fn create_selects_the_first_segment_and_installs_no_callback() {
1202 for n in [0usize, 1, 2, 7] {
1203 let seg = Segmented::create(n_labels(n));
1204 assert_eq!(
1205 seg.segmented_state.inner.selected_index, 0,
1206 "n={n}: a fresh control starts on segment 0"
1207 );
1208 assert!(
1209 seg.segmented_state.on_change.as_ref().is_none(),
1210 "n={n}: create must not wire a callback"
1211 );
1212 }
1213 }
1214
1215 #[test]
1216 fn create_installs_the_shared_container_style() {
1217 let seg = Segmented::create(labels(&["a", "b"]));
1218 assert_eq!(
1219 seg.container_style.as_ref(),
1220 SEGMENTED_CONTAINER_STYLE,
1221 "create must install the shared container style"
1222 );
1223
1224 let style = &seg.container_style;
1227 assert_eq!(
1228 declares(style, |p| matches!(
1229 p, CssProperty::Display(d) if d.get_property() == Some(&LayoutDisplay::Flex))),
1230 1,
1231 "the row container must be a flex box"
1232 );
1233 assert_eq!(
1234 declares(style, |p| matches!(
1235 p, CssProperty::FlexDirection(d) if d.get_property() == Some(&LayoutFlexDirection::Row))),
1236 1,
1237 "segments are joined horizontally"
1238 );
1239 assert_eq!(
1240 declares(style, |p| matches!(
1241 p, CssProperty::AlignItems(a) if a.get_property() == Some(&LayoutAlignItems::Center))),
1242 1
1243 );
1244 assert_eq!(
1245 declares(style, |p| matches!(
1246 p, CssProperty::AlignSelf(a) if a.get_property() == Some(&LayoutAlignSelf::Start))),
1247 1
1248 );
1249 assert_eq!(
1250 declares(style, |p| matches!(
1251 p, CssProperty::FlexGrow(f) if f.get_property().map(|f| f.inner.get()) == Some(0.0))),
1252 1,
1253 "the group hugs its segments instead of filling the parent"
1254 );
1255
1256 for p in seg.container_style.as_ref() {
1257 assert!(p.apply_if.as_ref().is_empty(), "{:?} is conditional", p.property);
1258 }
1259 }
1260
1261 #[test]
1262 fn create_with_no_labels_equals_default() {
1263 let empty = Segmented::create(StringVec::from_const_slice(&[]));
1264 assert_eq!(empty, Segmented::default(), "Default must be the empty control");
1265 assert_eq!(empty.labels.as_ref().len(), 0);
1266 assert!(Segmented::default().segmented_state.on_change.as_ref().is_none());
1267 }
1268
1269 #[test]
1270 fn create_scales_to_a_very_long_label_list() {
1271 let n = 4096;
1272 let seg = Segmented::create(n_labels(n));
1273 assert_eq!(seg.labels.as_ref().len(), n);
1274 assert_eq!(seg.labels.as_ref()[n - 1].as_str(), format!("s{}", n - 1));
1275 assert_eq!(seg.segmented_state.inner.selected_index, 0);
1276 }
1277
1278 #[test]
1283 fn set_selected_index_stores_every_boundary_value_verbatim() {
1284 for i in boundary_indices() {
1287 let mut seg = Segmented::create(labels(&["a", "b", "c"]));
1288 seg.set_selected_index(i);
1289 assert_eq!(seg.segmented_state.inner.selected_index, i, "index {i} was not stored as-is");
1290 }
1291 }
1292
1293 #[test]
1294 fn set_selected_index_does_not_clamp_to_the_label_count() {
1295 let mut seg = Segmented::create(labels(&["a", "b"]));
1298 for i in [2usize, 3, 1_000, usize::MAX] {
1299 seg.set_selected_index(i);
1300 assert_eq!(seg.segmented_state.inner.selected_index, i);
1301 assert_eq!(seg.labels.as_ref().len(), 2, "the setter must not touch the labels");
1302 }
1303 }
1304
1305 #[test]
1306 fn set_selected_index_is_idempotent_and_last_write_wins() {
1307 let mut seg = Segmented::create(labels(&["a", "b", "c"]));
1308 for i in [1usize, 1, 1] {
1309 seg.set_selected_index(i);
1310 }
1311 assert_eq!(seg.segmented_state.inner.selected_index, 1);
1312
1313 for i in [0usize, usize::MAX, 2, 0] {
1314 seg.set_selected_index(i);
1315 }
1316 assert_eq!(seg.segmented_state.inner.selected_index, 0, "the last write must win");
1317 }
1318
1319 #[test]
1320 fn set_selected_index_leaves_every_other_field_alone() {
1321 let mut seg = Segmented::create(labels(&["a", "b"]))
1322 .with_on_change(RefAny::new(7u8), change_cb(change_do_nothing));
1323 let before = seg.clone();
1324
1325 seg.set_selected_index(usize::MAX);
1326
1327 assert_eq!(seg.labels, before.labels, "labels changed");
1328 assert_eq!(seg.container_style, before.container_style, "container style changed");
1329 assert_eq!(
1330 seg.segmented_state.on_change, before.segmented_state.on_change,
1331 "the callback was disturbed"
1332 );
1333 }
1334
1335 #[test]
1336 fn with_selected_index_round_trips_through_the_setter() {
1337 for i in boundary_indices() {
1338 let via_builder = Segmented::create(labels(&["a", "b"])).with_selected_index(i);
1339 let mut via_setter = Segmented::create(labels(&["a", "b"]));
1340 via_setter.set_selected_index(i);
1341
1342 assert_eq!(via_builder, via_setter, "index {i}: builder and setter diverge");
1343 assert_eq!(via_builder.segmented_state.inner.selected_index, i);
1344 }
1345 }
1346
1347 #[test]
1348 fn with_selected_index_preserves_the_rest_of_the_widget() {
1349 let base = Segmented::create(labels(&["a", "b", "c"]));
1350 let built = base.clone().with_selected_index(2);
1351
1352 assert_eq!(built.labels, base.labels);
1353 assert_eq!(built.container_style, base.container_style);
1354 assert_eq!(built.labels.as_ref().len(), 3, "len/contents must stay consistent");
1355 assert!(built.segmented_state.on_change.as_ref().is_none());
1356 }
1357
1358 #[test]
1359 fn with_selected_index_chains_with_last_wins() {
1360 let seg = Segmented::create(labels(&["a", "b", "c"]))
1361 .with_selected_index(usize::MAX)
1362 .with_selected_index(0)
1363 .with_selected_index(2);
1364 assert_eq!(seg.segmented_state.inner.selected_index, 2);
1365 }
1366
1367 #[test]
1372 fn swap_with_default_returns_the_original_and_leaves_a_default_behind() {
1373 let mut seg = Segmented::create(labels(&["Day", "Week", "Month"])).with_selected_index(2);
1374 let expected = seg.clone();
1375
1376 let taken = seg.swap_with_default();
1377
1378 assert_eq!(taken, expected, "the caller must get the original widget back");
1379 assert_eq!(seg, Segmented::default(), "a default must be left in its place");
1380 assert_eq!(seg.labels.as_ref().len(), 0);
1381 assert_eq!(seg.segmented_state.inner.selected_index, 0);
1382 }
1383
1384 #[test]
1385 fn swap_with_default_moves_the_callback_out_with_the_widget() {
1386 let mut seg = Segmented::create(labels(&["a", "b"]))
1387 .with_on_change(RefAny::new(1u8), change_cb(record_index));
1388
1389 let taken = seg.swap_with_default();
1390
1391 assert!(
1392 taken.segmented_state.on_change.as_ref().is_some(),
1393 "the callback must travel with the taken widget"
1394 );
1395 assert!(
1396 seg.segmented_state.on_change.as_ref().is_none(),
1397 "the leftover default must not keep a handle on the callback"
1398 );
1399 }
1400
1401 #[test]
1402 fn swap_with_default_on_a_default_is_a_no_op() {
1403 let mut seg = Segmented::default();
1404 let taken = seg.swap_with_default();
1405 assert_eq!(taken, Segmented::default());
1406 assert_eq!(seg, Segmented::default());
1407 }
1408
1409 #[test]
1410 fn swap_with_default_twice_yields_a_default_the_second_time() {
1411 let mut seg = Segmented::create(labels(&["a", "b"])).with_selected_index(1);
1412 let first = seg.swap_with_default();
1413 let second = seg.swap_with_default();
1414
1415 assert_eq!(first.labels.as_ref().len(), 2);
1416 assert_eq!(first.segmented_state.inner.selected_index, 1);
1417 assert_eq!(second, Segmented::default(), "the second take is the default we left behind");
1418 assert_eq!(seg, Segmented::default());
1419 }
1420
1421 #[test]
1422 fn swap_with_default_does_not_truncate_a_large_label_list() {
1423 let n = 1024;
1424 let mut seg = Segmented::create(n_labels(n)).with_selected_index(n - 1);
1425 let taken = seg.swap_with_default();
1426
1427 assert_eq!(taken.labels.as_ref().len(), n);
1428 assert_eq!(taken.labels.as_ref()[n - 1].as_str(), format!("s{}", n - 1));
1429 assert_eq!(taken.segmented_state.inner.selected_index, n - 1);
1430 }
1431
1432 #[test]
1437 fn set_on_change_installs_the_callback_and_its_payload() {
1438 let mut seg = Segmented::create(labels(&["a", "b"]));
1439 let mut payload = RefAny::new(IndexLog { seen: Vec::new() });
1440 seg.set_on_change(payload.clone(), change_cb(record_index));
1441
1442 let installed =
1443 seg.segmented_state.on_change.as_ref().expect("set_on_change must install a callback");
1444 assert_eq!(installed.callback.cb as usize, record_index as usize, "wrong function installed");
1445 assert!(
1446 matches!(installed.callback.ctx, OptionRefAny::None),
1447 "a native Rust callback carries no FFI context"
1448 );
1449
1450 let mut stored = installed.refany.clone();
1453 {
1454 let mut log = stored.downcast_mut::<IndexLog>().expect("payload type must survive");
1455 log.seen.push(42);
1456 }
1457 assert_eq!(log_indices(&mut payload), vec![42], "the payload was copied, not shared");
1458 }
1459
1460 #[test]
1461 fn set_on_change_overwrites_a_previously_installed_callback() {
1462 let mut seg = Segmented::create(labels(&["a", "b"]));
1463 seg.set_on_change(RefAny::new(1u8), change_cb(record_index));
1464 seg.set_on_change(RefAny::new(2u8), change_cb(change_refresh_all));
1465
1466 let installed = seg.segmented_state.on_change.as_ref().expect("callback");
1467 assert_eq!(installed.callback.cb as usize, change_refresh_all as usize, "the last setter must win");
1468 assert_ne!(installed.callback.cb as usize, record_index as usize);
1469 }
1470
1471 #[test]
1472 fn set_on_change_does_not_disturb_labels_or_selection() {
1473 let mut seg = Segmented::create(labels(&["a", "b", "c"])).with_selected_index(2);
1474 seg.set_on_change(RefAny::new(0u8), change_cb(change_do_nothing));
1475
1476 assert_eq!(seg.labels.as_ref().len(), 3);
1477 assert_eq!(seg.segmented_state.inner.selected_index, 2, "installing a callback moved the selection");
1478 }
1479
1480 #[test]
1481 fn with_on_change_matches_the_setter_exactly() {
1482 let payload = RefAny::new(9u8);
1483 let via_builder = Segmented::create(labels(&["a", "b"]))
1484 .with_on_change(payload.clone(), change_cb(change_do_nothing));
1485 let mut via_setter = Segmented::create(labels(&["a", "b"]));
1486 via_setter.set_on_change(payload, change_cb(change_do_nothing));
1487
1488 assert_eq!(via_builder, via_setter, "builder and setter must produce the same widget");
1489 }
1490
1491 #[test]
1492 fn with_on_change_holds_its_invariants_after_construction() {
1493 let seg = Segmented::create(n_labels(5))
1494 .with_selected_index(3)
1495 .with_on_change(RefAny::new(0u8), change_cb(change_refresh_all));
1496
1497 assert_eq!(seg.labels.as_ref().len(), 5, "label count must survive the builder chain");
1498 assert_eq!(seg.segmented_state.inner.selected_index, 3, "the selection must survive");
1499 assert_eq!(
1500 seg.container_style.as_ref(),
1501 SEGMENTED_CONTAINER_STYLE,
1502 "the container style must survive"
1503 );
1504 let installed = seg.segmented_state.on_change.as_ref().expect("callback");
1505 assert_eq!(installed.callback.cb as usize, change_refresh_all as usize);
1506 }
1507
1508 #[test]
1509 fn with_on_change_chains_with_last_wins() {
1510 let seg = Segmented::create(labels(&["a"]))
1511 .with_on_change(RefAny::new(0u8), change_cb(record_index))
1512 .with_on_change(RefAny::new(0u8), change_cb(change_do_nothing));
1513 let installed = seg.segmented_state.on_change.as_ref().expect("callback");
1514 assert_eq!(installed.callback.cb as usize, change_do_nothing as usize);
1515 }
1516
1517 #[test]
1522 fn dom_emits_one_text_child_per_label_in_order() {
1523 let case = ["Day", "Week", "Month", "Year"];
1524 let dom = Segmented::create(labels(&case)).dom();
1525
1526 assert!(matches!(dom.root.get_node_type(), NodeType::Div), "the group is a div");
1527 assert!(dom.root.has_class("__azul-native-segmented"));
1528 assert!(dom.root.get_callbacks().as_ref().is_empty(), "the container itself is not clickable");
1529
1530 let children = dom.children.as_ref();
1531 assert_eq!(children.len(), case.len());
1532 for (i, child) in children.iter().enumerate() {
1533 assert_eq!(text_of(child), Some(case[i]), "segment {i} shows the wrong caption");
1534 assert!(child.root.has_class("__azul-native-segmented-item"), "segment {i} lost its class");
1535 }
1536 }
1537
1538 #[test]
1539 fn dom_of_an_empty_control_is_a_childless_container() {
1540 let dom = Segmented::create(StringVec::from_const_slice(&[])).dom();
1542 assert_eq!(dom.children.as_ref().len(), 0);
1543 assert_eq!(dom.estimated_total_children, 0);
1544 assert!(dom.root.has_class("__azul-native-segmented"));
1545
1546 let styled = StyledDom::create_from_dom(dom);
1547 assert_eq!(styled.node_hierarchy.as_ref().len(), 1, "just the container");
1548 }
1549
1550 #[test]
1551 fn dom_styles_each_segment_by_its_position_and_selection() {
1552 for n in [1usize, 2, 3, 5] {
1553 for selected in 0..n {
1554 let dom = Segmented::create(n_labels(n)).with_selected_index(selected).dom();
1555 let children = dom.children.as_ref();
1556 assert_eq!(children.len(), n);
1557
1558 for (i, child) in children.iter().enumerate() {
1559 let expected =
1560 properties(&build_segment_style(i == selected, i == 0, i + 1 == n));
1561 assert_eq!(
1562 inline_properties(child),
1563 expected,
1564 "n={n} selected={selected}: segment {i} carries the wrong style"
1565 );
1566 }
1567 }
1568 }
1569 }
1570
1571 #[test]
1572 fn dom_marks_exactly_one_segment_as_selected() {
1573 for n in [1usize, 2, 4] {
1574 for selected in 0..n {
1575 let dom = Segmented::create(n_labels(n)).with_selected_index(selected).dom();
1576 let marked: Vec<usize> = dom
1577 .children
1578 .as_ref()
1579 .iter()
1580 .enumerate()
1581 .filter(|(_, c)| {
1582 inline_properties(c).iter().any(|p| matches!(
1583 p, CssProperty::TextColor(t)
1584 if t.get_property().map(|t| t.inner) == Some(SEG_SELECTED_TEXT)))
1585 })
1586 .map(|(i, _)| i)
1587 .collect();
1588 assert_eq!(marked, vec![selected], "n={n}: mutual exclusivity broken");
1589 }
1590 }
1591 }
1592
1593 #[test]
1594 fn dom_with_an_out_of_range_selection_marks_nothing_and_does_not_panic() {
1595 let n = 3;
1598 for selected in [n, n + 1, 1_000, usize::MAX, usize::MAX - 1] {
1599 let dom = Segmented::create(n_labels(n)).with_selected_index(selected).dom();
1600 assert_eq!(dom.children.as_ref().len(), n, "selected={selected}: child count changed");
1601
1602 for (i, child) in dom.children.as_ref().iter().enumerate() {
1603 let expected = properties(&build_segment_style(false, i == 0, i + 1 == n));
1604 assert_eq!(
1605 inline_properties(child),
1606 expected,
1607 "selected={selected}: segment {i} must render unselected"
1608 );
1609 }
1610 }
1611 }
1612
1613 #[test]
1614 fn dom_rounds_only_the_two_outer_segments() {
1615 let n = 4;
1616 let dom = Segmented::create(n_labels(n)).dom();
1617 let r = SEG_RADIUS as f32;
1618
1619 let radii_of = |child: &Dom| -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
1620 let props = inline_properties(child);
1621 let find = |f: &dyn Fn(&CssProperty) -> Option<f32>| props.iter().find_map(f);
1622 (
1623 find(&|p| match p {
1624 CssProperty::BorderTopLeftRadius(x) => x.get_property().map(|x| px(&x.inner)),
1625 _ => None,
1626 }),
1627 find(&|p| match p {
1628 CssProperty::BorderTopRightRadius(x) => x.get_property().map(|x| px(&x.inner)),
1629 _ => None,
1630 }),
1631 find(&|p| match p {
1632 CssProperty::BorderBottomLeftRadius(x) => x.get_property().map(|x| px(&x.inner)),
1633 _ => None,
1634 }),
1635 find(&|p| match p {
1636 CssProperty::BorderBottomRightRadius(x) => {
1637 x.get_property().map(|x| px(&x.inner))
1638 }
1639 _ => None,
1640 }),
1641 )
1642 };
1643
1644 let children = dom.children.as_ref();
1645 assert_eq!(radii_of(&children[0]), (Some(r), None, Some(r), None), "first: left corners only");
1646 assert_eq!(radii_of(&children[1]), (None, None, None, None), "interior segments are square");
1647 assert_eq!(radii_of(&children[2]), (None, None, None, None), "interior segments are square");
1648 assert_eq!(
1649 radii_of(&children[3]),
1650 (None, Some(r), None, Some(r)),
1651 "last: right corners only"
1652 );
1653 }
1654
1655 #[test]
1656 fn dom_of_a_single_segment_is_rounded_on_both_ends() {
1657 let dom = Segmented::create(labels(&["only"])).dom();
1658 let children = dom.children.as_ref();
1659 assert_eq!(children.len(), 1);
1660
1661 let expected = properties(&build_segment_style(true, true, true));
1662 assert_eq!(
1663 inline_properties(&children[0]),
1664 expected,
1665 "a lone segment is simultaneously first and last"
1666 );
1667 }
1668
1669 #[test]
1670 fn dom_makes_every_segment_clickable_and_keyboard_reachable() {
1671 let n = 3;
1672 let dom = Segmented::create(n_labels(n)).dom();
1673 for (i, child) in dom.children.as_ref().iter().enumerate() {
1674 let cbs = child.root.get_callbacks();
1675 assert_eq!(cbs.as_ref().len(), 1, "segment {i}: exactly one handler");
1676 assert_eq!(cbs.as_ref()[0].event, EventFilter::Hover(HoverEventFilter::MouseUp));
1677 assert_eq!(cbs.as_ref()[0].callback.cb, on_segment_click as usize);
1678 assert!(matches!(cbs.as_ref()[0].callback.ctx, OptionRefAny::None));
1679 assert_eq!(
1680 child.root.get_tab_index(),
1681 Some(TabIndex::Auto),
1682 "segment {i} must be tab-reachable"
1683 );
1684 }
1685 }
1686
1687 #[test]
1688 fn dom_shares_one_state_refany_across_every_segment() {
1689 let dom = Segmented::create(n_labels(4)).dom();
1693
1694 let mut first = segment_state(&dom, 0);
1695 {
1696 let mut w = first
1697 .downcast_mut::<SegmentedStateWrapper>()
1698 .expect("segment state must be a SegmentedStateWrapper");
1699 w.inner.selected_index = 3;
1700 }
1701
1702 for i in 1..4 {
1703 let mut other = segment_state(&dom, i);
1704 assert_eq!(
1705 selected_index_of(&mut other),
1706 3,
1707 "segment {i} does not share segment 0's state"
1708 );
1709 }
1710 }
1711
1712 #[test]
1713 fn dom_carries_the_installed_callback_into_the_shared_state() {
1714 let dom = Segmented::create(labels(&["a", "b"]))
1715 .with_on_change(RefAny::new(0u8), change_cb(change_refresh_all))
1716 .dom();
1717 let mut state = segment_state(&dom, 0);
1718 let wrapper =
1719 state.downcast_ref::<SegmentedStateWrapper>().expect("SegmentedStateWrapper");
1720 let installed = wrapper.on_change.as_ref().expect("the user callback must reach the DOM");
1721 assert_eq!(installed.callback.cb as usize, change_refresh_all as usize);
1722 }
1723
1724 #[test]
1725 fn dom_preserves_adversarial_labels_verbatim() {
1726 for s in adversarial_strings() {
1727 let dom = Segmented::create(labels(&[s.as_str(), "other"])).dom();
1728 let children = dom.children.as_ref();
1729 assert_eq!(children.len(), 2);
1730 match children[0].root.get_node_type() {
1731 NodeType::Text(t) => {
1732 assert_eq!(t.as_ref().as_str(), s.as_str(), "the caption changed inside dom()");
1733 assert_eq!(t.as_ref().len(), s.len(), "byte length changed (NUL truncation?)");
1734 }
1735 other => panic!("expected a text node, got {other:?}"),
1736 }
1737 }
1738 }
1739
1740 #[test]
1741 fn dom_keeps_estimated_total_children_in_sync() {
1742 for n in [0usize, 1, 2, 3, 5, 64, 257] {
1745 let dom = Segmented::create(n_labels(n)).dom();
1746 assert_eq!(dom.children.as_ref().len(), n, "child count for n={n}");
1747 assert_eq!(
1748 dom.estimated_total_children,
1749 recursive_descendants(&dom),
1750 "cached descendant count desynced for n={n}"
1751 );
1752 assert_eq!(dom.estimated_total_children, n, "for n={n}");
1753 }
1754 }
1755
1756 #[test]
1757 fn dom_of_many_segments_flattens_without_panicking() {
1758 let n = 512;
1759 let styled = StyledDom::create_from_dom(Segmented::create(n_labels(n)).dom());
1760 assert_eq!(styled.node_hierarchy.as_ref().len(), n + 1, "root + n segments");
1761 }
1762
1763 #[test]
1764 fn dom_via_from_matches_dom_exactly() {
1765 let build = || Segmented::create(n_labels(3)).with_selected_index(1);
1766 let via_into: Dom = build().into();
1767 let via_dom = build().dom();
1768
1769 assert_eq!(via_into.children.as_ref().len(), via_dom.children.as_ref().len());
1770 assert_eq!(
1771 via_into.estimated_total_children,
1772 via_dom.estimated_total_children
1773 );
1774 for i in 0..via_dom.children.as_ref().len() {
1775 assert_eq!(
1776 inline_properties(&via_into.children.as_ref()[i]),
1777 inline_properties(&via_dom.children.as_ref()[i]),
1778 "`From` diverges from `dom()` at segment {i}"
1779 );
1780 assert_eq!(
1781 text_of(&via_into.children.as_ref()[i]),
1782 text_of(&via_dom.children.as_ref()[i])
1783 );
1784 }
1785 }
1786
1787 #[test]
1788 fn dom_with_duplicate_labels_still_produces_distinct_positional_segments() {
1789 let dom = Segmented::create(labels(&["same", "same", "same"])).with_selected_index(1).dom();
1792 let children = dom.children.as_ref();
1793 for (i, child) in children.iter().enumerate() {
1794 assert_eq!(text_of(child), Some("same"));
1795 let expected = properties(&build_segment_style(i == 1, i == 0, i == 2));
1796 assert_eq!(inline_properties(child), expected, "segment {i}");
1797 }
1798 }
1799
1800 #[test]
1805 fn click_selects_the_segment_at_the_clicked_position() {
1806 let n = 4;
1807 let (styled, state) = flatten(Segmented::create(n_labels(n)));
1808 assert_eq!(styled.node_hierarchy.as_ref().len(), n + 1, "fixture: root + n segments");
1809
1810 for i in 0..n {
1811 let mut state = state.clone();
1812 let (update, changes) = run_click(Some(styled.clone()), i + 1, state.clone());
1813
1814 assert_eq!(
1815 update,
1816 Update::DoNothing,
1817 "with no on_change installed the handler reports nothing to redraw"
1818 );
1819 assert_eq!(selected_index_of(&mut state), i, "node {} must select segment {i}", i + 1);
1820 assert_eq!(restyle_writes(&changes).len(), 2 * n, "every segment must be restyled");
1821 }
1822 }
1823
1824 #[test]
1825 fn click_restyle_agrees_with_a_freshly_built_style() {
1826 let n = 4;
1829 let (styled, state) = flatten(Segmented::create(n_labels(n)));
1830
1831 for clicked in 0..n {
1832 let (_, changes) = run_click(Some(styled.clone()), clicked + 1, state.clone());
1833 let writes = restyle_writes(&changes);
1834 assert_eq!(writes.len(), 2 * n);
1835
1836 for i in 0..n {
1837 let fresh = build_segment_style(i == clicked, i == 0, i + 1 == n);
1838 assert_eq!(
1839 writes[2 * i],
1840 (i + 1, "bg", background_color(&fresh).expect("background")),
1841 "clicked={clicked}: segment {i} background"
1842 );
1843 assert_eq!(
1844 writes[2 * i + 1],
1845 (i + 1, "text", text_color(&fresh).expect("text colour")),
1846 "clicked={clicked}: segment {i} text colour"
1847 );
1848 }
1849 }
1850 }
1851
1852 #[test]
1853 fn click_invokes_the_user_callback_with_the_updated_state() {
1854 let mut log = RefAny::new(IndexLog { seen: Vec::new() });
1855 let seg = Segmented::create(n_labels(4))
1856 .with_on_change(log.clone(), change_cb(record_index));
1857 let (styled, state) = flatten(seg);
1858
1859 let (update, changes) = run_click(Some(styled.clone()), 3, state.clone());
1860 assert_eq!(update, Update::RefreshDom, "the user's Update must propagate");
1861 assert_eq!(log_indices(&mut log), vec![2], "the callback sees the *new* index");
1862 assert_eq!(restyle_writes(&changes).len(), 8, "the restyle must still run");
1863
1864 let (_, _) = run_click(Some(styled), 1, state.clone());
1866 assert_eq!(log_indices(&mut log), vec![2, 0]);
1867
1868 let mut state = state;
1869 assert_eq!(selected_index_of(&mut state), 0, "the state holds the *last* clicked index");
1870 }
1871
1872 #[test]
1873 fn click_propagates_every_update_variant_unchanged() {
1874 for (cb, expected) in [
1875 (change_cb(change_do_nothing), Update::DoNothing),
1876 (change_cb(change_refresh_all), Update::RefreshDomAllWindows),
1877 ] {
1878 let seg =
1879 Segmented::create(labels(&["a", "b"])).with_on_change(RefAny::new(0u8), cb);
1880 let (styled, state) = flatten(seg);
1881 let (update, changes) = run_click(Some(styled), 2, state);
1882 assert_eq!(update, expected);
1883 assert_eq!(
1884 restyle_writes(&changes).len(),
1885 4,
1886 "the restyle runs regardless of what the user returns"
1887 );
1888 }
1889 }
1890
1891 #[test]
1892 fn click_restyles_even_without_a_user_callback() {
1893 let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
1894 let (update, changes) = run_click(Some(styled), 1, state);
1895
1896 assert_eq!(update, Update::DoNothing);
1897 assert_eq!(
1898 restyle_writes(&changes),
1899 vec![
1900 (1, "bg", SEG_SELECTED_BG_COLOR),
1901 (1, "text", SEG_SELECTED_TEXT),
1902 (2, "bg", SEG_UNSELECTED_BG_COLOR),
1903 (2, "text", SEG_UNSELECTED_TEXT),
1904 ],
1905 "selection feedback must not depend on the user wiring a callback"
1906 );
1907 }
1908
1909 #[test]
1910 fn click_on_a_single_segment_control_stays_at_zero() {
1911 let (styled, state) = flatten(Segmented::create(labels(&["only"])));
1912 let mut probe = state.clone();
1913 let (update, changes) = run_click(Some(styled), 1, state);
1914
1915 assert_eq!(update, Update::DoNothing);
1916 assert_eq!(selected_index_of(&mut probe), 0);
1917 assert_eq!(
1918 restyle_writes(&changes),
1919 vec![(1, "bg", SEG_SELECTED_BG_COLOR), (1, "text", SEG_SELECTED_TEXT)]
1920 );
1921 }
1922
1923 #[test]
1924 fn click_on_the_root_node_does_nothing() {
1925 let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
1927 let mut probe = state.clone();
1928
1929 let (update, changes) = run_click(Some(styled), 0, state);
1930
1931 assert_eq!(update, Update::DoNothing);
1932 assert!(changes.is_empty(), "nothing may be restyled when the click is not on a segment");
1933 assert_eq!(selected_index_of(&mut probe), 0, "state must be untouched");
1934 }
1935
1936 #[test]
1937 fn click_on_an_out_of_range_node_does_nothing() {
1938 let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
1939 let mut probe = state.clone();
1940
1941 let (update, changes) = run_click(Some(styled), 9999, state);
1942
1943 assert_eq!(update, Update::DoNothing, "a hit node outside the tree must not panic");
1944 assert!(changes.is_empty());
1945 assert_eq!(selected_index_of(&mut probe), 0);
1946 }
1947
1948 #[test]
1949 fn click_with_no_layout_result_does_nothing() {
1950 let dom = Segmented::create(labels(&["a", "b"])).dom();
1951 let state = segment_state(&dom, 0);
1952
1953 let (update, changes) = run_click(None, 1, state);
1954
1955 assert_eq!(update, Update::DoNothing, "an empty LayoutWindow must be handled, not unwrapped");
1956 assert!(changes.is_empty());
1957 }
1958
1959 #[test]
1960 fn click_with_a_foreign_payload_does_nothing() {
1961 let (styled, _) = flatten(Segmented::create(labels(&["a", "b"])));
1965 let (update, changes) = run_click(Some(styled), 1, RefAny::new(0u32));
1966
1967 assert_eq!(update, Update::DoNothing);
1968 assert!(changes.is_empty(), "a failed downcast must not leave a half-applied restyle");
1969 }
1970
1971 #[test]
1972 fn click_with_the_state_already_borrowed_does_nothing() {
1973 let (styled, state) = flatten(Segmented::create(labels(&["a", "b"])));
1974
1975 let mut held = state.clone();
1978 let guard = held.downcast_mut::<SegmentedStateWrapper>().expect("first borrow succeeds");
1979
1980 let (update, changes) = run_click(Some(styled), 1, state);
1981
1982 assert_eq!(update, Update::DoNothing);
1983 assert!(changes.is_empty());
1984 drop(guard);
1985 }
1986
1987 #[test]
1988 fn click_holds_the_state_borrow_across_the_user_callback() {
1989 let mut probe = RefAny::new(ReentrantProbe {
1997 state: RefAny::new(0u8),
1998 saw_index: Some(usize::MAX),
1999 calls: 0,
2000 });
2001 let state = RefAny::new(SegmentedStateWrapper {
2002 inner: SegmentedState { selected_index: 0 },
2003 on_change: Some(SegmentedOnChange {
2004 callback: change_cb(probe_state_reentrantly),
2005 refany: probe.clone(),
2006 })
2007 .into(),
2008 });
2009 {
2010 let mut p = probe.downcast_mut::<ReentrantProbe>().expect("ReentrantProbe");
2011 p.state = state.clone();
2012 }
2013
2014 let styled = StyledDom::create_from_dom(Segmented::create(labels(&["a", "b"])).dom());
2015 let (update, changes) = run_click(Some(styled), 2, state.clone());
2016
2017 assert_eq!(update, Update::DoNothing);
2018 assert_eq!(restyle_writes(&changes).len(), 4, "the restyle must still run afterwards");
2019
2020 let p = probe.downcast_ref::<ReentrantProbe>().expect("ReentrantProbe");
2021 assert_eq!(p.calls, 1, "the user callback must have run exactly once");
2022 assert_eq!(p.saw_index, None, "a re-entrant read of the state must be refused, not aliased");
2023 }
2024
2025 #[test]
2026 fn click_indices_stay_within_the_label_count() {
2027 let n = 128;
2030 let (styled, state) = flatten(Segmented::create(n_labels(n)));
2031
2032 for hit in [1usize, 2, n / 2, n - 1, n] {
2033 let mut probe = state.clone();
2034 let (_, changes) = run_click(Some(styled.clone()), hit, state.clone());
2035 let idx = selected_index_of(&mut probe);
2036 assert_eq!(idx, hit - 1, "node {hit} sits at sibling position {}", hit - 1);
2037 assert!(idx < n, "the reported index must always address a real label");
2038 assert_eq!(restyle_writes(&changes).len(), 2 * n);
2039 }
2040 }
2041
2042 #[test]
2043 fn click_recovers_a_state_left_out_of_range_by_the_setter() {
2044 let seg = Segmented::create(n_labels(3)).with_selected_index(usize::MAX);
2047 let (styled, state) = flatten(seg);
2048 let mut probe = state.clone();
2049
2050 let (_, changes) = run_click(Some(styled), 2, state);
2051
2052 assert_eq!(selected_index_of(&mut probe), 1);
2053 assert_eq!(
2054 restyle_writes(&changes),
2055 vec![
2056 (1, "bg", SEG_UNSELECTED_BG_COLOR),
2057 (1, "text", SEG_UNSELECTED_TEXT),
2058 (2, "bg", SEG_SELECTED_BG_COLOR),
2059 (2, "text", SEG_SELECTED_TEXT),
2060 (3, "bg", SEG_UNSELECTED_BG_COLOR),
2061 (3, "text", SEG_UNSELECTED_TEXT),
2062 ]
2063 );
2064 }
2065}