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().tabular_numerals();
201 if let Some(p) = opts.placeholder {
202 text_opts = text_opts.placeholder(p);
203 }
204 let field_key = format!("{key}:field");
205 let field = text_input_with(&field_key, value, selection, text_opts).width(Size::Fill(1.0));
206
207 let children: Vec<El> = if opts.stacked {
218 vec![field, stacked_chevron_column(key, caller)]
219 } else {
220 let dec = button("−")
221 .at_loc(caller)
222 .key(format!("{key}:dec"))
223 .ghost()
224 .width(Size::Fixed(tokens::CONTROL_HEIGHT))
225 .height(Size::Fixed(tokens::CONTROL_HEIGHT));
226 let inc = button("+")
227 .at_loc(caller)
228 .key(format!("{key}:inc"))
229 .ghost()
230 .width(Size::Fixed(tokens::CONTROL_HEIGHT))
231 .height(Size::Fixed(tokens::CONTROL_HEIGHT));
232 vec![dec, field, inc]
233 };
234
235 row(children)
236 .at_loc(caller)
237 .key(key.to_string())
238 .gap(tokens::RING_WIDTH)
239 .align(Align::Center)
240 .default_width(Size::Fixed(DEFAULT_WIDTH))
241 .default_height(Size::Fixed(tokens::CONTROL_HEIGHT))
242}
243
244const STACKED_CHEVRON_WIDTH: f32 = 22.0;
248
249pub const DEFAULT_WIDTH: f32 = 144.0;
262
263fn stacked_chevron_column(key: &str, caller: &'static Location<'static>) -> El {
275 let half_h = (tokens::CONTROL_HEIGHT * 0.5).floor();
276 let inc = icon_button("chevron-up")
277 .at_loc(caller)
278 .key(format!("{key}:inc"))
279 .ghost()
280 .icon_size(tokens::ICON_XS)
281 .focus_ring_inside()
282 .width(Size::Fixed(STACKED_CHEVRON_WIDTH))
283 .height(Size::Fixed(half_h));
284 let dec = icon_button("chevron-down")
285 .at_loc(caller)
286 .key(format!("{key}:dec"))
287 .ghost()
288 .icon_size(tokens::ICON_XS)
289 .focus_ring_inside()
290 .width(Size::Fixed(STACKED_CHEVRON_WIDTH))
291 .height(Size::Fixed(half_h));
292 column([inc, dec])
293 .at_loc(caller)
294 .gap(0.0)
295 .width(Size::Fixed(STACKED_CHEVRON_WIDTH))
296 .height(Size::Fixed(tokens::CONTROL_HEIGHT))
297}
298
299pub fn apply_event(
311 value: &mut String,
312 selection: &mut Selection,
313 key: &str,
314 opts: &NumericInputOpts<'_>,
315 event: &UiEvent,
316) -> bool {
317 if matches!(event.kind, UiEventKind::Click | UiEventKind::Activate) {
318 let inc_key = format!("{key}:inc");
319 let dec_key = format!("{key}:dec");
320 if event.route() == Some(inc_key.as_str()) {
321 step_value(value, opts, 1, event.modifiers);
322 return true;
323 }
324 if event.route() == Some(dec_key.as_str()) {
325 step_value(value, opts, -1, event.modifiers);
326 return true;
327 }
328 }
329
330 let field_key = format!("{key}:field");
331
332 if event.kind == UiEventKind::KeyDown
338 && event.is_route(&field_key)
339 && let Some(kp) = event.key_press.as_ref()
340 {
341 let dir = match kp.logical.named() {
342 Some(NamedKey::ArrowUp) => Some(1),
343 Some(NamedKey::ArrowDown) => Some(-1),
344 _ => None,
345 };
346 if let Some(d) = dir {
347 step_value(value, opts, d, kp.modifiers);
348 return true;
349 }
350 }
351
352 if event.target_key() != Some(field_key.as_str()) {
358 return false;
359 }
360
361 let mut text_opts = TextInputOpts::default().tabular_numerals();
364 if let Some(p) = opts.placeholder {
365 text_opts = text_opts.placeholder(p);
366 }
367
368 let prev_value = value.clone();
374 let prev_selection = selection.clone();
375 let changed = text_input_apply(value, selection, event, &field_key, &text_opts);
376 if changed && !is_acceptable_numeric_progress(value) {
377 *value = prev_value;
378 *selection = prev_selection;
379 return true;
382 }
383 changed
384}
385
386fn is_acceptable_numeric_progress(s: &str) -> bool {
387 s.is_empty()
388 || s.chars()
389 .all(|c| matches!(c, '0'..='9' | '.' | 'e' | 'E' | '+' | '-'))
390}
391
392fn step_value(value: &mut String, opts: &NumericInputOpts<'_>, dir: i32, mods: KeyModifiers) {
393 let parsed = value
397 .parse::<f64>()
398 .ok()
399 .unwrap_or_else(|| opts.min.unwrap_or(0.0));
400 let stepped = parsed + (dir as f64) * opts.step * step_scale(mods);
401 let clamped = clamp_opt(stepped, opts.min, opts.max);
402 *value = format_numeric(clamped, opts.decimals);
403}
404
405fn step_scale(mods: KeyModifiers) -> f64 {
410 if mods.shift {
411 10.0
412 } else if mods.alt {
413 0.1
414 } else {
415 1.0
416 }
417}
418
419fn clamp_opt(n: f64, min: Option<f64>, max: Option<f64>) -> f64 {
420 let n = if let Some(hi) = max { n.min(hi) } else { n };
421 if let Some(lo) = min { n.max(lo) } else { n }
422}
423
424fn format_numeric(n: f64, decimals: Option<u8>) -> String {
425 match decimals {
426 Some(d) => format!("{:.*}", d as usize, n),
427 None if n.fract() == 0.0 && n.is_finite() && n.abs() < 1e18 => {
428 format!("{}", n as i64)
432 }
433 None => format!("{n}"),
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::event::{KeyModifiers, LogicalKey, PhysicalKey, UiTarget};
441 use crate::layout::layout;
442 use crate::state::UiState;
443 use crate::tree::Rect;
444
445 fn click(key: &str) -> UiEvent {
446 UiEvent::synthetic_click(key)
447 }
448
449 #[test]
450 fn default_is_fixed_width_with_inner_field_filling() {
451 let value = String::from("42");
459 let sel = Selection::default();
460 let widget = numeric_input("n", &value, &sel, NumericInputOpts::default());
461 let mut tree = crate::widgets::form::form([crate::widgets::form::form_item([
462 crate::widgets::form::form_control(widget),
463 ])]);
464 let mut state = UiState::new();
465 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 320.0, 200.0));
466
467 let row_rect = state.rect_of_key("n").expect("row rect");
468 let field_rect = state.rect_of_key("n:field").expect("field rect");
469 assert_eq!(
470 row_rect.w, DEFAULT_WIDTH,
471 "row should keep its fixed default width inside a wide form parent"
472 );
473 let expected_field_w =
476 DEFAULT_WIDTH - 2.0 * tokens::CONTROL_HEIGHT - 2.0 * tokens::RING_WIDTH;
477 assert!(
478 (field_rect.w - expected_field_w).abs() < 0.5,
479 "field should take leftover space inside wrapper, got {} expected ~{}",
480 field_rect.w,
481 expected_field_w,
482 );
483 }
484
485 #[test]
486 fn explicit_width_fill_still_works() {
487 let value = String::from("42");
491 let sel = Selection::default();
492 let widget =
493 numeric_input("n", &value, &sel, NumericInputOpts::default()).width(Size::Fill(1.0));
494 let mut tree = crate::widgets::form::form([crate::widgets::form::form_item([
495 crate::widgets::form::form_control(widget),
496 ])]);
497 let mut state = UiState::new();
498 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 320.0, 200.0));
499 let row_rect = state.rect_of_key("n").expect("row rect");
500 assert!(
501 row_rect.w > DEFAULT_WIDTH,
502 "explicit `.width(Fill)` should override the fixed default, got {}",
503 row_rect.w,
504 );
505 }
506
507 fn text_event(target_key: &str, text: &str) -> UiEvent {
511 UiEvent {
512 path: None,
513 key: Some(target_key.to_string()),
514 target: Some(UiTarget {
515 key: target_key.to_string(),
516 node_id: format!("/{target_key}").into(),
517 rect: Rect::new(0.0, 0.0, 100.0, 32.0),
518 tooltip: None,
519 scroll_offset_y: 0.0,
520 }),
521 pointer: None,
522 key_press: None,
523 text: Some(text.to_string()),
524 selection: None,
525 modifiers: KeyModifiers::default(),
526 click_count: 0,
527 pointer_kind: None,
528 wheel_delta: None,
529 kind: UiEventKind::TextInput,
530 }
531 }
532
533 #[test]
534 fn inc_steps_value_up_by_step() {
535 let mut value = String::from("3");
536 let mut sel = Selection::default();
537 let opts = NumericInputOpts::default().step(2.0);
538 assert!(apply_event(
539 &mut value,
540 &mut sel,
541 "n",
542 &opts,
543 &click("n:inc")
544 ));
545 assert_eq!(value, "5");
546 }
547
548 #[test]
549 fn dec_steps_value_down_by_step() {
550 let mut value = String::from("3");
551 let mut sel = Selection::default();
552 let opts = NumericInputOpts::default().step(0.5).decimals(1);
553 assert!(apply_event(
554 &mut value,
555 &mut sel,
556 "n",
557 &opts,
558 &click("n:dec")
559 ));
560 assert_eq!(value, "2.5");
561 }
562
563 #[test]
564 fn inc_clamps_to_max() {
565 let mut value = String::from("99");
566 let mut sel = Selection::default();
567 let opts = NumericInputOpts::default().min(0.0).max(100.0);
568 let opts = opts.step(5.0);
570 assert!(apply_event(
571 &mut value,
572 &mut sel,
573 "n",
574 &opts,
575 &click("n:inc")
576 ));
577 assert_eq!(value, "100");
578 }
579
580 #[test]
581 fn dec_clamps_to_min() {
582 let mut value = String::from("1");
583 let mut sel = Selection::default();
584 let opts = NumericInputOpts::default().min(0.0).max(100.0);
585 assert!(apply_event(
586 &mut value,
587 &mut sel,
588 "n",
589 &opts,
590 &click("n:dec")
591 ));
592 assert_eq!(value, "0");
593 assert!(apply_event(
595 &mut value,
596 &mut sel,
597 "n",
598 &opts,
599 &click("n:dec")
600 ));
601 assert_eq!(value, "0");
602 }
603
604 #[test]
605 fn empty_value_treated_as_min_when_set() {
606 let mut value = String::new();
607 let mut sel = Selection::default();
608 let opts = NumericInputOpts::default().min(10.0).max(100.0);
609 assert!(apply_event(
611 &mut value,
612 &mut sel,
613 "n",
614 &opts,
615 &click("n:inc")
616 ));
617 assert_eq!(value, "11");
618 }
619
620 #[test]
621 fn empty_value_treated_as_zero_when_no_min() {
622 let mut value = String::new();
623 let mut sel = Selection::default();
624 let opts = NumericInputOpts::default();
625 assert!(apply_event(
626 &mut value,
627 &mut sel,
628 "n",
629 &opts,
630 &click("n:inc")
631 ));
632 assert_eq!(value, "1");
633 }
634
635 #[test]
636 fn unparseable_value_treated_as_zero_when_no_min() {
637 let mut value = String::from("abc");
638 let mut sel = Selection::default();
639 let opts = NumericInputOpts::default();
640 assert!(apply_event(
641 &mut value,
642 &mut sel,
643 "n",
644 &opts,
645 &click("n:inc")
646 ));
647 assert_eq!(value, "1");
648 }
649
650 #[test]
651 fn ignores_unrelated_keys() {
652 let mut value = String::from("3");
653 let mut sel = Selection::default();
654 let opts = NumericInputOpts::default();
655 assert!(!apply_event(
657 &mut value,
658 &mut sel,
659 "n",
660 &opts,
661 &click("other:inc")
662 ));
663 assert_eq!(value, "3");
664 }
665
666 #[test]
667 fn decimals_format_pads_zeros() {
668 let mut value = String::from("0");
669 let mut sel = Selection::default();
670 let opts = NumericInputOpts::default().step(0.10).decimals(2);
671 assert!(apply_event(
672 &mut value,
673 &mut sel,
674 "n",
675 &opts,
676 &click("n:inc")
677 ));
678 assert_eq!(value, "0.10");
679 }
680
681 #[test]
682 fn no_decimals_strips_trailing_zero() {
683 let mut value = String::from("0");
684 let mut sel = Selection::default();
685 let opts = NumericInputOpts::default().step(1.0);
686 assert!(apply_event(
687 &mut value,
688 &mut sel,
689 "n",
690 &opts,
691 &click("n:inc")
692 ));
693 assert_eq!(value, "1");
696 }
697
698 #[test]
699 fn text_event_for_other_widget_is_ignored() {
700 let mut value = String::from("42");
705 let mut sel = Selection::default();
706 let opts = NumericInputOpts::default();
707 assert!(!apply_event(
710 &mut value,
711 &mut sel,
712 "n",
713 &opts,
714 &text_event("other-input", "x"),
715 ));
716 assert_eq!(value, "42");
717 }
718
719 #[test]
720 fn text_event_filter_rejects_non_numeric_chars() {
721 let mut value = String::from("12");
727 let mut sel = Selection::default();
728 let opts = NumericInputOpts::default();
729 assert!(apply_event(
730 &mut value,
731 &mut sel,
732 "n",
733 &opts,
734 &text_event("n:field", "abc"),
735 ));
736 assert_eq!(value, "12");
737 }
738
739 #[test]
740 fn text_event_filter_accepts_partial_numeric_states() {
741 for partial in ["-", "1.", "1.5e", "1.5e+", ".5", "+"] {
745 let mut value = String::new();
746 let mut sel = Selection::default();
747 let opts = NumericInputOpts::default();
748 assert!(
749 apply_event(
750 &mut value,
751 &mut sel,
752 "n",
753 &opts,
754 &text_event("n:field", partial),
755 ),
756 "filter should accept partial value {partial:?}",
757 );
758 assert_eq!(value, partial, "value should equal {partial:?}");
759 }
760 }
761
762 #[test]
763 fn text_event_filter_accepts_full_numeric_paste() {
764 let mut value = String::new();
765 let mut sel = Selection::default();
766 let opts = NumericInputOpts::default();
767 assert!(apply_event(
768 &mut value,
769 &mut sel,
770 "n",
771 &opts,
772 &text_event("n:field", "42.5"),
773 ));
774 assert_eq!(value, "42.5");
775 }
776
777 #[test]
778 fn build_widget_has_three_children_and_correct_keys() {
779 let value = String::from("0");
780 let sel = Selection::default();
781 let opts = NumericInputOpts::default();
782 let el = numeric_input("n", &value, &sel, opts);
783 assert_eq!(el.key.as_deref(), Some("n"));
784 assert_eq!(el.children.len(), 3, "decrement, field, increment");
785 assert_eq!(el.children[0].key.as_deref(), Some("n:dec"));
786 assert_eq!(el.children[1].key.as_deref(), Some("n:field"));
787 assert_eq!(el.children[2].key.as_deref(), Some("n:inc"));
788 }
789
790 fn key_event(key: &str, ui_key: LogicalKey, modifiers: KeyModifiers) -> UiEvent {
794 use crate::event::KeyPress;
795 UiEvent {
796 path: None,
797 key: Some(key.to_string()),
798 target: Some(UiTarget {
799 key: key.to_string(),
800 node_id: format!("/{key}").into(),
801 rect: Rect::new(0.0, 0.0, 100.0, 32.0),
802 tooltip: None,
803 scroll_offset_y: 0.0,
804 }),
805 pointer: None,
806 key_press: Some(KeyPress {
807 logical: ui_key,
808 physical: PhysicalKey::Unidentified,
809 modifiers,
810 repeat: false,
811 }),
812 text: None,
813 selection: None,
814 modifiers,
815 click_count: 0,
816 pointer_kind: None,
817 wheel_delta: None,
818 kind: UiEventKind::KeyDown,
819 }
820 }
821
822 #[test]
823 fn arrow_up_on_field_steps_up() {
824 let mut value = String::from("3");
825 let mut sel = Selection::default();
826 let opts = NumericInputOpts::default().step(1.0);
827 assert!(apply_event(
828 &mut value,
829 &mut sel,
830 "n",
831 &opts,
832 &key_event(
833 "n:field",
834 LogicalKey::Named(NamedKey::ArrowUp),
835 KeyModifiers::default()
836 ),
837 ));
838 assert_eq!(value, "4");
839 }
840
841 #[test]
842 fn arrow_down_on_field_steps_down() {
843 let mut value = String::from("3");
844 let mut sel = Selection::default();
845 let opts = NumericInputOpts::default().step(1.0);
846 assert!(apply_event(
847 &mut value,
848 &mut sel,
849 "n",
850 &opts,
851 &key_event(
852 "n:field",
853 LogicalKey::Named(NamedKey::ArrowDown),
854 KeyModifiers::default()
855 ),
856 ));
857 assert_eq!(value, "2");
858 }
859
860 #[test]
861 fn shift_arrow_steps_by_ten_times() {
862 let mut value = String::from("3");
863 let mut sel = Selection::default();
864 let opts = NumericInputOpts::default().step(1.0);
865 let shift = KeyModifiers {
866 shift: true,
867 ..KeyModifiers::default()
868 };
869 assert!(apply_event(
870 &mut value,
871 &mut sel,
872 "n",
873 &opts,
874 &key_event("n:field", LogicalKey::Named(NamedKey::ArrowUp), shift),
875 ));
876 assert_eq!(value, "13");
877 }
878
879 #[test]
880 fn alt_arrow_steps_by_one_tenth() {
881 let mut value = String::from("0");
884 let mut sel = Selection::default();
885 let opts = NumericInputOpts::default().step(0.1).decimals(2);
886 let alt = KeyModifiers {
887 alt: true,
888 ..KeyModifiers::default()
889 };
890 assert!(apply_event(
891 &mut value,
892 &mut sel,
893 "n",
894 &opts,
895 &key_event("n:field", LogicalKey::Named(NamedKey::ArrowUp), alt),
896 ));
897 assert_eq!(value, "0.01");
898 }
899
900 #[test]
901 fn shift_click_on_inc_button_scales_step() {
902 let mut value = String::from("3");
905 let mut sel = Selection::default();
906 let opts = NumericInputOpts::default().step(1.0);
907 let mut ev = click("n:inc");
908 ev.modifiers = KeyModifiers {
909 shift: true,
910 ..KeyModifiers::default()
911 };
912 assert!(apply_event(&mut value, &mut sel, "n", &opts, &ev));
913 assert_eq!(value, "13");
914 }
915
916 #[test]
917 fn arrow_key_on_field_clamps_to_max() {
918 let mut value = String::from("99");
919 let mut sel = Selection::default();
920 let opts = NumericInputOpts::default().step(5.0).max(100.0);
921 assert!(apply_event(
922 &mut value,
923 &mut sel,
924 "n",
925 &opts,
926 &key_event(
927 "n:field",
928 LogicalKey::Named(NamedKey::ArrowUp),
929 KeyModifiers::default()
930 ),
931 ));
932 assert_eq!(value, "100");
933 }
934
935 #[test]
936 fn arrow_key_routed_elsewhere_is_ignored() {
937 let mut value = String::from("3");
941 let mut sel = Selection::default();
942 let opts = NumericInputOpts::default();
943 assert!(!apply_event(
944 &mut value,
945 &mut sel,
946 "n",
947 &opts,
948 &key_event(
949 "other:field",
950 LogicalKey::Named(NamedKey::ArrowUp),
951 KeyModifiers::default()
952 ),
953 ));
954 assert_eq!(value, "3");
955 }
956
957 #[test]
958 fn non_arrow_keydown_on_field_falls_through() {
959 let mut value = String::from("3");
963 let mut sel = Selection::default();
964 let opts = NumericInputOpts::default();
965 assert!(!apply_event(
966 &mut value,
967 &mut sel,
968 "n",
969 &opts,
970 &key_event(
971 "n:field",
972 LogicalKey::Named(NamedKey::Tab),
973 KeyModifiers::default()
974 ),
975 ));
976 assert_eq!(value, "3");
977 }
978
979 #[test]
980 fn stacked_variant_has_field_and_chevron_column() {
981 let value = String::from("0");
982 let sel = Selection::default();
983 let opts = NumericInputOpts::default().stacked();
984 let el = numeric_input("n", &value, &sel, opts);
985 assert_eq!(el.key.as_deref(), Some("n"));
986 assert_eq!(el.children.len(), 2, "field + chevron column");
990 assert_eq!(el.children[0].key.as_deref(), Some("n:field"));
991 let column_children = &el.children[1].children;
992 assert_eq!(column_children.len(), 2, "chevron-up over chevron-down");
993 assert_eq!(column_children[0].key.as_deref(), Some("n:inc"));
994 assert_eq!(column_children[1].key.as_deref(), Some("n:dec"));
995 }
996
997 #[test]
998 fn stacked_variant_keeps_apply_event_contract() {
999 let mut value = String::from("3");
1002 let mut sel = Selection::default();
1003 let opts = NumericInputOpts::default().stacked();
1004 assert!(apply_event(
1005 &mut value,
1006 &mut sel,
1007 "n",
1008 &opts,
1009 &click("n:inc"),
1010 ));
1011 assert_eq!(value, "4");
1012 assert!(apply_event(
1013 &mut value,
1014 &mut sel,
1015 "n",
1016 &opts,
1017 &key_event(
1018 "n:field",
1019 LogicalKey::Named(NamedKey::ArrowDown),
1020 KeyModifiers::default()
1021 ),
1022 ));
1023 assert_eq!(value, "3");
1024 }
1025
1026 #[test]
1031 fn spinner_glyphs_render_whole_not_ellipsized() {
1032 use crate::draw_ops::draw_ops;
1033 use crate::ir::DrawOp;
1034 use crate::layout::layout;
1035 use crate::state::UiState;
1036 use crate::tree::Rect;
1037
1038 let sel = Selection::default();
1039 let mut tree = numeric_input("n", "3", &sel, NumericInputOpts::default());
1040 let mut state = UiState::new();
1041 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 240.0, 48.0));
1042 let ops = draw_ops(&tree, &state);
1043 let labels: Vec<&str> = ops
1044 .iter()
1045 .filter_map(|op| match op {
1046 DrawOp::GlyphRun { text, .. } => Some(text.as_str()),
1047 _ => None,
1048 })
1049 .collect();
1050 assert!(labels.contains(&"−"), "decrement glyph intact: {labels:?}");
1051 assert!(labels.contains(&"+"), "increment glyph intact: {labels:?}");
1052 assert!(
1053 !labels.iter().any(|l| l.contains('\u{2026}')),
1054 "nothing ellipsized at natural size: {labels:?}"
1055 );
1056 }
1057}