1#![warn(missing_docs)]
87
88use std::panic::Location;
89
90use crate::event::{KeyModifiers, NamedKey, UiEvent, UiEventKind};
91use crate::selection::Selection;
92use crate::tokens;
93use crate::tree::*;
94use crate::widgets::button::{button, icon_button};
95use crate::widgets::text_input::{
96 TextInputOpts, apply_event_with as text_input_apply, text_input_with,
97};
98
99#[derive(Clone, Copy, Debug)]
108pub struct NumericInputOpts<'a> {
109 pub min: Option<f64>,
112 pub max: Option<f64>,
115 pub step: f64,
117 pub decimals: Option<u8>,
122 pub placeholder: Option<&'a str>,
124 pub stacked: bool,
131}
132
133impl Default for NumericInputOpts<'_> {
134 fn default() -> Self {
135 Self {
136 min: None,
137 max: None,
138 step: 1.0,
139 decimals: None,
140 placeholder: None,
141 stacked: false,
142 }
143 }
144}
145
146impl<'a> NumericInputOpts<'a> {
147 pub fn min(mut self, v: f64) -> Self {
149 self.min = Some(v);
150 self
151 }
152 pub fn max(mut self, v: f64) -> Self {
154 self.max = Some(v);
155 self
156 }
157 pub fn step(mut self, v: f64) -> Self {
159 self.step = v;
160 self
161 }
162 pub fn decimals(mut self, v: u8) -> Self {
165 self.decimals = Some(v);
166 self
167 }
168 pub fn placeholder(mut self, p: &'a str) -> Self {
170 self.placeholder = Some(p);
171 self
172 }
173 pub fn stacked(mut self) -> Self {
176 self.stacked = true;
177 self
178 }
179}
180
181#[track_caller]
190pub fn numeric_input(
191 key: &str,
192 value: &str,
193 selection: &Selection,
194 opts: NumericInputOpts<'_>,
195) -> El {
196 let caller = Location::caller();
197
198 let mut text_opts = TextInputOpts::default();
199 if let Some(p) = opts.placeholder {
200 text_opts = text_opts.placeholder(p);
201 }
202 let field_key = format!("{key}:field");
203 let field = text_input_with(&field_key, value, selection, text_opts).width(Size::Fill(1.0));
204
205 let children: Vec<El> = if opts.stacked {
216 vec![field, stacked_chevron_column(key, caller)]
217 } else {
218 let dec = button("−")
219 .at_loc(caller)
220 .key(format!("{key}:dec"))
221 .ghost()
222 .width(Size::Fixed(tokens::CONTROL_HEIGHT))
223 .height(Size::Fixed(tokens::CONTROL_HEIGHT));
224 let inc = button("+")
225 .at_loc(caller)
226 .key(format!("{key}:inc"))
227 .ghost()
228 .width(Size::Fixed(tokens::CONTROL_HEIGHT))
229 .height(Size::Fixed(tokens::CONTROL_HEIGHT));
230 vec![dec, field, inc]
231 };
232
233 row(children)
234 .at_loc(caller)
235 .key(key.to_string())
236 .gap(tokens::RING_WIDTH)
237 .align(Align::Center)
238 .default_width(Size::Fixed(DEFAULT_WIDTH))
239 .default_height(Size::Fixed(tokens::CONTROL_HEIGHT))
240}
241
242const STACKED_CHEVRON_WIDTH: f32 = 22.0;
246
247pub const DEFAULT_WIDTH: f32 = 144.0;
260
261fn stacked_chevron_column(key: &str, caller: &'static Location<'static>) -> El {
273 let half_h = (tokens::CONTROL_HEIGHT * 0.5).floor();
274 let inc = icon_button("chevron-up")
275 .at_loc(caller)
276 .key(format!("{key}:inc"))
277 .ghost()
278 .icon_size(tokens::ICON_XS)
279 .focus_ring_inside()
280 .width(Size::Fixed(STACKED_CHEVRON_WIDTH))
281 .height(Size::Fixed(half_h));
282 let dec = icon_button("chevron-down")
283 .at_loc(caller)
284 .key(format!("{key}:dec"))
285 .ghost()
286 .icon_size(tokens::ICON_XS)
287 .focus_ring_inside()
288 .width(Size::Fixed(STACKED_CHEVRON_WIDTH))
289 .height(Size::Fixed(half_h));
290 column([inc, dec])
291 .at_loc(caller)
292 .gap(0.0)
293 .width(Size::Fixed(STACKED_CHEVRON_WIDTH))
294 .height(Size::Fixed(tokens::CONTROL_HEIGHT))
295}
296
297pub fn apply_event(
309 value: &mut String,
310 selection: &mut Selection,
311 key: &str,
312 opts: &NumericInputOpts<'_>,
313 event: &UiEvent,
314) -> bool {
315 if matches!(event.kind, UiEventKind::Click | UiEventKind::Activate) {
316 let inc_key = format!("{key}:inc");
317 let dec_key = format!("{key}:dec");
318 if event.route() == Some(inc_key.as_str()) {
319 step_value(value, opts, 1, event.modifiers);
320 return true;
321 }
322 if event.route() == Some(dec_key.as_str()) {
323 step_value(value, opts, -1, event.modifiers);
324 return true;
325 }
326 }
327
328 let field_key = format!("{key}:field");
329
330 if event.kind == UiEventKind::KeyDown
336 && event.is_route(&field_key)
337 && let Some(kp) = event.key_press.as_ref()
338 {
339 let dir = match kp.logical.named() {
340 Some(NamedKey::ArrowUp) => Some(1),
341 Some(NamedKey::ArrowDown) => Some(-1),
342 _ => None,
343 };
344 if let Some(d) = dir {
345 step_value(value, opts, d, kp.modifiers);
346 return true;
347 }
348 }
349
350 if event.target_key() != Some(field_key.as_str()) {
356 return false;
357 }
358
359 let text_opts = match opts.placeholder {
360 Some(p) => TextInputOpts::default().placeholder(p),
361 None => TextInputOpts::default(),
362 };
363
364 let prev_value = value.clone();
370 let prev_selection = selection.clone();
371 let changed = text_input_apply(value, selection, event, &field_key, &text_opts);
372 if changed && !is_acceptable_numeric_progress(value) {
373 *value = prev_value;
374 *selection = prev_selection;
375 return true;
378 }
379 changed
380}
381
382fn is_acceptable_numeric_progress(s: &str) -> bool {
383 s.is_empty()
384 || s.chars()
385 .all(|c| matches!(c, '0'..='9' | '.' | 'e' | 'E' | '+' | '-'))
386}
387
388fn step_value(value: &mut String, opts: &NumericInputOpts<'_>, dir: i32, mods: KeyModifiers) {
389 let parsed = value
393 .parse::<f64>()
394 .ok()
395 .unwrap_or_else(|| opts.min.unwrap_or(0.0));
396 let stepped = parsed + (dir as f64) * opts.step * step_scale(mods);
397 let clamped = clamp_opt(stepped, opts.min, opts.max);
398 *value = format_numeric(clamped, opts.decimals);
399}
400
401fn step_scale(mods: KeyModifiers) -> f64 {
406 if mods.shift {
407 10.0
408 } else if mods.alt {
409 0.1
410 } else {
411 1.0
412 }
413}
414
415fn clamp_opt(n: f64, min: Option<f64>, max: Option<f64>) -> f64 {
416 let n = if let Some(hi) = max { n.min(hi) } else { n };
417 if let Some(lo) = min { n.max(lo) } else { n }
418}
419
420fn format_numeric(n: f64, decimals: Option<u8>) -> String {
421 match decimals {
422 Some(d) => format!("{:.*}", d as usize, n),
423 None if n.fract() == 0.0 && n.is_finite() && n.abs() < 1e18 => {
424 format!("{}", n as i64)
428 }
429 None => format!("{n}"),
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use crate::event::{KeyModifiers, LogicalKey, PhysicalKey, UiTarget};
437 use crate::layout::layout;
438 use crate::state::UiState;
439 use crate::tree::Rect;
440
441 fn click(key: &str) -> UiEvent {
442 UiEvent::synthetic_click(key)
443 }
444
445 #[test]
446 fn default_is_fixed_width_with_inner_field_filling() {
447 let value = String::from("42");
455 let sel = Selection::default();
456 let widget = numeric_input("n", &value, &sel, NumericInputOpts::default());
457 let mut tree = crate::widgets::form::form([crate::widgets::form::form_item([
458 crate::widgets::form::form_control(widget),
459 ])]);
460 let mut state = UiState::new();
461 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 320.0, 200.0));
462
463 let row_rect = state.rect_of_key("n").expect("row rect");
464 let field_rect = state.rect_of_key("n:field").expect("field rect");
465 assert_eq!(
466 row_rect.w, DEFAULT_WIDTH,
467 "row should keep its fixed default width inside a wide form parent"
468 );
469 let expected_field_w =
472 DEFAULT_WIDTH - 2.0 * tokens::CONTROL_HEIGHT - 2.0 * tokens::RING_WIDTH;
473 assert!(
474 (field_rect.w - expected_field_w).abs() < 0.5,
475 "field should take leftover space inside wrapper, got {} expected ~{}",
476 field_rect.w,
477 expected_field_w,
478 );
479 }
480
481 #[test]
482 fn explicit_width_fill_still_works() {
483 let value = String::from("42");
487 let sel = Selection::default();
488 let widget =
489 numeric_input("n", &value, &sel, NumericInputOpts::default()).width(Size::Fill(1.0));
490 let mut tree = crate::widgets::form::form([crate::widgets::form::form_item([
491 crate::widgets::form::form_control(widget),
492 ])]);
493 let mut state = UiState::new();
494 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 320.0, 200.0));
495 let row_rect = state.rect_of_key("n").expect("row rect");
496 assert!(
497 row_rect.w > DEFAULT_WIDTH,
498 "explicit `.width(Fill)` should override the fixed default, got {}",
499 row_rect.w,
500 );
501 }
502
503 fn text_event(target_key: &str, text: &str) -> UiEvent {
507 UiEvent {
508 path: None,
509 key: Some(target_key.to_string()),
510 target: Some(UiTarget {
511 key: target_key.to_string(),
512 node_id: format!("/{target_key}").into(),
513 rect: Rect::new(0.0, 0.0, 100.0, 32.0),
514 tooltip: None,
515 scroll_offset_y: 0.0,
516 }),
517 pointer: None,
518 key_press: None,
519 text: Some(text.to_string()),
520 selection: None,
521 modifiers: KeyModifiers::default(),
522 click_count: 0,
523 pointer_kind: None,
524 wheel_delta: None,
525 kind: UiEventKind::TextInput,
526 }
527 }
528
529 #[test]
530 fn inc_steps_value_up_by_step() {
531 let mut value = String::from("3");
532 let mut sel = Selection::default();
533 let opts = NumericInputOpts::default().step(2.0);
534 assert!(apply_event(
535 &mut value,
536 &mut sel,
537 "n",
538 &opts,
539 &click("n:inc")
540 ));
541 assert_eq!(value, "5");
542 }
543
544 #[test]
545 fn dec_steps_value_down_by_step() {
546 let mut value = String::from("3");
547 let mut sel = Selection::default();
548 let opts = NumericInputOpts::default().step(0.5).decimals(1);
549 assert!(apply_event(
550 &mut value,
551 &mut sel,
552 "n",
553 &opts,
554 &click("n:dec")
555 ));
556 assert_eq!(value, "2.5");
557 }
558
559 #[test]
560 fn inc_clamps_to_max() {
561 let mut value = String::from("99");
562 let mut sel = Selection::default();
563 let opts = NumericInputOpts::default().min(0.0).max(100.0);
564 let opts = opts.step(5.0);
566 assert!(apply_event(
567 &mut value,
568 &mut sel,
569 "n",
570 &opts,
571 &click("n:inc")
572 ));
573 assert_eq!(value, "100");
574 }
575
576 #[test]
577 fn dec_clamps_to_min() {
578 let mut value = String::from("1");
579 let mut sel = Selection::default();
580 let opts = NumericInputOpts::default().min(0.0).max(100.0);
581 assert!(apply_event(
582 &mut value,
583 &mut sel,
584 "n",
585 &opts,
586 &click("n:dec")
587 ));
588 assert_eq!(value, "0");
589 assert!(apply_event(
591 &mut value,
592 &mut sel,
593 "n",
594 &opts,
595 &click("n:dec")
596 ));
597 assert_eq!(value, "0");
598 }
599
600 #[test]
601 fn empty_value_treated_as_min_when_set() {
602 let mut value = String::new();
603 let mut sel = Selection::default();
604 let opts = NumericInputOpts::default().min(10.0).max(100.0);
605 assert!(apply_event(
607 &mut value,
608 &mut sel,
609 "n",
610 &opts,
611 &click("n:inc")
612 ));
613 assert_eq!(value, "11");
614 }
615
616 #[test]
617 fn empty_value_treated_as_zero_when_no_min() {
618 let mut value = String::new();
619 let mut sel = Selection::default();
620 let opts = NumericInputOpts::default();
621 assert!(apply_event(
622 &mut value,
623 &mut sel,
624 "n",
625 &opts,
626 &click("n:inc")
627 ));
628 assert_eq!(value, "1");
629 }
630
631 #[test]
632 fn unparseable_value_treated_as_zero_when_no_min() {
633 let mut value = String::from("abc");
634 let mut sel = Selection::default();
635 let opts = NumericInputOpts::default();
636 assert!(apply_event(
637 &mut value,
638 &mut sel,
639 "n",
640 &opts,
641 &click("n:inc")
642 ));
643 assert_eq!(value, "1");
644 }
645
646 #[test]
647 fn ignores_unrelated_keys() {
648 let mut value = String::from("3");
649 let mut sel = Selection::default();
650 let opts = NumericInputOpts::default();
651 assert!(!apply_event(
653 &mut value,
654 &mut sel,
655 "n",
656 &opts,
657 &click("other:inc")
658 ));
659 assert_eq!(value, "3");
660 }
661
662 #[test]
663 fn decimals_format_pads_zeros() {
664 let mut value = String::from("0");
665 let mut sel = Selection::default();
666 let opts = NumericInputOpts::default().step(0.10).decimals(2);
667 assert!(apply_event(
668 &mut value,
669 &mut sel,
670 "n",
671 &opts,
672 &click("n:inc")
673 ));
674 assert_eq!(value, "0.10");
675 }
676
677 #[test]
678 fn no_decimals_strips_trailing_zero() {
679 let mut value = String::from("0");
680 let mut sel = Selection::default();
681 let opts = NumericInputOpts::default().step(1.0);
682 assert!(apply_event(
683 &mut value,
684 &mut sel,
685 "n",
686 &opts,
687 &click("n:inc")
688 ));
689 assert_eq!(value, "1");
692 }
693
694 #[test]
695 fn text_event_for_other_widget_is_ignored() {
696 let mut value = String::from("42");
701 let mut sel = Selection::default();
702 let opts = NumericInputOpts::default();
703 assert!(!apply_event(
706 &mut value,
707 &mut sel,
708 "n",
709 &opts,
710 &text_event("other-input", "x"),
711 ));
712 assert_eq!(value, "42");
713 }
714
715 #[test]
716 fn text_event_filter_rejects_non_numeric_chars() {
717 let mut value = String::from("12");
723 let mut sel = Selection::default();
724 let opts = NumericInputOpts::default();
725 assert!(apply_event(
726 &mut value,
727 &mut sel,
728 "n",
729 &opts,
730 &text_event("n:field", "abc"),
731 ));
732 assert_eq!(value, "12");
733 }
734
735 #[test]
736 fn text_event_filter_accepts_partial_numeric_states() {
737 for partial in ["-", "1.", "1.5e", "1.5e+", ".5", "+"] {
741 let mut value = String::new();
742 let mut sel = Selection::default();
743 let opts = NumericInputOpts::default();
744 assert!(
745 apply_event(
746 &mut value,
747 &mut sel,
748 "n",
749 &opts,
750 &text_event("n:field", partial),
751 ),
752 "filter should accept partial value {partial:?}",
753 );
754 assert_eq!(value, partial, "value should equal {partial:?}");
755 }
756 }
757
758 #[test]
759 fn text_event_filter_accepts_full_numeric_paste() {
760 let mut value = String::new();
761 let mut sel = Selection::default();
762 let opts = NumericInputOpts::default();
763 assert!(apply_event(
764 &mut value,
765 &mut sel,
766 "n",
767 &opts,
768 &text_event("n:field", "42.5"),
769 ));
770 assert_eq!(value, "42.5");
771 }
772
773 #[test]
774 fn build_widget_has_three_children_and_correct_keys() {
775 let value = String::from("0");
776 let sel = Selection::default();
777 let opts = NumericInputOpts::default();
778 let el = numeric_input("n", &value, &sel, opts);
779 assert_eq!(el.key.as_deref(), Some("n"));
780 assert_eq!(el.children.len(), 3, "decrement, field, increment");
781 assert_eq!(el.children[0].key.as_deref(), Some("n:dec"));
782 assert_eq!(el.children[1].key.as_deref(), Some("n:field"));
783 assert_eq!(el.children[2].key.as_deref(), Some("n:inc"));
784 }
785
786 fn key_event(key: &str, ui_key: LogicalKey, modifiers: KeyModifiers) -> UiEvent {
790 use crate::event::KeyPress;
791 UiEvent {
792 path: None,
793 key: Some(key.to_string()),
794 target: Some(UiTarget {
795 key: key.to_string(),
796 node_id: format!("/{key}").into(),
797 rect: Rect::new(0.0, 0.0, 100.0, 32.0),
798 tooltip: None,
799 scroll_offset_y: 0.0,
800 }),
801 pointer: None,
802 key_press: Some(KeyPress {
803 logical: ui_key,
804 physical: PhysicalKey::Unidentified,
805 modifiers,
806 repeat: false,
807 }),
808 text: None,
809 selection: None,
810 modifiers,
811 click_count: 0,
812 pointer_kind: None,
813 wheel_delta: None,
814 kind: UiEventKind::KeyDown,
815 }
816 }
817
818 #[test]
819 fn arrow_up_on_field_steps_up() {
820 let mut value = String::from("3");
821 let mut sel = Selection::default();
822 let opts = NumericInputOpts::default().step(1.0);
823 assert!(apply_event(
824 &mut value,
825 &mut sel,
826 "n",
827 &opts,
828 &key_event(
829 "n:field",
830 LogicalKey::Named(NamedKey::ArrowUp),
831 KeyModifiers::default()
832 ),
833 ));
834 assert_eq!(value, "4");
835 }
836
837 #[test]
838 fn arrow_down_on_field_steps_down() {
839 let mut value = String::from("3");
840 let mut sel = Selection::default();
841 let opts = NumericInputOpts::default().step(1.0);
842 assert!(apply_event(
843 &mut value,
844 &mut sel,
845 "n",
846 &opts,
847 &key_event(
848 "n:field",
849 LogicalKey::Named(NamedKey::ArrowDown),
850 KeyModifiers::default()
851 ),
852 ));
853 assert_eq!(value, "2");
854 }
855
856 #[test]
857 fn shift_arrow_steps_by_ten_times() {
858 let mut value = String::from("3");
859 let mut sel = Selection::default();
860 let opts = NumericInputOpts::default().step(1.0);
861 let shift = KeyModifiers {
862 shift: true,
863 ..KeyModifiers::default()
864 };
865 assert!(apply_event(
866 &mut value,
867 &mut sel,
868 "n",
869 &opts,
870 &key_event("n:field", LogicalKey::Named(NamedKey::ArrowUp), shift),
871 ));
872 assert_eq!(value, "13");
873 }
874
875 #[test]
876 fn alt_arrow_steps_by_one_tenth() {
877 let mut value = String::from("0");
880 let mut sel = Selection::default();
881 let opts = NumericInputOpts::default().step(0.1).decimals(2);
882 let alt = KeyModifiers {
883 alt: true,
884 ..KeyModifiers::default()
885 };
886 assert!(apply_event(
887 &mut value,
888 &mut sel,
889 "n",
890 &opts,
891 &key_event("n:field", LogicalKey::Named(NamedKey::ArrowUp), alt),
892 ));
893 assert_eq!(value, "0.01");
894 }
895
896 #[test]
897 fn shift_click_on_inc_button_scales_step() {
898 let mut value = String::from("3");
901 let mut sel = Selection::default();
902 let opts = NumericInputOpts::default().step(1.0);
903 let mut ev = click("n:inc");
904 ev.modifiers = KeyModifiers {
905 shift: true,
906 ..KeyModifiers::default()
907 };
908 assert!(apply_event(&mut value, &mut sel, "n", &opts, &ev));
909 assert_eq!(value, "13");
910 }
911
912 #[test]
913 fn arrow_key_on_field_clamps_to_max() {
914 let mut value = String::from("99");
915 let mut sel = Selection::default();
916 let opts = NumericInputOpts::default().step(5.0).max(100.0);
917 assert!(apply_event(
918 &mut value,
919 &mut sel,
920 "n",
921 &opts,
922 &key_event(
923 "n:field",
924 LogicalKey::Named(NamedKey::ArrowUp),
925 KeyModifiers::default()
926 ),
927 ));
928 assert_eq!(value, "100");
929 }
930
931 #[test]
932 fn arrow_key_routed_elsewhere_is_ignored() {
933 let mut value = String::from("3");
937 let mut sel = Selection::default();
938 let opts = NumericInputOpts::default();
939 assert!(!apply_event(
940 &mut value,
941 &mut sel,
942 "n",
943 &opts,
944 &key_event(
945 "other:field",
946 LogicalKey::Named(NamedKey::ArrowUp),
947 KeyModifiers::default()
948 ),
949 ));
950 assert_eq!(value, "3");
951 }
952
953 #[test]
954 fn non_arrow_keydown_on_field_falls_through() {
955 let mut value = String::from("3");
959 let mut sel = Selection::default();
960 let opts = NumericInputOpts::default();
961 assert!(!apply_event(
962 &mut value,
963 &mut sel,
964 "n",
965 &opts,
966 &key_event(
967 "n:field",
968 LogicalKey::Named(NamedKey::Tab),
969 KeyModifiers::default()
970 ),
971 ));
972 assert_eq!(value, "3");
973 }
974
975 #[test]
976 fn stacked_variant_has_field_and_chevron_column() {
977 let value = String::from("0");
978 let sel = Selection::default();
979 let opts = NumericInputOpts::default().stacked();
980 let el = numeric_input("n", &value, &sel, opts);
981 assert_eq!(el.key.as_deref(), Some("n"));
982 assert_eq!(el.children.len(), 2, "field + chevron column");
986 assert_eq!(el.children[0].key.as_deref(), Some("n:field"));
987 let column_children = &el.children[1].children;
988 assert_eq!(column_children.len(), 2, "chevron-up over chevron-down");
989 assert_eq!(column_children[0].key.as_deref(), Some("n:inc"));
990 assert_eq!(column_children[1].key.as_deref(), Some("n:dec"));
991 }
992
993 #[test]
994 fn stacked_variant_keeps_apply_event_contract() {
995 let mut value = String::from("3");
998 let mut sel = Selection::default();
999 let opts = NumericInputOpts::default().stacked();
1000 assert!(apply_event(
1001 &mut value,
1002 &mut sel,
1003 "n",
1004 &opts,
1005 &click("n:inc"),
1006 ));
1007 assert_eq!(value, "4");
1008 assert!(apply_event(
1009 &mut value,
1010 &mut sel,
1011 "n",
1012 &opts,
1013 &key_event(
1014 "n:field",
1015 LogicalKey::Named(NamedKey::ArrowDown),
1016 KeyModifiers::default()
1017 ),
1018 ));
1019 assert_eq!(value, "3");
1020 }
1021
1022 #[test]
1027 fn spinner_glyphs_render_whole_not_ellipsized() {
1028 use crate::draw_ops::draw_ops;
1029 use crate::ir::DrawOp;
1030 use crate::layout::layout;
1031 use crate::state::UiState;
1032 use crate::tree::Rect;
1033
1034 let sel = Selection::default();
1035 let mut tree = numeric_input("n", "3", &sel, NumericInputOpts::default());
1036 let mut state = UiState::new();
1037 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 240.0, 48.0));
1038 let ops = draw_ops(&tree, &state);
1039 let labels: Vec<&str> = ops
1040 .iter()
1041 .filter_map(|op| match op {
1042 DrawOp::GlyphRun { text, .. } => Some(text.as_str()),
1043 _ => None,
1044 })
1045 .collect();
1046 assert!(labels.contains(&"−"), "decrement glyph intact: {labels:?}");
1047 assert!(labels.contains(&"+"), "increment glyph intact: {labels:?}");
1048 assert!(
1049 !labels.iter().any(|l| l.contains('\u{2026}')),
1050 "nothing ellipsized at natural size: {labels:?}"
1051 );
1052 }
1053}