1use super::*;
2use std::time::Duration;
3use winit::keyboard::NamedKey;
4
5const DOUBLE_CLICK: Duration = Duration::from_millis(500);
6/// The key that carries shortcuts: Command on macOS, Control elsewhere.
7const SHORTCUT: ModifiersState = if cfg!(target_vendor = "apple") {
8 ModifiersState::SUPER
9} else {
10 ModifiersState::CONTROL
11};
12
13thread_local! {
14 static START: Instant = Instant::now();
15 static FRAMES: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
16}
17
18/// Builds a frame in a 400 by 300 window, 16 ms after this thread's previous one.
19fn frame(ui: &mut Ui, build: impl FnOnce(&mut Ui)) {
20 sized_frame(ui, [400.0, 300.0], build);
21}
22
23/// Builds a frame in a window `size` large, 16 ms after this thread's previous one.
24fn sized_frame(ui: &mut Ui, size: [f32; 2], build: impl FnOnce(&mut Ui)) {
25 let frames = FRAMES.with(|frames| {
26 frames.set(frames.get() + 1);
27 frames.get()
28 });
29 let now = START.with(|start| *start) + Duration::from_millis(16) * frames;
30 ui.begin(size, 2.0, now);
31 build(ui);
32 ui.end();
33}
34
35#[test]
36fn fill_yields_to_strict_siblings_along_the_flow() {
37 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
38 let mut ids = Vec::new();
39 frame(&mut ui, |ui| {
40 ui.open(
41 "row",
42 Spec {
43 size: [fill(), px(100.0)],
44 ..Spec::default()
45 },
46 );
47 ids.push(ui.open(
48 "page",
49 Spec {
50 size: [fill(), fill()],
51 ..Spec::default()
52 },
53 ));
54 ui.close();
55 ids.push(ui.open(
56 "list",
57 Spec {
58 size: [px(120.0), fill()],
59 ..Spec::default()
60 },
61 ));
62 ui.close();
63 ui.close();
64 });
65 assert_eq!(ui.laid_out(ids[0]), Some([0.0, 0.0, 280.0, 100.0]));
66 assert_eq!(ui.laid_out(ids[1]), Some([280.0, 0.0, 400.0, 100.0]));
67}
68
69#[test]
70fn padding_wider_than_its_box_leaves_loose_children_empty_not_negative() {
71 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
72 let mut child = None;
73 frame(&mut ui, |ui| {
74 ui.open(
75 "cramped",
76 Spec {
77 size: [px(10.0), px(10.0)],
78 pad: [8.0, 8.0],
79 ..Spec::default()
80 },
81 );
82 let loose = Extent {
83 size: Size::Pixels(20.0),
84 strictness: 0.0,
85 };
86 child = Some(ui.open(
87 "child",
88 Spec {
89 size: [loose; 2],
90 ..Spec::default()
91 },
92 ));
93 ui.close();
94 ui.close();
95 });
96 assert_eq!(ui.laid_out(child.unwrap()), Some([8.0, 8.0, 8.0, 8.0]));
97}
98
99#[test]
100fn children_sum_with_gaps_and_padding() {
101 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
102 let mut column = None;
103 let mut second = None;
104 frame(&mut ui, |ui| {
105 column = Some(ui.open(
106 "column",
107 Spec {
108 axis: Axis::Y,
109 pad: [4.0, 6.0],
110 gap: 2.0,
111 ..Spec::default()
112 },
113 ));
114 ui.leaf(
115 1,
116 Spec {
117 size: [px(50.0), px(10.0)],
118 ..Spec::default()
119 },
120 );
121 second = Some(ui.open(
122 2,
123 Spec {
124 size: [px(30.0), px(20.0)],
125 ..Spec::default()
126 },
127 ));
128 ui.close();
129 ui.close();
130 });
131 assert_eq!(ui.laid_out(column.unwrap()), Some([0.0, 0.0, 58.0, 44.0]));
132 assert_eq!(ui.laid_out(second.unwrap()), Some([4.0, 18.0, 34.0, 38.0]));
133}
134
135/// A row `width` wide of a space, a box that yields all but a quarter of its 80 pixels, and
136/// groups 100 wide or 20 folded at each priority; returns the rectangles of the loose box and
137/// of each group's full and folded button.
138fn folding_row(ui: &mut Ui, width: f32, priorities: &[u32]) -> Vec<Option<[f32; 4]>> {
139 let mut ids = Vec::new();
140 frame(ui, |ui| {
141 ui.open(
142 "row",
143 Spec {
144 size: [px(width), px(20.0)],
145 ..Spec::default()
146 },
147 );
148 ui.leaf(
149 "space",
150 Spec {
151 size: [fill(), px(1.0)],
152 ..Spec::default()
153 },
154 );
155 let loose = Extent {
156 size: Size::Pixels(80.0),
157 strictness: 0.25,
158 };
159 ids.push(ui.open(
160 "loose",
161 Spec {
162 size: [loose, px(20.0)],
163 ..Spec::default()
164 },
165 ));
166 ui.close();
167 for priority in priorities {
168 let group = ui.open(
169 priority,
170 Spec {
171 fold: Some(*priority),
172 ..Spec::default()
173 },
174 );
175 for (form, width) in [("full", 100.0), ("folded", 20.0)] {
176 ui.open(form, Spec::default());
177 ui.leaf(
178 "button",
179 Spec {
180 flags: Flags::CLICKABLE,
181 size: [px(width), px(20.0)],
182 ..Spec::default()
183 },
184 );
185 ui.close();
186 }
187 ui.close();
188 for form in ["full", "folded"] {
189 ids.push(group.child(form).child("button"));
190 }
191 }
192 ui.close();
193 });
194 ids.into_iter().map(|id| ui.laid_out(id)).collect()
195}
196
197#[test]
198fn a_row_folds_its_lowest_priorities_first_until_it_fits() {
199 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
200 // 80 + 3 × 100 fits 380 with nothing folded or squeezed.
201 let rects = folding_row(&mut ui, 380.0, &[2, 0, 1]);
202 assert_eq!(rects[0], Some([0.0, 0.0, 80.0, 20.0]));
203 assert_eq!(rects[1], Some([80.0, 0.0, 180.0, 20.0]));
204 // At 300, folding priority 0 alone leaves 80 + 100 + 20 + 100; the full form's button
205 // stands where the folded one shows.
206 let rects = folding_row(&mut ui, 300.0, &[2, 0, 1]);
207 assert_eq!(rects[1], Some([80.0, 0.0, 180.0, 20.0]));
208 assert_eq!(rects[3..5], [Some([180.0, 0.0, 200.0, 20.0]); 2]);
209 assert_eq!(rects[5], Some([200.0, 0.0, 300.0, 20.0]));
210}
211
212#[test]
213fn a_row_squeezes_its_boxes_only_once_every_group_is_folded() {
214 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
215 let rects = folding_row(&mut ui, 130.0, &[0, 1]);
216 // 80 + 20 + 20 fits 130 with 10 to spare, taken by the space.
217 assert_eq!(rects[0], Some([10.0, 0.0, 90.0, 20.0]));
218 assert_eq!(rects[4], Some([110.0, 0.0, 130.0, 20.0]));
219 let rects = folding_row(&mut ui, 100.0, &[0, 1]);
220 assert_eq!(rects[0], Some([0.0, 0.0, 60.0, 20.0]));
221 assert_eq!(rects[4], Some([80.0, 0.0, 100.0, 20.0]));
222}
223
224#[test]
225fn a_row_is_narrowest_with_every_group_folded_and_every_box_squeezed() {
226 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
227 folding_row(&mut ui, 300.0, &[0, 1]);
228 let row = Id::ROOT.child("row");
229 // The space gives all, the loose box 60 of 80, each group 80 of 100.
230 assert_eq!(ui.narrowest(row), Some(60.0));
231 let rects = folding_row(&mut ui, 60.0, &[0, 1]);
232 assert_eq!(rects[4], Some([40.0, 0.0, 60.0, 20.0]));
233}
234
235#[test]
236fn a_group_inside_a_box_sized_by_its_children_folds_with_the_row() {
237 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
238 let build = |ui: &mut Ui, width: f32| {
239 frame(ui, |ui| {
240 ui.open(
241 "row",
242 Spec {
243 size: [px(width), px(20.0)],
244 ..Spec::default()
245 },
246 );
247 ui.open("outer", Spec::default());
248 ui.leaf(
249 "fixed",
250 Spec {
251 size: [px(50.0), px(20.0)],
252 ..Spec::default()
253 },
254 );
255 ui.open(
256 "group",
257 Spec {
258 fold: Some(0),
259 ..Spec::default()
260 },
261 );
262 for (form, width) in [("full", 100.0), ("folded", 20.0)] {
263 ui.open(form, Spec::default());
264 ui.leaf(
265 "button",
266 Spec {
267 size: [px(width), px(20.0)],
268 ..Spec::default()
269 },
270 );
271 ui.close();
272 }
273 ui.close();
274 ui.close();
275 ui.open(
276 "later",
277 Spec {
278 fold: Some(1),
279 ..Spec::default()
280 },
281 );
282 for (form, width) in [("full", 30.0), ("folded", 30.0)] {
283 ui.open(form, Spec::default());
284 ui.leaf(
285 "button",
286 Spec {
287 size: [px(width), px(20.0)],
288 ..Spec::default()
289 },
290 );
291 ui.close();
292 }
293 ui.close();
294 ui.close();
295 });
296 };
297 let [outer, after] = ["outer", "later"].map(|part| Id::ROOT.child("row").child(part));
298 build(&mut ui, 180.0);
299 assert_eq!(ui.laid_out(outer), Some([0.0, 0.0, 150.0, 20.0]));
300 // 50 + 100 + 30 overflows 179, so the group folds before the later one of higher priority,
301 // and the box around it narrows to 70.
302 build(&mut ui, 179.0);
303 assert_eq!(ui.laid_out(outer), Some([0.0, 0.0, 70.0, 20.0]));
304 assert_eq!(ui.laid_out(after), Some([70.0, 0.0, 100.0, 20.0]));
305 assert_eq!(ui.narrowest(Id::ROOT.child("row")), Some(100.0));
306}
307
308#[test]
309fn a_box_sized_by_its_children_yields_what_they_yield() {
310 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
311 let mut combo = None;
312 frame(&mut ui, |ui| {
313 ui.open(
314 "row",
315 Spec {
316 size: [px(100.0), px(20.0)],
317 ..Spec::default()
318 },
319 );
320 ui.open("group", Spec::default());
321 let loose = Extent {
322 size: Size::Pixels(80.0),
323 strictness: 0.5,
324 };
325 combo = Some(ui.open(
326 "combo",
327 Spec {
328 size: [loose, px(20.0)],
329 ..Spec::default()
330 },
331 ));
332 ui.close();
333 ui.leaf(
334 "button",
335 Spec {
336 size: [px(40.0), px(20.0)],
337 ..Spec::default()
338 },
339 );
340 ui.close();
341 ui.close();
342 });
343 assert_eq!(ui.laid_out(combo.unwrap()), Some([0.0, 0.0, 60.0, 20.0]));
344}
345
346#[test]
347fn a_folded_away_form_takes_no_input() {
348 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
349 folding_row(&mut ui, 110.0, &[0]);
350 let folded = Id::ROOT
351 .child("row")
352 .child(0_u32)
353 .child("folded")
354 .child("button");
355 assert_eq!(ui.box_at([90.0, 10.0]), Some(folded));
356 // Where the full form's button would lie past the folded one.
357 assert_eq!(ui.box_at([150.0, 10.0]), None);
358}
359
360fn button_frame(ui: &mut Ui) -> Signal {
361 let mut signal = Signal::default();
362 frame(ui, |ui| {
363 signal = ui.leaf(
364 "button",
365 Spec {
366 flags: Flags::CLICKABLE,
367 size: [px(100.0), px(40.0)],
368 hover_fill: Some([1.0; 4]),
369 ..Spec::default()
370 },
371 );
372 });
373 signal
374}
375
376#[test]
377fn clicks_use_the_previous_layout_and_hover_animates() {
378 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
379 button_frame(&mut ui);
380 let at = Instant::now();
381 ui.event(Event::PointerMoved([10.0, 10.0]));
382 ui.event(Event::Button {
383 button: MouseButton::Left,
384 pressed: true,
385 at,
386 });
387 let signal = button_frame(&mut ui);
388 assert!(signal.hovered && signal.pressed && signal.dragging && !signal.clicked);
389 assert!(ui.wants_frame(), "hover fades in");
390 ui.event(Event::Button {
391 button: MouseButton::Left,
392 pressed: false,
393 at,
394 });
395 ui.event(Event::Button {
396 button: MouseButton::Left,
397 pressed: true,
398 at: at + Duration::from_millis(100),
399 });
400 let signal = button_frame(&mut ui);
401 assert!(signal.clicked && signal.pressed);
402 button_frame(&mut ui);
403 ui.event(Event::PointerMoved([200.0, 10.0]));
404 ui.event(Event::Button {
405 button: MouseButton::Left,
406 pressed: false,
407 at,
408 });
409 let signal = button_frame(&mut ui);
410 assert!(!signal.clicked && !signal.hovered && !signal.dragging);
411 assert!(ui.wants_frame(), "hover fades out");
412 for _ in 0..40 {
413 button_frame(&mut ui);
414 }
415 assert!(!ui.wants_frame());
416}
417
418/// Two boxes swapping places under a still pointer, as dropped tabs reorder, pass the hover
419/// to the one that arrives.
420#[test]
421fn a_box_moving_under_a_still_pointer_takes_the_hover() {
422 let build = |ui: &mut Ui, swapped: bool| {
423 let mut hovered = [false; 2];
424 frame(ui, |ui| {
425 for (index, hover) in hovered.iter_mut().enumerate() {
426 let x = if (index == 0) != swapped { 0.0 } else { 100.0 };
427 *hover = ui
428 .leaf(
429 index,
430 Spec {
431 flags: Flags::CLICKABLE | Flags::FLOAT,
432 size: [px(50.0), px(20.0)],
433 position: [x, 0.0],
434 ..Spec::default()
435 },
436 )
437 .hovered;
438 }
439 });
440 hovered
441 };
442 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
443 build(&mut ui, false);
444 ui.event(Event::PointerMoved([10.0, 10.0]));
445 assert_eq!(build(&mut ui, false), [true, false]);
446 build(&mut ui, true);
447 assert_eq!(build(&mut ui, true), [false, true]);
448}
449
450/// A click whose press and release arrive together starts an animation that a box built
451/// earlier in the frame reads: the frame that handled the click asks for the next one.
452#[test]
453fn a_click_within_one_frame_starts_an_animation_read_before_it() {
454 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
455 let mut open = false;
456 let mut width = 0.0;
457 let build = |ui: &mut Ui, open: &mut bool, width: &mut f32| {
458 *width = ui.animate(Id::ROOT.child("panel"), if *open { 100.0 } else { 0.0 });
459 if button(ui, "toggle", "Toggle").clicked {
460 *open = true;
461 }
462 };
463 frame(&mut ui, |ui| build(ui, &mut open, &mut width));
464 // The pointer rests on the button until its hover has faded in.
465 ui.event(Event::PointerMoved([5.0, 5.0]));
466 for _ in 0..40 {
467 frame(&mut ui, |ui| build(ui, &mut open, &mut width));
468 }
469 assert!(!ui.wants_frame());
470 let at = Instant::now();
471 for pressed in [true, false] {
472 ui.event(Event::Button {
473 button: MouseButton::Left,
474 pressed,
475 at,
476 });
477 }
478 frame(&mut ui, |ui| build(ui, &mut open, &mut width));
479 assert!(open && width == 0.0);
480 assert!(ui.wants_frame(), "the click's frame asks for another");
481 frame(&mut ui, |ui| build(ui, &mut open, &mut width));
482 frame(&mut ui, |ui| build(ui, &mut open, &mut width));
483 assert!(width > 0.0 && ui.wants_frame(), "the panel eases open");
484}
485
486#[test]
487fn hover_fades_its_own_colour_in_over_a_box_without_a_fill() {
488 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
489 button_frame(&mut ui);
490 ui.event(Event::PointerMoved([10.0, 10.0]));
491 button_frame(&mut ui);
492 let fill = ui.display.iter().find_map(|item| match item {
493 Display::Rect { fill, .. } => Some(*fill),
494 _ => None,
495 });
496 let [red, green, blue, alpha] = fill.expect("the hovered button paints");
497 assert_eq!(
498 [red, green, blue],
499 [1.0; 3],
500 "no darker colour on the way in"
501 );
502 assert!(alpha > 0.0 && alpha < 1.0);
503}
504
505#[test]
506fn custom_boxes_receive_their_events_and_a_leave_when_the_pointer_moves_off() {
507 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
508 let build = |ui: &mut Ui| {
509 let mut signals = Vec::new();
510 frame(ui, |ui| {
511 ui.open(
512 "row",
513 Spec {
514 size: [fill(), fill()],
515 ..Spec::default()
516 },
517 );
518 signals.push(ui.leaf(
519 "page",
520 Spec {
521 flags: Flags::CUSTOM | Flags::FOCUSABLE,
522 size: [fill(), fill()],
523 ..Spec::default()
524 },
525 ));
526 signals.push(ui.leaf(
527 "list",
528 Spec {
529 flags: Flags::CLICKABLE,
530 size: [px(100.0), fill()],
531 ..Spec::default()
532 },
533 ));
534 ui.close();
535 });
536 signals
537 };
538 build(&mut ui);
539 let at = Instant::now();
540 ui.event(Event::PointerMoved([20.0, 20.0]));
541 ui.event(Event::Button {
542 button: MouseButton::Left,
543 pressed: true,
544 at,
545 });
546 ui.event(Event::Key {
547 key: Key::Character("a".into()),
548 text: Some("a".into()),
549 });
550 let signals = build(&mut ui);
551 assert!(signals[0].focused);
552 assert_eq!(signals[0].events.len(), 3);
553 assert!(matches!(signals[0].events[2], Event::Key { .. }));
554 assert!(
555 ui.cursor().is_none(),
556 "the host picks the cursor over its box"
557 );
558 ui.event(Event::Pressure(Some(0.5)));
559 ui.event(Event::PointerMoved([350.0, 20.0]));
560 let signals = build(&mut ui);
561 assert_eq!(
562 signals[0].events,
563 [
564 Event::Pressure(Some(0.5)),
565 Event::PointerMoved([350.0, 20.0])
566 ],
567 "a drag keeps its box, and the pen's pressure goes with it"
568 );
569 ui.event(Event::Button {
570 button: MouseButton::Left,
571 pressed: false,
572 at,
573 });
574 let signals = build(&mut ui);
575 assert!(matches!(
576 signals[0].events[..],
577 [Event::Button { .. }, Event::PointerLeft]
578 ));
579 assert!(signals[1].hovered);
580 ui.event(Event::Modifiers(ModifiersState::SHIFT));
581 let signals = build(&mut ui);
582 assert_eq!(signals[0].events, [Event::Modifiers(ModifiersState::SHIFT)]);
583}
584
585#[test]
586fn wheel_scrolls_within_the_content_and_clips_children() {
587 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
588 let mut list = None;
589 let mut last = None;
590 let mut build = |ui: &mut Ui| {
591 frame(ui, |ui| {
592 list = Some(ui.open(
593 "list",
594 Spec {
595 flags: Flags::SCROLL | Flags::CLIP,
596 axis: Axis::Y,
597 size: [px(100.0), px(100.0)],
598 ..Spec::default()
599 },
600 ));
601 for row in 0..10 {
602 last = Some(ui.open(
603 row,
604 Spec {
605 flags: Flags::CLICKABLE,
606 size: [fill(), px(30.0)],
607 ..Spec::default()
608 },
609 ));
610 ui.close();
611 }
612 ui.close();
613 });
614 };
615 build(&mut ui);
616 ui.event(Event::PointerMoved([50.0, 50.0]));
617 ui.event(Event::Wheel([0.0, -1000.0]));
618 build(&mut ui);
619 assert_eq!(
620 ui.states[&list.unwrap()].scroll,
621 200.0,
622 "at once, as on the page"
623 );
624 assert_eq!(ui.laid_out(last.unwrap()), Some([0.0, 70.0, 100.0, 100.0]));
625 let layers = ui.layers();
626 assert!(matches!(
627 layers[..],
628 [Layer::Primitives(Primitives {
629 clip: Some([0.0, 0.0, 100.0, 100.0]),
630 ..
631 })] | []
632 ));
633}
634
635fn field_frame(ui: &mut Ui, text: &mut String) -> Signal {
636 let mut signal = Signal::default();
637 frame(ui, |ui| {
638 signal = text_field(
639 ui,
640 field(),
641 text,
642 "Filter",
643 Spec {
644 size: [px(200.0), px(26.0)],
645 pad: [6.0, 0.0],
646 ..Spec::default()
647 },
648 );
649 });
650 signal
651}
652
653fn field() -> Id {
654 Id::ROOT.child("filter")
655}
656
657fn key(named: NamedKey) -> Event {
658 Event::Key {
659 key: Key::Named(named),
660 text: None,
661 }
662}
663
664fn typed(text: &str) -> Event {
665 Event::Key {
666 key: Key::Character(text.into()),
667 text: Some(text.into()),
668 }
669}
670
671/// A focused field holding `text`.
672fn focused_field(text: &str) -> (Ui, String) {
673 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
674 let mut text = text.to_owned();
675 field_frame(&mut ui, &mut text);
676 ui.set_focus(Some(field()));
677 (ui, text)
678}
679
680/// Moves the pointer over the caret before byte `index` of the field's `text`.
681fn point_to(ui: &mut Ui, text: &str, index: usize) {
682 let size = ui.theme.font_size;
683 let (texts, frame) = (&mut ui.texts, ui.frame);
684 let layout = &texts.label(text, size, frame).layout;
685 let x = parley::editing::Cursor::from_byte_index(layout, index, parley::Affinity::Downstream)
686 .geometry(layout, 1.0)
687 .x0 as f32;
688 ui.event(Event::PointerMoved([6.0 + x, 13.0]));
689}
690
691fn press(ui: &mut Ui, at: Instant, pressed: bool) {
692 ui.event(Event::Button {
693 button: MouseButton::Left,
694 pressed,
695 at,
696 });
697}
698
699#[test]
700fn text_fields_edit_with_keys_and_selection() {
701 let (mut ui, mut text) = focused_field("café");
702 ui.event(key(NamedKey::End));
703 ui.event(key(NamedKey::Backspace));
704 ui.event(typed("e"));
705 field_frame(&mut ui, &mut text);
706 assert_eq!(text, "cafe");
707 ui.event(Event::Modifiers(ModifiersState::SHIFT));
708 ui.event(key(NamedKey::ArrowLeft));
709 ui.event(key(NamedKey::ArrowLeft));
710 field_frame(&mut ui, &mut text);
711 ui.event(Event::Modifiers(ModifiersState::empty()));
712 ui.event(Event::Ime(Ime::Commit("é".into())));
713 field_frame(&mut ui, &mut text);
714 assert_eq!(text, "caé");
715 ui.event(Event::Modifiers(SHORTCUT));
716 ui.event(typed("a"));
717 field_frame(&mut ui, &mut text);
718 ui.event(Event::Modifiers(ModifiersState::empty()));
719 ui.event(key(NamedKey::Delete));
720 let signal = field_frame(&mut ui, &mut text);
721 assert!(signal.focused);
722 assert_eq!(text, "");
723}
724
725#[test]
726fn a_field_focused_whole_selects_its_text_once_for_typing_over() {
727 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
728 let mut text = "New Section 1".to_owned();
729 ui.focus_all(field());
730 field_frame(&mut ui, &mut text);
731 ui.event(typed("K"));
732 field_frame(&mut ui, &mut text);
733 ui.event(typed("i"));
734 let signal = field_frame(&mut ui, &mut text);
735 assert!(signal.focused);
736 assert_eq!(text, "Ki");
737}
738
739#[test]
740#[cfg_attr(not(target_vendor = "apple"), ignore = "macOS editing conventions")]
741fn text_field_carets_fade_on_appkit_s_blink_and_restart_when_moved() {
742 let (mut ui, mut text) = focused_field("one");
743 let start = Instant::now();
744 let mut at = |ui: &mut Ui, millis: u64| {
745 ui.begin([400.0, 300.0], 2.0, start + Duration::from_millis(millis));
746 text_field(
747 ui,
748 field(),
749 &mut text,
750 "Filter",
751 Spec {
752 size: [px(200.0), px(26.0)],
753 pad: [6.0, 0.0],
754 ..Spec::default()
755 },
756 );
757 ui.end();
758 let caret = ui.theme.caret;
759 let opacity = ui
760 .layers()
761 .iter()
762 .flat_map(|layer| match layer {
763 Layer::Primitives(layer) => layer.primitives.as_slice(),
764 Layer::Custom { .. } => &[],
765 })
766 .find_map(|primitive| match primitive {
767 Primitive::RoundedRect { rect, color, .. } if color[..3] == caret[..3] => {
768 assert_eq!(rect[2] - rect[0], draw::edit::CARET_WIDTH);
769 Some(color[3])
770 }
771 _ => None,
772 })
773 .unwrap_or(0.0);
774 (opacity, ui.wake_at().map(|wake| wake - start))
775 };
776 assert_eq!(at(&mut ui, 0), (1.0, Some(Duration::from_micros(687_500))));
777 let theme = Theme::dark();
778 let faded = draw::edit::caret_color(theme.caret, theme.base, 0.75)[3];
779 assert_eq!(at(&mut ui, 700).0, faded);
780 assert_eq!(at(&mut ui, 900).0, 0.0);
781 ui.event(key(NamedKey::ArrowRight));
782 assert_eq!(
783 at(&mut ui, 910),
784 (1.0, Some(Duration::from_micros(1_597_500)))
785 );
786 ui.window_focused = false;
787 assert_eq!(
788 at(&mut ui, 920),
789 (0.0, None),
790 "an inactive window shows no caret"
791 );
792}
793
794#[test]
795#[cfg_attr(not(target_vendor = "apple"), ignore = "macOS editing conventions")]
796fn text_fields_move_and_select_by_word() {
797 let (mut ui, mut text) = focused_field("one two three");
798 ui.event(key(NamedKey::End));
799 ui.event(Event::Modifiers(ModifiersState::ALT));
800 ui.event(key(NamedKey::ArrowLeft));
801 ui.event(key(NamedKey::ArrowLeft));
802 field_frame(&mut ui, &mut text);
803 ui.event(Event::Modifiers(
804 ModifiersState::ALT | ModifiersState::SHIFT,
805 ));
806 ui.event(key(NamedKey::ArrowRight));
807 field_frame(&mut ui, &mut text);
808 ui.event(Event::Modifiers(ModifiersState::empty()));
809 ui.event(typed("2"));
810 field_frame(&mut ui, &mut text);
811 assert_eq!(text, "one 2 three");
812}
813
814#[test]
815#[cfg_attr(
816 target_vendor = "apple",
817 ignore = "GTK and Windows editing conventions"
818)]
819fn text_fields_follow_gtk_and_windows_chords() {
820 let (mut ui, mut text) = focused_field("one two three");
821 ui.event(key(NamedKey::End));
822 ui.event(Event::Modifiers(ModifiersState::CONTROL));
823 ui.event(key(NamedKey::ArrowLeft));
824 ui.event(key(NamedKey::ArrowLeft));
825 field_frame(&mut ui, &mut text);
826 ui.event(Event::Modifiers(
827 ModifiersState::CONTROL | ModifiersState::SHIFT,
828 ));
829 ui.event(key(NamedKey::ArrowRight));
830 field_frame(&mut ui, &mut text);
831 ui.event(Event::Modifiers(ModifiersState::empty()));
832 ui.event(typed("2"));
833 field_frame(&mut ui, &mut text);
834 assert_eq!(text, "one 2 three");
835 ui.event(Event::Modifiers(ModifiersState::CONTROL));
836 ui.event(key(NamedKey::Backspace));
837 field_frame(&mut ui, &mut text);
838 assert_eq!(text, "one three");
839 ui.event(Event::Modifiers(ModifiersState::empty()));
840 ui.event(key(NamedKey::Home));
841 ui.event(typed("<"));
842 ui.event(key(NamedKey::End));
843 ui.event(typed(">"));
844 field_frame(&mut ui, &mut text);
845 assert_eq!(text, "<one three>");
846 ui.event(Event::Modifiers(ModifiersState::CONTROL));
847 ui.event(typed("a"));
848 field_frame(&mut ui, &mut text);
849 ui.event(Event::Modifiers(ModifiersState::empty()));
850 ui.event(key(NamedKey::Delete));
851 field_frame(&mut ui, &mut text);
852 assert_eq!(text, "");
853}
854
855#[test]
856#[cfg_attr(not(target_vendor = "apple"), ignore = "macOS editing conventions")]
857fn text_fields_delete_by_word_and_line_chords() {
858 let (mut ui, mut text) = focused_field("one two three");
859 ui.event(key(NamedKey::End));
860 ui.event(Event::Modifiers(ModifiersState::ALT));
861 ui.event(key(NamedKey::Backspace));
862 field_frame(&mut ui, &mut text);
863 assert_eq!(text, "one two ");
864 ui.event(Event::Modifiers(ModifiersState::CONTROL));
865 ui.event(typed("b"));
866 ui.event(typed("k"));
867 field_frame(&mut ui, &mut text);
868 assert_eq!(text, "one two");
869 ui.event(Event::Modifiers(ModifiersState::SUPER));
870 ui.event(key(NamedKey::Backspace));
871 field_frame(&mut ui, &mut text);
872 assert_eq!(text, "");
873}
874
875#[test]
876fn text_fields_double_press_selects_a_word_and_drags_by_words() {
877 let (mut ui, mut text) = focused_field("one two three");
878 let at = Instant::now();
879 point_to(&mut ui, &text, 5);
880 press(&mut ui, at, true);
881 press(&mut ui, at, false);
882 field_frame(&mut ui, &mut text);
883 press(&mut ui, at + Duration::from_millis(100), true);
884 field_frame(&mut ui, &mut text);
885 assert_eq!(ui.states[&field()].selection.text_range(), 4..7);
886 point_to(&mut ui, &text, 10);
887 field_frame(&mut ui, &mut text);
888 assert_eq!(ui.states[&field()].selection.text_range(), 4..13);
889 point_to(&mut ui, &text, 1);
890 field_frame(&mut ui, &mut text);
891 press(&mut ui, at + Duration::from_millis(200), false);
892 field_frame(&mut ui, &mut text);
893 let selection = ui.states[&field()].selection;
894 assert_eq!(
895 (selection.anchor().index(), selection.focus().index()),
896 (7, 0),
897 "reversing keeps the pressed word"
898 );
899}
900
901#[test]
902fn text_fields_drag_select_and_shift_press_extends() {
903 let (mut ui, mut text) = focused_field("one two three");
904 let at = Instant::now();
905 point_to(&mut ui, &text, 4);
906 press(&mut ui, at, true);
907 field_frame(&mut ui, &mut text);
908 point_to(&mut ui, &text, 7);
909 field_frame(&mut ui, &mut text);
910 press(&mut ui, at, false);
911 ui.event(Event::Modifiers(ModifiersState::SHIFT));
912 field_frame(&mut ui, &mut text);
913 assert_eq!(ui.states[&field()].selection.text_range(), 4..7);
914 point_to(&mut ui, &text, 13);
915 press(&mut ui, at + Duration::from_secs(1), true);
916 press(&mut ui, at + Duration::from_secs(1), false);
917 field_frame(&mut ui, &mut text);
918 assert_eq!(ui.states[&field()].selection.text_range(), 4..13);
919 ui.event(Event::Modifiers(ModifiersState::empty()));
920 field_frame(&mut ui, &mut text);
921 ui.event(key(NamedKey::ArrowLeft));
922 ui.event(typed("_"));
923 field_frame(&mut ui, &mut text);
924 assert_eq!(
925 text, "one _two three",
926 "Left collapses to the selection's start"
927 );
928}
929
930#[test]
931fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {
932 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
933 let mut offset = 0.0;
934 let build = |ui: &mut Ui, offset: &mut f32| {
935 frame(ui, |ui| {
936 ui.open(
937 "view",
938 Spec {
939 flags: Flags::CUSTOM,
940 size: [px(200.0), px(218.0)],
941 ..Spec::default()
942 },
943 );
944 if let Some(chosen) = scrollbar(
945 ui,
946 "bar",
947 Axis::Y,
948 *offset,
949 [0.0, 1000.0],
950 400.0,
951 true,
952 [0.5; 4],
953 ) {
954 *offset = chosen;
955 }
956 ui.close();
957 });
958 };
959 build(&mut ui, &mut offset);
960 build(&mut ui, &mut offset);
961 let thumb = Id::ROOT.child("view").child("bar");
962 let rect = ui.laid_out(thumb).unwrap();
963 assert_eq!(
964 rect,
965 [190.0, 4.0, 196.0, 61.0],
966 "a 400 of 1400 share of a 200 track"
967 );
968 let at = Instant::now();
969 ui.event(Event::PointerMoved([193.0, 10.0]));
970 ui.event(Event::Button {
971 button: MouseButton::Left,
972 pressed: true,
973 at,
974 });
975 build(&mut ui, &mut offset);
976 assert_eq!(offset, 0.0, "a press alone keeps the offset");
977 ui.event(Event::PointerMoved([193.0, 500.0]));
978 build(&mut ui, &mut offset);
979 assert_eq!(offset, 1000.0);
980 build(&mut ui, &mut offset);
981 assert_eq!(ui.laid_out(thumb).unwrap(), [190.0, 147.0, 196.0, 204.0]);
982 ui.event(Event::PointerMoved([193.0, -500.0]));
983 build(&mut ui, &mut offset);
984 assert_eq!(offset, 0.0);
985}
986
987#[test]
988fn a_scrollbar_without_a_corner_ends_as_far_from_its_end_as_from_its_start() {
989 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
990 for _ in 0..2 {
991 frame(&mut ui, |ui| {
992 ui.open(
993 "view",
994 Spec {
995 flags: Flags::CUSTOM,
996 size: [px(200.0), px(208.0)],
997 ..Spec::default()
998 },
999 );
1000 scrollbar(
1001 ui,
1002 "bar",
1003 Axis::Y,
1004 1000.0,
1005 [0.0, 1000.0],
1006 400.0,
1007 false,
1008 [0.5; 4],
1009 );
1010 ui.close();
1011 });
1012 }
1013 let rect = ui.laid_out(Id::ROOT.child("view").child("bar")).unwrap();
1014 assert_eq!(rect[3], 204.0);
1015}
1016
1017#[test]
1018fn system_scrollers_step_page_and_drag_by_the_parts_the_platform_paints() {
1019 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1020 // Arrows together at the far end, as 10.6 places them.
1021 ui.scrollers = Some(Scrollers {
1022 thickness: 15.0,
1023 paint: Box::new(|request: &Scroller| {
1024 let slot = request.length - 30.0;
1025 let knob = slot * request.proportion;
1026 let start = request.value * (slot - knob);
1027 PaintedScroller {
1028 image: RasterImage::new(
1029 [15, request.length as u32],
1030 vec![0; 60 * request.length as usize],
1031 )
1032 .unwrap(),
1033 knob: [start, start + knob],
1034 slot: [0.0, slot],
1035 decrement: [slot, slot + 15.0],
1036 increment: [slot + 15.0, request.length],
1037 }
1038 }),
1039 });
1040 let mut offset = 0.0;
1041 let build = |ui: &mut Ui, offset: &mut f32| {
1042 frame(ui, |ui| {
1043 ui.open(
1044 "view",
1045 Spec {
1046 flags: Flags::CUSTOM,
1047 size: [px(200.0), px(215.0)],
1048 ..Spec::default()
1049 },
1050 );
1051 if let Some(chosen) = scrollbar(
1052 ui,
1053 "bar",
1054 Axis::Y,
1055 *offset,
1056 [0.0, 1000.0],
1057 400.0,
1058 true,
1059 [0.5; 4],
1060 ) {
1061 *offset = chosen;
1062 }
1063 ui.close();
1064 });
1065 };
1066 build(&mut ui, &mut offset);
1067 build(&mut ui, &mut offset);
1068 let bar = Id::ROOT.child("view").child("bar");
1069 assert_eq!(
1070 ui.laid_out(bar).unwrap(),
1071 [185.0, 0.0, 200.0, 200.0],
1072 "the corner stays clear"
1073 );
1074 let at = Instant::now();
1075 let click = |ui: &mut Ui, y: f32, pressed: bool| {
1076 ui.event(Event::PointerMoved([190.0, y]));
1077 ui.event(Event::Button {
1078 button: MouseButton::Left,
1079 pressed,
1080 at,
1081 });
1082 };
1083 click(&mut ui, 190.0, true);
1084 build(&mut ui, &mut offset);
1085 assert_eq!(
1086 offset, 20.0,
1087 "the increment arrow steps a twentieth of the view"
1088 );
1089 click(&mut ui, 190.0, false);
1090 build(&mut ui, &mut offset);
1091 // Below the knob, 170 × 400 / 1400 long, the track pages.
1092 click(&mut ui, 160.0, true);
1093 build(&mut ui, &mut offset);
1094 assert_eq!(offset, 400.0);
1095 click(&mut ui, 160.0, false);
1096 build(&mut ui, &mut offset);
1097 click(&mut ui, 60.0, true);
1098 build(&mut ui, &mut offset);
1099 ui.event(Event::PointerMoved([190.0, 500.0]));
1100 build(&mut ui, &mut offset);
1101 assert_eq!(offset, 1000.0, "the knob drags to the end");
1102}
1103
1104fn items() -> Vec<popup::Item<'static>> {
1105 let item = |text| popup::Item {
1106 text,
1107 ..popup::Item::default()
1108 };
1109 vec![
1110 item("Cut"),
1111 item("Copy"),
1112 popup::Item {
1113 disabled: true,
1114 ..item("Paste")
1115 },
1116 popup::Item {
1117 separated: true,
1118 ..item("Select all")
1119 },
1120 ]
1121}
1122
1123fn menu_id() -> Id {
1124 Id::ROOT.child("menu")
1125}
1126
1127/// Builds a frame with a button filling the window under the menu, and returns the
1128/// button's signal and the item chosen.
1129fn menu_frame(
1130 ui: &mut Ui,
1131 anchor: impl FnOnce(&mut Ui) -> Anchor,
1132 filter: Option<&str>,
1133) -> (Signal, Option<usize>) {
1134 let mut result = (Signal::default(), None);
1135 frame(ui, |ui| {
1136 let under = ui.leaf(
1137 "under",
1138 Spec {
1139 flags: Flags::CLICKABLE,
1140 size: [fill(), fill()],
1141 ..Spec::default()
1142 },
1143 );
1144 let anchor = anchor(ui);
1145 let chosen = popup::menu(ui, menu_id(), anchor, &items(), filter);
1146 result = (under, chosen);
1147 });
1148 result
1149}
1150
1151/// A box floating at `rect` in the window, built at its root, for a popup to open beside.
1152fn spot(ui: &mut Ui, rect: [f32; 4]) -> Id {
1153 let [left, top, right, bottom] = rect;
1154 let id = Id::ROOT.child(("spot", rect.map(f32::to_bits)));
1155 ui.open_as(
1156 id,
1157 Spec {
1158 flags: Flags::FLOAT,
1159 size: [px(right - left), px(bottom - top)],
1160 position: [left, top],
1161 ..Spec::default()
1162 },
1163 );
1164 ui.close();
1165 id
1166}
1167
1168/// Below a box at (20, 20), 80 by 20.
1169fn below(ui: &mut Ui) -> Anchor {
1170 Anchor::Below(spot(ui, [20.0, 20.0, 100.0, 40.0]))
1171}
1172
1173/// A window with the menu open and faded in `below`.
1174fn open_menu(filter: Option<&str>) -> Ui {
1175 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1176 menu_frame(&mut ui, below, filter);
1177 ui.open_popup(menu_id());
1178 for _ in 0..40 {
1179 menu_frame(&mut ui, below, filter);
1180 }
1181 ui
1182}
1183
1184/// The centre of the menu's row for `item`, from the latest layout.
1185fn row(ui: &Ui, item: usize) -> [f32; 2] {
1186 let rect = ui.laid_out(menu_id().child("rows").child(item)).unwrap();
1187 [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0]
1188}
1189
1190fn click(ui: &mut Ui, point: [f32; 2]) {
1191 let at = Instant::now();
1192 ui.event(Event::PointerMoved(point));
1193 press(ui, at, true);
1194 press(ui, at, false);
1195}
1196
1197/// Two tool buttons with tooltips, the second at the window's trailing edge; returns
1198/// their tooltips' rectangles.
1199fn tip_frame(ui: &mut Ui) -> [Option<[f32; 4]>; 2] {
1200 let mut tips = [Id::ROOT; 2];
1201 frame(ui, |ui| {
1202 for (index, x) in [0.0, 380.0].into_iter().enumerate() {
1203 let id = ui.open(
1204 ("tool", index),
1205 Spec {
1206 flags: Flags::CLICKABLE | Flags::FLOAT,
1207 size: [px(20.0), px(20.0)],
1208 position: [x, 0.0],
1209 ..Spec::default()
1210 },
1211 );
1212 ui.close();
1213 popup::tooltip(ui, "Bold", "Ctrl+B", Some("Makes the selected text bold."));
1214 tips[index] = id.child("tooltip");
1215 }
1216 });
1217 tips.map(|tip| ui.laid_out(tip))
1218}
1219
1220/// Frames the pointer rests through for `millis`, returning the tooltips as they end.
1221fn rest(ui: &mut Ui, millis: u32) -> [Option<[f32; 4]>; 2] {
1222 let mut tips = [None; 2];
1223 for _ in 0..millis / 16 {
1224 tips = tip_frame(ui);
1225 }
1226 tips
1227}
1228
1229#[test]
1230fn tooltips_wait_then_switch_at_once_and_hide_on_press_until_left() {
1231 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1232 tip_frame(&mut ui);
1233 ui.event(Event::PointerMoved([10.0, 10.0]));
1234 assert_eq!(rest(&mut ui, 400), [None; 2]);
1235 assert!(ui.wake_at().is_some(), "the delay wakes the host");
1236 let [Some(first), None] = rest(&mut ui, 200) else {
1237 panic!("the first tooltip shows after its delay");
1238 };
1239 assert_eq!(
1240 first[..2],
1241 [POPUP_MARGIN, 20.0 + popup::PAD],
1242 "off the window's edge"
1243 );
1244
1245 ui.event(Event::PointerMoved([390.0, 10.0]));
1246 let [None, Some(second)] = tip_frame(&mut ui) else {
1247 panic!("moving to another shows its tooltip at once");
1248 };
1249 assert_eq!(second[2], 400.0 - POPUP_MARGIN, "kept inside the window");
1250
1251 press(&mut ui, Instant::now(), true);
1252 press(&mut ui, Instant::now(), false);
1253 assert_eq!(
1254 rest(&mut ui, 1000),
1255 [None; 2],
1256 "a press hides it while hovered"
1257 );
1258 ui.event(Event::PointerMoved([200.0, 10.0]));
1259 rest(&mut ui, 100);
1260 ui.event(Event::PointerMoved([10.0, 10.0]));
1261 assert_eq!(
1262 rest(&mut ui, 100),
1263 [None; 2],
1264 "after a press it waits again"
1265 );
1266 assert!(rest(&mut ui, 500)[0].is_some());
1267}
1268
1269#[test]
1270fn a_tooltip_centres_under_its_control_and_shifts_only_to_stay_in_the_window() {
1271 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1272 let mut shown_at = |x: f32| {
1273 ui.event(Event::PointerMoved([x + 10.0, 110.0]));
1274 let mut tip = None;
1275 for _ in 0..40 {
1276 frame(&mut ui, |ui| {
1277 let id = ui.open(
1278 "tool",
1279 Spec {
1280 flags: Flags::CLICKABLE | Flags::FLOAT,
1281 size: [px(20.0), px(20.0)],
1282 position: [x, 100.0],
1283 ..Spec::default()
1284 },
1285 );
1286 ui.close();
1287 popup::tooltip(ui, "Bold", "Ctrl+B", Some("Makes the selected text bold."));
1288 tip = Some(id.child("tooltip"));
1289 });
1290 }
1291 ui.laid_out(tip.unwrap()).unwrap()
1292 };
1293 let middle = shown_at(190.0);
1294 assert_eq!(middle[1], 120.0 + popup::PAD, "under the control");
1295 assert!(
1296 ((middle[0] + middle[2]) / 2.0 - 200.0).abs() <= 0.5,
1297 "centred on it: {middle:?}"
1298 );
1299 let width = middle[2] - middle[0];
1300 assert_eq!(
1301 shown_at(2.0)[..3],
1302 [POPUP_MARGIN, 120.0 + popup::PAD, POPUP_MARGIN + width]
1303 );
1304 assert_eq!(
1305 shown_at(378.0)[0],
1306 400.0 - POPUP_MARGIN - width,
1307 "shifted only to fit"
1308 );
1309}
1310
1311#[test]
1312fn a_popup_opens_beside_a_box_first_laid_out_in_the_same_frame() {
1313 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1314 ui.open_popup(menu_id());
1315 menu_frame(&mut ui, below, None);
1316 assert_eq!(ui.laid_out(menu_id()).unwrap()[..2], [20.0, 44.0]);
1317}
1318
1319#[test]
1320fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() {
1321 let mut ui = open_menu(None);
1322 let rect = ui.laid_out(menu_id()).unwrap();
1323 assert_eq!(
1324 [rect[0], rect[1]],
1325 [20.0, 44.0],
1326 "under the anchor, a gap below"
1327 );
1328 assert_eq!(rect[2] - rect[0], 140.0, "no narrower than a menu");
1329 let height = rect[3] - rect[1];
1330 let corner = |ui: &mut Ui| Anchor::Below(spot(ui, [300.0, 260.0, 380.0, 280.0]));
1331 menu_frame(&mut ui, corner, None);
1332 menu_frame(&mut ui, corner, None);
1333 assert_eq!(
1334 ui.laid_out(menu_id()),
1335 Some([240.0, 256.0 - height, 380.0, 256.0]),
1336 "above, level with the anchor's far edge"
1337 );
1338 let edge = |ui: &mut Ui| Anchor::Right(spot(ui, [350.0, 100.0, 390.0, 120.0]));
1339 menu_frame(&mut ui, edge, None);
1340 menu_frame(&mut ui, edge, None);
1341 assert_eq!(
1342 ui.laid_out(menu_id()),
1343 Some([210.0, 96.0, 350.0, 96.0 + height]),
1344 "a submenu that cannot fit to the right opens to the left, its first row level"
1345 );
1346}
1347
1348#[test]
1349fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
1350 let inside = |rect: [f32; 4], window: [f32; 2]| {
1351 rect[0] >= POPUP_MARGIN
1352 && rect[1] >= POPUP_MARGIN
1353 && rect[2] <= window[0] - POPUP_MARGIN
1354 && rect[3] <= window[1] - POPUP_MARGIN
1355 };
1356 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1357 let narrow = [100.0, 300.0];
1358 let menu = |ui: &mut Ui| {
1359 sized_frame(ui, narrow, |ui| {
1360 let anchor = below(ui);
1361 popup::menu(ui, menu_id(), anchor, &items(), None);
1362 });
1363 };
1364 menu(&mut ui);
1365 ui.open_popup(menu_id());
1366 for _ in 0..40 {
1367 menu(&mut ui);
1368 }
1369 let rect = ui.laid_out(menu_id()).unwrap();
1370 assert!(inside(rect, narrow), "{rect:?} leaves the window's margin");
1371 assert_eq!(
1372 rect[2] - rect[0],
1373 100.0 - 2.0 * POPUP_MARGIN,
1374 "as wide as fits"
1375 );
1376 let row = ui.laid_out(menu_id().child("rows").child(0_u64)).unwrap();
1377 assert!(row[2] <= rect[2], "its rows narrow with it");
1378
1379 // A dialog taller than a short window scrolls what it holds.
1380 let short = [400.0, 120.0];
1381 let dialog = Id::ROOT.child("dialog");
1382 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1383 let build = |ui: &mut Ui| {
1384 sized_frame(ui, short, |ui| {
1385 ui.open_as(
1386 dialog,
1387 Spec {
1388 axis: Axis::Y,
1389 size: [px(600.0), children()],
1390 anchor: Some(Anchor::Dialog),
1391 ..Spec::default()
1392 },
1393 );
1394 for line in 0..10 {
1395 ui.leaf(
1396 line,
1397 Spec {
1398 size: [fill(), px(30.0)],
1399 text: Some("Line"),
1400 // Wider than the squeezed dialog leaves it.
1401 pad: [250.0, 0.0],
1402 ..Spec::default()
1403 },
1404 );
1405 }
1406 ui.close();
1407 });
1408 };
1409 build(&mut ui);
1410 ui.open_popup(dialog);
1411 for _ in 0..40 {
1412 build(&mut ui);
1413 }
1414 let rect = ui.laid_out(dialog).unwrap();
1415 assert!(inside(rect, short), "{rect:?} leaves the window's margin");
1416 let first = ui.laid_out(dialog.child(0)).unwrap();
1417 ui.event(Event::PointerMoved([200.0, 60.0]));
1418 ui.event(Event::Wheel([0.0, -100.0]));
1419 for _ in 0..40 {
1420 build(&mut ui);
1421 }
1422 let scrolled = ui.laid_out(dialog.child(0)).unwrap();
1423 assert!(scrolled[1] < first[1], "the wheel scrolls it");
1424 for layer in ui.layers() {
1425 let Layer::Primitives(layer) = layer else {
1426 continue;
1427 };
1428 for primitive in &layer.primitives {
1429 if let draw::Primitive::Text {
1430 clip: Some(clip), ..
1431 } = primitive
1432 {
1433 assert!(clip[0] <= clip[2], "{clip:?} is inverted");
1434 }
1435 }
1436 }
1437}
1438
1439#[test]
1440fn popups_show_at_once_by_their_anchor() {
1441 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1442 menu_frame(&mut ui, below, None);
1443 ui.open_popup(menu_id());
1444 menu_frame(&mut ui, below, None);
1445 menu_frame(&mut ui, below, None);
1446 assert_eq!(
1447 ui.laid_out(menu_id()).unwrap()[1],
1448 44.0,
1449 "right under the anchor"
1450 );
1451 ui.close_popup(menu_id());
1452 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1453 ui.open_popup(menu_id());
1454 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1455 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1456 assert_eq!(ui.laid_out(menu_id()).unwrap()[..2], [50.0, 50.0]);
1457}
1458
1459#[test]
1460fn a_press_outside_dismisses_without_reaching_what_is_beneath() {
1461 let mut ui = open_menu(None);
1462 click(&mut ui, [300.0, 250.0]);
1463 let (under, chosen) = menu_frame(&mut ui, below, None);
1464 assert!(!ui.popup_open(menu_id()) && chosen.is_none());
1465 assert!(!under.pressed && !under.clicked);
1466 click(&mut ui, [300.0, 250.0]);
1467 let (under, _) = menu_frame(&mut ui, below, None);
1468 assert!(under.clicked, "once closed, beneath takes presses again");
1469}
1470
1471#[test]
1472fn escape_dismisses_and_returns_the_focus() {
1473 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1474 let page = Id::ROOT.child("under");
1475 menu_frame(&mut ui, below, None);
1476 ui.set_focus(Some(page));
1477 ui.open_popup(menu_id());
1478 menu_frame(&mut ui, below, None);
1479 assert_eq!(ui.focused(), Some(menu_id()));
1480 ui.event(key(NamedKey::Escape));
1481 menu_frame(&mut ui, below, None);
1482 assert!(!ui.popup_open(menu_id()));
1483 assert_eq!(ui.focused(), Some(page));
1484}
1485
1486#[test]
1487fn input_over_a_popup_stays_with_it() {
1488 let mut ui = open_menu(None);
1489 ui.event(Event::PointerMoved(row(&ui, 1)));
1490 let (under, _) = menu_frame(&mut ui, below, None);
1491 assert!(!under.hovered);
1492 let point = row(&ui, 1);
1493 click(&mut ui, point);
1494 let (under, chosen) = menu_frame(&mut ui, below, None);
1495 assert_eq!(chosen, Some(1));
1496 assert!(!under.pressed && !under.clicked);
1497 assert!(!ui.popup_open(menu_id()), "choosing closes it");
1498}
1499
1500#[test]
1501fn disabled_items_ignore_the_pointer() {
1502 let mut ui = open_menu(None);
1503 let point = row(&ui, 2);
1504 click(&mut ui, point);
1505 let (_, chosen) = menu_frame(&mut ui, below, None);
1506 assert_eq!(chosen, None);
1507 assert!(ui.popup_open(menu_id()));
1508}
1509
1510#[test]
1511fn keys_move_the_highlight_past_disabled_items() {
1512 let mut ui = open_menu(None);
1513 for (keys, highlight) in [
1514 (vec![NamedKey::ArrowDown], 0),
1515 (vec![NamedKey::ArrowDown, NamedKey::ArrowDown], 3),
1516 (vec![NamedKey::ArrowDown], 3),
1517 (vec![NamedKey::ArrowUp], 1),
1518 (vec![NamedKey::End], 3),
1519 (vec![NamedKey::Home], 0),
1520 (vec![NamedKey::PageDown], 3),
1521 ] {
1522 for named in keys {
1523 ui.event(key(named));
1524 }
1525 menu_frame(&mut ui, below, None);
1526 assert_eq!(ui.popups[0].highlight, Some(highlight));
1527 }
1528 ui.event(key(NamedKey::Enter));
1529 assert_eq!(menu_frame(&mut ui, below, None).1, Some(3));
1530}
1531
1532#[test]
1533fn the_pointer_moves_the_highlight_only_when_it_moves() {
1534 let mut ui = open_menu(None);
1535 ui.event(Event::PointerMoved(row(&ui, 1)));
1536 menu_frame(&mut ui, below, None);
1537 ui.event(key(NamedKey::ArrowDown));
1538 menu_frame(&mut ui, below, None);
1539 menu_frame(&mut ui, below, None);
1540 assert_eq!(
1541 ui.popups[0].highlight,
1542 Some(3),
1543 "a still pointer keeps its row"
1544 );
1545}
1546
1547#[test]
1548fn headings_are_never_chosen_and_hide_while_filtered() {
1549 let mut ui = open_menu(Some("Filter"));
1550 let mut items = items();
1551 items.insert(
1552 3,
1553 popup::Item {
1554 text: "Selection",
1555 heading: true,
1556 separated: true,
1557 ..popup::Item::default()
1558 },
1559 );
1560 items[4].separated = false;
1561 let build = |ui: &mut Ui| {
1562 let mut chosen = None;
1563 frame(ui, |ui| {
1564 let anchor = below(ui);
1565 chosen = popup::menu(ui, menu_id(), anchor, &items, Some("Filter"));
1566 });
1567 chosen
1568 };
1569 build(&mut ui);
1570 ui.event(key(NamedKey::End));
1571 build(&mut ui);
1572 ui.event(key(NamedKey::ArrowUp));
1573 build(&mut ui);
1574 assert_eq!(
1575 ui.popups[0].highlight,
1576 Some(1),
1577 "keys skip the heading and Paste"
1578 );
1579 ui.event(typed("sel"));
1580 build(&mut ui);
1581 ui.event(key(NamedKey::Enter));
1582 assert_eq!(
1583 build(&mut ui),
1584 Some(4),
1585 "the query matches the item, not its heading"
1586 );
1587}
1588
1589#[test]
1590fn typing_filters_to_the_best_matches_first() {
1591 let mut ui = open_menu(Some("Filter"));
1592 assert_eq!(ui.focused(), Some(menu_id().child("filter")));
1593 for text in ["c", "t"] {
1594 ui.event(typed(text));
1595 }
1596 let settle = |ui: &mut Ui| {
1597 for _ in 0..30 {
1598 menu_frame(ui, below, Some("Filter"));
1599 }
1600 };
1601 settle(&mut ui);
1602 assert_eq!(ui.popups[0].query, "ct");
1603 let shown = |ui: &Ui| {
1604 let mut shown: Vec<_> = (0..4_usize)
1605 .filter_map(|item| {
1606 let rect = ui.laid_out(menu_id().child("rows").child(item))?;
1607 Some((rect[1] as i32, item))
1608 })
1609 .collect();
1610 shown.sort();
1611 shown.into_iter().map(|(_, item)| item).collect::<Vec<_>>()
1612 };
1613 assert_eq!(
1614 shown(&ui),
1615 [0, 3],
1616 "a word's start before the middle of one"
1617 );
1618 ui.event(key(NamedKey::Backspace));
1619 settle(&mut ui);
1620 assert_eq!(shown(&ui), [0, 1, 3], "equal matches keep their order");
1621 ui.event(key(NamedKey::ArrowDown));
1622 ui.event(key(NamedKey::Enter));
1623 assert_eq!(menu_frame(&mut ui, below, Some("Filter")).1, Some(1));
1624}
1625
1626#[test]
1627fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {
1628 let mut ui = open_menu(Some("Filter"));
1629 ui.event(typed("z"));
1630 menu_frame(&mut ui, below, Some("Filter"));
1631 ui.event(key(NamedKey::ArrowDown));
1632 let (_, chosen) = menu_frame(&mut ui, below, Some("Filter"));
1633 assert_eq!(chosen, None);
1634 assert!(ui.popup_open(menu_id()));
1635 ui.event(key(NamedKey::Enter));
1636 let (_, chosen) = menu_frame(&mut ui, below, Some("Filter"));
1637 assert_eq!(chosen, None);
1638 assert!(!ui.popup_open(menu_id()));
1639}
1640
1641#[test]
1642fn palette_commands_keep_the_origin_field_while_the_query_keeps_typing_focus() {
1643 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1644 let palette = Id::ROOT.child("palette");
1645 let page = Id::ROOT.child("page");
1646 let commands = items();
1647 let mut text = "kept".to_owned();
1648 let mut build = |ui: &mut Ui| {
1649 frame(ui, |ui| {
1650 text_field(
1651 ui,
1652 field(),
1653 &mut text,
1654 "Filter",
1655 Spec {
1656 size: [px(200.0), px(26.0)],
1657 ..Spec::default()
1658 },
1659 );
1660 ui.leaf(
1661 "page",
1662 Spec {
1663 size: [px(200.0), px(100.0)],
1664 flags: Flags::FOCUSABLE | Flags::CUSTOM,
1665 ..Spec::default()
1666 },
1667 );
1668 let _ = popup::palette(ui, palette, &[("", &commands)], "Search commands");
1669 });
1670 };
1671 build(&mut ui);
1672 for origin in [page, field()] {
1673 ui.set_focus(Some(origin));
1674 let expected = (origin == field()).then_some(origin);
1675 assert_eq!(ui.focused_field(Some(palette)), expected);
1676 ui.open_popup(palette);
1677 build(&mut ui);
1678 let query = palette.child("filter");
1679 assert_eq!(ui.focused_field(None), Some(query));
1680 assert_eq!(ui.focused_field(Some(palette)), expected);
1681 assert_eq!(ui.focused(), Some(query));
1682 ui.event(typed("Undo"));
1683 build(&mut ui);
1684 assert_eq!(popup::query(&ui, palette), Some("Undo"));
1685 assert_eq!(ui.focused_field(Some(palette)), expected);
1686 assert_eq!(ui.focused(), Some(query));
1687 ui.close_popup(palette);
1688 assert_eq!(ui.focused(), Some(origin));
1689 assert_eq!(ui.focused_field(None), expected);
1690 }
1691 assert_eq!(text, "kept");
1692}
1693
1694#[test]
1695fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {
1696 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1697 let palette = Id::ROOT.child("palette");
1698 let commands = items();
1699 let build = |ui: &mut Ui| {
1700 let mut chosen = None;
1701 frame(ui, |ui| {
1702 chosen = popup::palette(ui, palette, &[("", &commands)], "Search commands")
1703 });
1704 match chosen {
1705 Some(popup::Pick::Run(_, index)) => Some(index),
1706 _ => None,
1707 }
1708 };
1709 build(&mut ui);
1710 ui.open_popup(palette);
1711 for _ in 0..40 {
1712 build(&mut ui);
1713 }
1714 let rect = ui.laid_out(palette).unwrap();
1715 assert_eq!(rect[0] + rect[2], 400.0, "centred");
1716 assert_eq!(rect[1], 300.0 / 8.0, "an eighth down");
1717 ui.event(typed("a"));
1718 ui.event(key(NamedKey::Enter));
1719 assert_eq!(
1720 build(&mut ui),
1721 Some(3),
1722 "`all` begins a word; `Paste` is disabled"
1723 );
1724}
1725
1726#[test]
1727fn enter_runs_no_worse_match_past_a_disabled_best_one() {
1728 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1729 let palette = Id::ROOT.child("palette");
1730 let commands = [
1731 popup::Item {
1732 text: "Hide Spelling Errors",
1733 ..popup::Item::default()
1734 },
1735 popup::Item {
1736 text: "Spelling",
1737 disabled: true,
1738 ..popup::Item::default()
1739 },
1740 ];
1741 let build = |ui: &mut Ui| {
1742 let mut chosen = None;
1743 frame(ui, |ui| {
1744 chosen = popup::palette(ui, palette, &[("", &commands)], "Search commands")
1745 });
1746 chosen
1747 };
1748 build(&mut ui);
1749 ui.open_popup(palette);
1750 build(&mut ui);
1751 ui.event(typed("Spelling"));
1752 build(&mut ui);
1753 ui.event(key(NamedKey::Enter));
1754 assert_eq!(build(&mut ui), None);
1755 assert!(ui.popup_open(palette));
1756}
1757
1758#[test]
1759fn a_prefix_typed_in_the_palette_switches_what_it_lists() {
1760 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1761 let palette = Id::ROOT.child("palette");
1762 let [commands, places] = [items(), fonts()];
1763 let build = |ui: &mut Ui| {
1764 let mut chosen = None;
1765 frame(ui, |ui| {
1766 let modes: [(&str, &[popup::Item]); 2] = [(">", &commands), ("", &places)];
1767 chosen = popup::palette(ui, palette, &modes, "Go to");
1768 });
1769 chosen
1770 };
1771 build(&mut ui);
1772 popup::open_with(&mut ui, palette, ">");
1773 build(&mut ui);
1774 ui.event(typed("sel"));
1775 ui.event(key(NamedKey::Enter));
1776 assert_eq!(
1777 build(&mut ui),
1778 Some(popup::Pick::Run(0, 3)),
1779 "typed after the prefix"
1780 );
1781 popup::open_with(&mut ui, palette, ">");
1782 build(&mut ui);
1783 ui.event(key(NamedKey::Backspace));
1784 build(&mut ui);
1785 assert_eq!(popup::query(&ui, palette), Some(""));
1786 ui.event(typed("tnr"));
1787 ui.event(key(NamedKey::Enter));
1788 assert_eq!(
1789 build(&mut ui),
1790 Some(popup::Pick::Run(1, 42)),
1791 "back among the places"
1792 );
1793}
1794
1795#[test]
1796fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {
1797 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1798 let palette = Id::ROOT.child("palette");
1799 let actions = popup::actions(palette);
1800 let mut places = items();
1801 // The first repeats an item further on, as a recent page does.
1802 places[0].repeated = true;
1803 let build = |ui: &mut Ui| {
1804 let mut picked = None;
1805 frame(ui, |ui| {
1806 picked = popup::palette(ui, palette, &[("", &places)], "Go to");
1807 popup::menu(
1808 ui,
1809 actions,
1810 Anchor::Point([0.0; 2]),
1811 &items(),
1812 Some("Actions"),
1813 );
1814 });
1815 picked
1816 };
1817 build(&mut ui);
1818 ui.open_popup(palette);
1819 build(&mut ui);
1820 ui.event(key(NamedKey::ArrowDown));
1821 build(&mut ui);
1822 ui.event(Event::Modifiers(SHORTCUT));
1823 ui.event(typed("k"));
1824 assert_eq!(build(&mut ui), Some(popup::Pick::Actions(0, 0)));
1825 assert!(ui.popup_open(actions));
1826 assert_eq!(
1827 popup::query(&ui, palette),
1828 Some(""),
1829 "the chord types nothing"
1830 );
1831 ui.event(Event::Modifiers(ModifiersState::empty()));
1832 build(&mut ui);
1833 ui.event(key(NamedKey::Escape));
1834 build(&mut ui);
1835 assert!(
1836 ui.popup_open(palette) && !ui.popup_open(actions),
1837 "Escape goes back to the palette"
1838 );
1839 let row = ui.laid_out(palette.child("rows").child(3_u64)).unwrap();
1840 ui.event(Event::PointerMoved([row[0] + 4.0, row[1] + 4.0]));
1841 ui.event(Event::Button {
1842 button: MouseButton::Right,
1843 pressed: true,
1844 at: Instant::now(),
1845 });
1846 assert_eq!(build(&mut ui), Some(popup::Pick::Actions(0, 3)));
1847 assert!(ui.popup_open(actions));
1848 ui.event(key(NamedKey::Escape));
1849 build(&mut ui);
1850 ui.event(typed("c"));
1851 build(&mut ui);
1852 assert_eq!(
1853 ui.laid_out(palette.child("rows").child(0_u64)),
1854 None,
1855 "a repeated item shows only while unfiltered"
1856 );
1857}
1858
1859#[test]
1860fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {
1861 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1862 let grid = Id::ROOT.child("colours");
1863 let swatches: Vec<([f32; 4], &str)> = (0..6)
1864 .map(|index| ([index as f32, 0.0, 0.0, 1.0], ""))
1865 .collect();
1866 let build = |ui: &mut Ui| {
1867 let mut chosen = None;
1868 frame(ui, |ui| {
1869 let anchor = below(ui);
1870 chosen = popup::colors(ui, grid, anchor, "Automatic", &swatches, 3);
1871 });
1872 chosen
1873 };
1874 build(&mut ui);
1875 ui.open_popup(grid);
1876 build(&mut ui);
1877 for named in [
1878 NamedKey::ArrowDown,
1879 NamedKey::ArrowDown,
1880 NamedKey::ArrowDown,
1881 NamedKey::ArrowRight,
1882 NamedKey::Enter,
1883 ] {
1884 ui.event(key(named));
1885 }
1886 assert_eq!(build(&mut ui), Some(Some(swatches[4].0)));
1887 ui.open_popup(grid);
1888 for _ in 0..40 {
1889 build(&mut ui);
1890 }
1891 let rect = ui.laid_out(grid.child(("cell", 0_usize))).unwrap();
1892 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
1893 assert_eq!(build(&mut ui), Some(None));
1894}
1895
1896#[test]
1897fn colour_grids_highlight_the_swatch_pointed_at_until_the_pointer_leaves() {
1898 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1899 let grid = Id::ROOT.child("colours");
1900 let swatches = [([1.0, 0.0, 0.0, 1.0], ""), ([0.0, 1.0, 0.0, 1.0], "")];
1901 let build = |ui: &mut Ui| {
1902 frame(ui, |ui| {
1903 let anchor = below(ui);
1904 popup::colors(ui, grid, anchor, "Automatic", &swatches, 2);
1905 });
1906 };
1907 build(&mut ui);
1908 ui.open_popup(grid);
1909 for _ in 0..40 {
1910 build(&mut ui);
1911 }
1912 let rect = ui.laid_out(grid.child(("cell", 2_usize))).unwrap();
1913 ui.event(Event::PointerMoved([rect[0] + 4.0, rect[1] + 4.0]));
1914 build(&mut ui);
1915 assert_eq!(popup::highlight(&ui, grid), Some(2));
1916 ui.event(Event::PointerMoved([399.0, 299.0]));
1917 build(&mut ui);
1918 assert_eq!(popup::highlight(&ui, grid), None);
1919}
1920
1921#[test]
1922fn docked_popups_lie_inside_their_box_along_its_trailing_edge() {
1923 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1924 let dock = Id::ROOT.child("dock");
1925 let build = |ui: &mut Ui| {
1926 frame(ui, |ui| {
1927 let page = spot(ui, [20.0, 40.0, 380.0, 290.0]);
1928 if ui.popup_open(dock) {
1929 ui.open_as(
1930 dock,
1931 Spec {
1932 size: [px(100.0), px(200.0)],
1933 anchor: Some(Anchor::Dock(page)),
1934 ..Spec::default()
1935 },
1936 );
1937 ui.close();
1938 }
1939 });
1940 };
1941 build(&mut ui);
1942 ui.open_popup(dock);
1943 for _ in 0..40 {
1944 build(&mut ui);
1945 }
1946 let [left, top, right, bottom] = ui.laid_out(dock).unwrap();
1947 assert_eq!([left, top, right, bottom], [276.0, 44.0, 376.0, 244.0]);
1948}
1949
1950#[test]
1951fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {
1952 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1953 let grid = Id::ROOT.child("table");
1954 let build = |ui: &mut Ui| {
1955 let mut chosen = None;
1956 frame(ui, |ui| {
1957 let anchor = below(ui);
1958 chosen = popup::table_picker(ui, grid, anchor, [4, 3]);
1959 });
1960 chosen
1961 };
1962 build(&mut ui);
1963 ui.open_popup(grid);
1964 build(&mut ui);
1965 for named in [
1966 NamedKey::ArrowDown,
1967 NamedKey::ArrowRight,
1968 NamedKey::ArrowRight,
1969 NamedKey::ArrowDown,
1970 NamedKey::ArrowDown,
1971 NamedKey::Enter,
1972 ] {
1973 ui.event(key(named));
1974 }
1975 assert_eq!(build(&mut ui), Some([3, 3]));
1976 ui.open_popup(grid);
1977 for _ in 0..40 {
1978 build(&mut ui);
1979 }
1980 let rect = ui.laid_out(grid.child(("cell", 5_usize))).unwrap();
1981 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
1982 assert_eq!(build(&mut ui), Some([2, 2]));
1983}
1984
1985#[test]
1986fn popups_opened_within_a_popup_stay_above_it() {
1987 let mut ui = open_menu(None);
1988 let submenu = Id::ROOT.child("submenu");
1989 let build = |ui: &mut Ui, open: bool| {
1990 frame(ui, |ui| {
1991 ui.open_as(
1992 menu_id(),
1993 Spec {
1994 size: [px(100.0), px(100.0)],
1995 anchor: Some(Anchor::Point([20.0, 44.0])),
1996 ..Spec::default()
1997 },
1998 );
1999 if open {
2000 ui.open_popup(submenu);
2001 }
2002 ui.close();
2003 let anchor = Anchor::Right(spot(ui, [100.0, 50.0, 120.0, 70.0]));
2004 popup::menu(ui, submenu, anchor, &items(), None);
2005 })
2006 };
2007 build(&mut ui, true);
2008 build(&mut ui, false);
2009 assert!(ui.popup_open(menu_id()) && ui.popup_open(submenu));
2010 ui.event(key(NamedKey::Escape));
2011 build(&mut ui, false);
2012 assert!(ui.popup_open(menu_id()) && !ui.popup_open(submenu));
2013}
2014
2015#[test]
2016fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_choice() {
2017 let submenu = Id::ROOT.child("submenu");
2018 let mut entries = items();
2019 entries[1].submenu = true;
2020 let build = |ui: &mut Ui| {
2021 let mut chosen = None;
2022 frame(ui, |ui| {
2023 let anchor = below(ui);
2024 chosen = popup::menu(ui, menu_id(), anchor, &entries, None);
2025 popup::submenus(ui, menu_id(), &entries, |item| {
2026 (item == 1).then_some(submenu)
2027 });
2028 popup::menu(ui, submenu, anchor, &items(), None);
2029 });
2030 chosen
2031 };
2032 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2033 build(&mut ui);
2034 ui.open_popup(menu_id());
2035 for _ in 0..40 {
2036 build(&mut ui);
2037 }
2038 ui.event(Event::PointerMoved(row(&ui, 1)));
2039 for _ in 0..10 {
2040 build(&mut ui);
2041 }
2042 assert!(!ui.popup_open(submenu), "not while the pointer passes");
2043 for _ in 0..30 {
2044 build(&mut ui);
2045 }
2046 assert!(ui.popup_open(submenu), "once it rests");
2047 let [menu, beside] = [menu_id(), submenu].map(|id| ui.laid_out(id).unwrap());
2048 assert!(
2049 beside[0] >= menu[2] - 1.0,
2050 "beside the menu, not below the anchor"
2051 );
2052 ui.event(key(NamedKey::Escape));
2053 for _ in 0..40 {
2054 build(&mut ui);
2055 }
2056 assert!(
2057 ui.popup_open(menu_id()) && !ui.popup_open(submenu),
2058 "Escape closes only the submenu, which stays shut while the pointer rests"
2059 );
2060 let point = row(&ui, 1);
2061 click(&mut ui, point);
2062 assert_eq!(build(&mut ui), None, "a click opens it, not a choice");
2063 build(&mut ui);
2064 assert!(ui.popup_open(menu_id()) && ui.popup_open(submenu));
2065 let first = ui.laid_out(submenu.child("rows").child(0_u64)).unwrap();
2066 click(&mut ui, [first[0] + 4.0, first[1] + 4.0]);
2067 build(&mut ui);
2068 assert!(
2069 !ui.popup_open(menu_id()) && !ui.popup_open(submenu),
2070 "a choice ends both"
2071 );
2072}
2073
2074/// Keys in order, counting the lookups a list makes.
2075struct Keyed {
2076 keys: Vec<u64>,
2077 at: HashMap<u64, usize>,
2078 lookups: std::cell::Cell<usize>,
2079}
2080
2081impl Keyed {
2082 fn new(keys: impl IntoIterator<Item = u64>) -> Self {
2083 let keys: Vec<u64> = keys.into_iter().collect();
2084 let at = keys
2085 .iter()
2086 .enumerate()
2087 .map(|(index, key)| (*key, index))
2088 .collect();
2089 Self {
2090 keys,
2091 at,
2092 lookups: Default::default(),
2093 }
2094 }
2095}
2096
2097impl Rows for Keyed {
2098 fn count(&self) -> usize {
2099 self.keys.len()
2100 }
2101
2102 fn key(&self, index: usize) -> u64 {
2103 self.lookups.set(self.lookups.get() + 1);
2104 self.keys[index]
2105 }
2106
2107 fn find(&self, key: u64) -> Option<usize> {
2108 self.lookups.set(self.lookups.get() + 1);
2109 self.at.get(&key).copied()
2110 }
2111}
2112
2113const LIST_ROW: f32 = 26.0;
2114const VIEW: f32 = 10.0 * LIST_ROW;
2115
2116fn list_id() -> Id {
2117 Id::ROOT.child("list")
2118}
2119
2120/// Builds a frame of a list over `rows`, ten rows in view.
2121fn list_frame(
2122 ui: &mut Ui,
2123 rows: &Keyed,
2124 selected: &mut Option<u64>,
2125 keys: &[NamedKey],
2126) -> Option<usize> {
2127 let mut clicked = None;
2128 frame(ui, |ui| {
2129 let list = List {
2130 rows,
2131 row: LIST_ROW,
2132 keys,
2133 hover_selects: false,
2134 };
2135 let spec = Spec {
2136 size: [px(200.0), px(VIEW)],
2137 ..Spec::default()
2138 };
2139 clicked = crate::list(ui, list_id(), spec, list, selected, |ui, row| {
2140 ui.leaf(
2141 "label",
2142 Spec {
2143 size: [fill(), fill()],
2144 fill: row.selected.then_some([1.0; 4]),
2145 ..Spec::default()
2146 },
2147 );
2148 });
2149 });
2150 clicked
2151}
2152
2153fn row_top(ui: &Ui, key: u64) -> Option<f32> {
2154 ui.laid_out(list_id().child(key)).map(|rect| rect[1])
2155}
2156
2157/// Frames until the list's animations end.
2158fn settle_list(ui: &mut Ui, rows: &Keyed, selected: &mut Option<u64>) {
2159 for _ in 0..40 {
2160 list_frame(ui, rows, selected, &[]);
2161 }
2162 assert!(!ui.wants_frame());
2163}
2164
2165#[test]
2166fn lists_build_only_the_rows_in_view() {
2167 let mut boxes = Vec::new();
2168 for count in [20, 200_000] {
2169 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2170 let rows = Keyed::new(0..count);
2171 let mut selected = None;
2172 settle_list(&mut ui, &rows, &mut selected);
2173 rows.lookups.set(0);
2174 list_frame(&mut ui, &rows, &mut selected, &[]);
2175 assert!(rows.lookups.get() < 100, "{} lookups", rows.lookups.get());
2176 boxes.push(ui.nodes.len());
2177 }
2178 assert_eq!(boxes[0], boxes[1]);
2179}
2180
2181#[test]
2182fn lists_hold_the_selection_still_as_items_arrive_and_leave_above() {
2183 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2184 let mut rows = Keyed::new(0..1000);
2185 let mut selected = Some(500);
2186 settle_list(&mut ui, &rows, &mut selected);
2187 let at = row_top(&ui, 500).unwrap();
2188 assert!(
2189 (0.0..VIEW).contains(&at),
2190 "a new selection scrolls into view"
2191 );
2192 rows = Keyed::new((10_000..10_100).chain(0..1000));
2193 list_frame(&mut ui, &rows, &mut selected, &[]);
2194 assert_eq!(row_top(&ui, 500), Some(at), "a hundred rows arrived above");
2195 rows = Keyed::new((0..1000).filter(|key| key % 2 == 1 || *key >= 500));
2196 list_frame(&mut ui, &rows, &mut selected, &[]);
2197 assert_eq!(row_top(&ui, 500), Some(at), "half the rows above left");
2198 ui.event(Event::PointerMoved([50.0, 50.0]));
2199 ui.event(Event::Wheel([0.0, -60.0]));
2200 list_frame(&mut ui, &rows, &mut selected, &[]);
2201 rows = Keyed::new((20_000..20_050).chain(rows.keys.iter().copied()));
2202 list_frame(&mut ui, &rows, &mut selected, &[]);
2203 assert_eq!(
2204 row_top(&ui, 500),
2205 Some(at - 60.0),
2206 "the wheel moves the rows at once, and they hold through arrivals"
2207 );
2208 settle_list(&mut ui, &rows, &mut selected);
2209 assert_eq!(row_top(&ui, 500), Some(at - 60.0));
2210}
2211
2212#[test]
2213fn keys_move_the_selection_and_the_view_eases_after_it() {
2214 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2215 let rows = Keyed::new(0..1000);
2216 let mut selected = None;
2217 settle_list(&mut ui, &rows, &mut selected);
2218 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::ArrowDown]);
2219 assert_eq!(selected, Some(0), "from nothing, the first row in view");
2220 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::PageDown]);
2221 assert_eq!(selected, Some(10));
2222 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::End]);
2223 assert_eq!(selected, Some(999));
2224 assert!(
2225 ui.wants_frame() && row_top(&ui, 999) != Some(VIEW - LIST_ROW),
2226 "the view eases towards it"
2227 );
2228 settle_list(&mut ui, &rows, &mut selected);
2229 assert_eq!(
2230 row_top(&ui, 999),
2231 Some(VIEW - LIST_ROW),
2232 "the last row at the bottom"
2233 );
2234 list_frame(
2235 &mut ui,
2236 &rows,
2237 &mut selected,
2238 &[NamedKey::PageUp, NamedKey::ArrowUp],
2239 );
2240 assert_eq!(selected, Some(988));
2241 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::Home]);
2242 settle_list(&mut ui, &rows, &mut selected);
2243 assert_eq!((selected, row_top(&ui, 0)), (Some(0), Some(0.0)));
2244}
2245
2246#[test]
2247fn a_menu_runs_to_the_window_edge_before_it_scrolls() {
2248 let names: Vec<String> = (0..20).map(|index| format!("Item {index}")).collect();
2249 let items: Vec<_> = names
2250 .iter()
2251 .map(|text| popup::Item {
2252 text,
2253 ..popup::Item::default()
2254 })
2255 .collect();
2256 let build = |ui: &mut Ui, height: f32| {
2257 sized_frame(ui, [400.0, height], |ui| {
2258 let anchor = below(ui);
2259 popup::menu(ui, menu_id(), anchor, &items, None);
2260 })
2261 };
2262 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2263 build(&mut ui, 800.0);
2264 ui.open_popup(menu_id());
2265 for _ in 0..3 {
2266 build(&mut ui, 800.0);
2267 }
2268 let bar = |ui: &Ui| ui.laid_out(menu_id().child("rows").child("bar"));
2269 assert!(bar(&ui).is_none(), "twenty rows fit a tall window whole");
2270 assert!(ui.laid_out(menu_id().child("rows").child(19u64)).is_some());
2271 for _ in 0..3 {
2272 build(&mut ui, 300.0);
2273 }
2274 assert!(bar(&ui).is_some(), "a short window scrolls them");
2275}
2276
2277#[test]
2278fn a_long_menu_scrolls_by_dragging_its_thumb() {
2279 let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect();
2280 let items: Vec<_> = names
2281 .iter()
2282 .map(|text| popup::Item {
2283 text,
2284 ..popup::Item::default()
2285 })
2286 .collect();
2287 let build = |ui: &mut Ui| {
2288 frame(ui, |ui| {
2289 let anchor = below(ui);
2290 popup::menu(ui, menu_id(), anchor, &items, None);
2291 })
2292 };
2293 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2294 build(&mut ui);
2295 ui.open_popup(menu_id());
2296 build(&mut ui);
2297 build(&mut ui);
2298 let thumb = ui.laid_out(menu_id().child("rows").child("bar")).unwrap();
2299 let top = ui
2300 .laid_out(menu_id().child("rows").child(0u64))
2301 .map(|rect| rect[1]);
2302 let at = Instant::now();
2303 let x = (thumb[0] + thumb[2]) / 2.0;
2304 ui.event(Event::PointerMoved([x, thumb[1] + 2.0]));
2305 press(&mut ui, at, true);
2306 build(&mut ui);
2307 ui.event(Event::PointerMoved([x, thumb[1] + 60.0]));
2308 build(&mut ui);
2309 build(&mut ui);
2310 assert!(ui.popup_open(menu_id()));
2311 assert_ne!(
2312 ui.laid_out(menu_id().child("rows").child(0u64))
2313 .map(|rect| rect[1]),
2314 top
2315 );
2316}
2317
2318#[test]
2319fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end() {
2320 let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect();
2321 let items: Vec<_> = names
2322 .iter()
2323 .map(|text| popup::Item {
2324 text,
2325 ..popup::Item::default()
2326 })
2327 .collect();
2328 let build = |ui: &mut Ui| {
2329 frame(ui, |ui| {
2330 let anchor = below(ui);
2331 popup::menu(ui, menu_id(), anchor, &items, None);
2332 })
2333 };
2334 let rows = menu_id().child("rows");
2335 let fade = |ui: &Ui| ui.nodes.iter().find(|node| node.id == rows).unwrap().fade;
2336 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2337 build(&mut ui);
2338 ui.open_popup(menu_id());
2339 build(&mut ui);
2340 build(&mut ui);
2341 assert_eq!(fade(&ui), [0.0, 12.0], "cut only at the bottom");
2342 let thumb = ui.laid_out(rows.child("bar")).unwrap();
2343 let x = (thumb[0] + thumb[2]) / 2.0;
2344 ui.event(Event::PointerMoved([x, thumb[1] + 2.0]));
2345 press(&mut ui, Instant::now(), true);
2346 build(&mut ui);
2347 ui.event(Event::PointerMoved([x, 1000.0]));
2348 build(&mut ui);
2349 build(&mut ui);
2350 assert_eq!(fade(&ui), [12.0, 0.0], "cut only at the top");
2351 let list = ui.laid_out(rows).unwrap();
2352 let thumb = ui.laid_out(rows.child("bar")).unwrap();
2353 assert_eq!(
2354 thumb[3],
2355 list[3] - 4.0,
2356 "the thumb ends as far from the bottom as it starts"
2357 );
2358}
2359
2360#[test]
2361fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() {
2362 let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect();
2363 let rows = menu_id().child("rows");
2364 for picker in [false, true] {
2365 let items: Vec<_> = names
2366 .iter()
2367 .enumerate()
2368 .map(|(index, text)| popup::Item {
2369 text,
2370 checked: Some(index == 30),
2371 current: picker && index == 30,
2372 ..popup::Item::default()
2373 })
2374 .collect();
2375 let build = |ui: &mut Ui| {
2376 frame(ui, |ui| {
2377 let anchor = below(ui);
2378 popup::menu(ui, menu_id(), anchor, &items, None);
2379 })
2380 };
2381 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2382 build(&mut ui);
2383 ui.open_popup(menu_id());
2384 for _ in 0..3 {
2385 build(&mut ui);
2386 }
2387 assert_eq!(ui.popups[0].highlight, picker.then_some(30));
2388 let top = ui.laid_out(rows).unwrap()[1];
2389 let first = ui.laid_out(rows.child(0u64)).map(|rect| rect[1]);
2390 assert_eq!(first == Some(top), !picker, "{first:?} under {top}");
2391 }
2392}
2393
2394const FONTS: [&str; 46] = [
2395 "American Typewriter",
2396 "Andale Mono",
2397 "Apple Braille",
2398 "Apple Chancery",
2399 "Arial",
2400 "Arial Black",
2401 "Arial Hebrew",
2402 "Arial Narrow",
2403 "Arial Rounded MT Bold",
2404 "Arial Unicode MS",
2405 "Avenir",
2406 "Baskerville",
2407 "Big Caslon",
2408 "Brush Script MT",
2409 "Chalkboard",
2410 "Charter",
2411 "Cochin",
2412 "Comic Sans MS",
2413 "Courier",
2414 "Courier New",
2415 "Didot",
2416 "Futura",
2417 "Geneva",
2418 "Georgia",
2419 "Gill Sans",
2420 "Helvetica",
2421 "Helvetica Neue",
2422 "Herculanum",
2423 "Hoefler Text",
2424 "Impact",
2425 "Lucida Grande",
2426 "Marker Felt",
2427 "Menlo",
2428 "Monaco",
2429 "Optima",
2430 "Palatino",
2431 "Papyrus",
2432 "Rockwell",
2433 "Savoye LET",
2434 "Skia",
2435 "Tahoma",
2436 "Times",
2437 "Times New Roman",
2438 "Trebuchet MS",
2439 "Verdana",
2440 "Zapfino",
2441];
2442
2443fn fonts() -> Vec<popup::Item<'static>> {
2444 FONTS
2445 .iter()
2446 .map(|text| popup::Item {
2447 text,
2448 ..popup::Item::default()
2449 })
2450 .collect()
2451}
2452
2453fn ranked(items: &[popup::Item], query: &str) -> Vec<&'static str> {
2454 let order = popup::Matches::new(items, query, 9.0).order;
2455 order.into_iter().map(|index| FONTS[index]).collect()
2456}
2457
2458#[test]
2459fn fuzzy_matches_rank_whole_words_first_and_ties_keep_their_order() {
2460 let fonts = fonts();
2461 let family = [
2462 "Arial",
2463 "Arial Black",
2464 "Arial Hebrew",
2465 "Arial Narrow",
2466 "Arial Rounded MT Bold",
2467 "Arial Unicode MS",
2468 ];
2469 assert_eq!(ranked(&fonts, "Arial"), family);
2470 assert_eq!(ranked(&fonts, "rial"), family, "case aside, mid-word");
2471 let ari = ranked(&fonts, "ari");
2472 assert_eq!(ari[..6], family, "letters together before letters apart");
2473 assert_eq!(
2474 ari[6..],
2475 ["American Typewriter", "Apple Braille", "Baskerville"]
2476 );
2477 assert_eq!(
2478 ranked(&fonts, "tnr"),
2479 ["Times New Roman"],
2480 "words' initials"
2481 );
2482 assert_eq!(
2483 ranked(&fonts, "ar bl")[0],
2484 "Arial Black",
2485 "words in any order"
2486 );
2487 assert_eq!(
2488 ranked(&fonts, "ms"),
2489 [
2490 "Arial Unicode MS",
2491 "Comic Sans MS",
2492 "Trebuchet MS",
2493 "Times",
2494 "Times New Roman"
2495 ]
2496 );
2497 assert!(ranked(&fonts, "Cafe").is_empty());
2498}
2499
2500#[test]
2501fn a_fallback_shows_only_when_a_query_leaves_nothing_else() {
2502 let mut items = fonts();
2503 let last = items.len() - 1;
2504 items[last].fallback = true;
2505 let order = |query| popup::Matches::new(&items, query, 9.0).order;
2506 assert!(!order("").contains(&last), "unfiltered");
2507 assert!(!order("Arial").contains(&last), "others match");
2508 assert_eq!(order("Cafe"), [last], "nothing else matches");
2509}
2510
2511#[test]
2512fn typing_more_never_reveals_what_a_shorter_query_filtered_out() {
2513 let fonts = fonts();
2514 for query in [
2515 "arial black",
2516 "times new roman",
2517 "helvetica neue",
2518 "comic sans ms",
2519 "tnr",
2520 ] {
2521 let mut shown = ranked(&fonts, "");
2522 for end in 1..=query.len() {
2523 let narrower = ranked(&fonts, &query[..end]);
2524 for font in &narrower {
2525 assert!(shown.contains(font), "{:?} revealed {font}", &query[..end]);
2526 }
2527 shown = narrower;
2528 }
2529 assert!(!shown.is_empty(), "{query}");
2530 }
2531}
2532
2533#[test]
2534fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {
2535 let fonts = fonts();
2536 let build = |ui: &mut Ui| {
2537 frame(ui, |ui| {
2538 let anchor = below(ui);
2539 popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"));
2540 })
2541 };
2542 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2543 build(&mut ui);
2544 ui.open_popup(menu_id());
2545 for _ in 0..12 {
2546 build(&mut ui);
2547 }
2548 let results = menu_id().child("rows");
2549 let height = |ui: &Ui| {
2550 let rect = ui.laid_out(results).unwrap();
2551 rect[3] - rect[1]
2552 };
2553 assert!(
2554 !ui.wants_frame() && height(&ui) > 5.0 * popup::MENU_ROW,
2555 "open at its full height"
2556 );
2557 ui.event(key(NamedKey::End));
2558 for _ in 0..40 {
2559 build(&mut ui);
2560 }
2561 // Each query shows its results where they settle in the frame it is typed.
2562 for query in ["t", "nr"] {
2563 ui.event(typed(query));
2564 build(&mut ui);
2565 let shown = [ui.laid_out(results), ui.laid_out(results.child(42_u64))];
2566 build(&mut ui);
2567 assert!(!ui.wants_frame(), "nothing eases");
2568 assert_eq!(
2569 [ui.laid_out(results), ui.laid_out(results.child(42_u64))],
2570 shown
2571 );
2572 }
2573 let row = ui.laid_out(results.child(42_u64)).unwrap();
2574 assert_eq!(
2575 row[1],
2576 ui.laid_out(results).unwrap()[1],
2577 "Times New Roman at the top"
2578 );
2579 assert_eq!(height(&ui), popup::MENU_ROW);
2580}
2581
2582/// A column `width` wide holding one label: its box, and the label as laid out.
2583fn fitted(
2584 ui: &mut Ui,
2585 text: &str,
2586 width: f32,
2587 overflow: Overflow,
2588 center: bool,
2589) -> ([f32; 4], Rc<Label>) {
2590 let mut id = None;
2591 frame(ui, |ui| {
2592 ui.open(
2593 "column",
2594 Spec {
2595 axis: Axis::Y,
2596 size: [px(width), children()],
2597 ..Spec::default()
2598 },
2599 );
2600 id = Some(ui.open(
2601 "label",
2602 Spec {
2603 size: [fill(), fit()],
2604 text: Some(text),
2605 overflow,
2606 center,
2607 pad: [4.0, 2.0],
2608 ..Spec::default()
2609 },
2610 ));
2611 ui.close();
2612 ui.close();
2613 });
2614 let node = ui.nodes.iter().find(|node| Some(node.id) == id).unwrap();
2615 (node.rect, node.label.clone().unwrap())
2616}
2617
2618fn advances(label: &Label) -> Vec<f32> {
2619 label
2620 .layout
2621 .lines()
2622 .map(|line| line.metrics().advance)
2623 .collect()
2624}
2625
2626#[test]
2627fn wrapped_labels_break_to_their_width_and_the_box_grows_to_their_height() {
2628 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2629 let text = "Conflicting changes are highlighted in red. This page cannot be edited.";
2630 let (_, label) = fitted(&mut ui, text, 1000.0, Overflow::Wrap, false);
2631 assert_eq!(
2632 advances(&label).len(),
2633 1,
2634 "a label that fits stays one line"
2635 );
2636 let line = label.size[1];
2637 let mut previous = 2;
2638 for width in [320.0, 200.0, 120.0, 60.0] {
2639 let (rect, label) = fitted(&mut ui, text, width, Overflow::Wrap, false);
2640 let lines = advances(&label);
2641 assert!(lines.len() >= previous, "{width}: {lines:?}");
2642 assert!(
2643 lines.iter().all(|line| *line <= width - 8.0 + 0.01),
2644 "{width}: {lines:?}"
2645 );
2646 assert!((rect[3] - rect[1] - 4.0 - label.size[1]).abs() <= 0.5);
2647 assert!((label.size[1] / line - lines.len() as f32).abs() < 0.1);
2648 assert_eq!(rect[2] - rect[0], width);
2649 previous = lines.len();
2650 }
2651}
2652
2653#[test]
2654fn wrapping_breaks_cjk_emoji_and_words_longer_than_the_line() {
2655 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2656 for text in [
2657 "東京都の天気は晴れのち曇り、明日は雨が降るでしょう",
2658 "🎉🎈🎂🎁🎊🎉🎈🎂🎁🎊🎉🎈🎂🎁🎊",
2659 "Supercalifragilisticexpialidocious-antidisestablishmentarianism",
2660 ] {
2661 let lines = advances(&fitted(&mut ui, text, 90.0, Overflow::Wrap, false).1);
2662 assert!(lines.len() > 2, "{text}: {lines:?}");
2663 assert!(lines.iter().all(|line| *line <= 82.01), "{text}: {lines:?}");
2664 }
2665}
2666
2667#[test]
2668fn centred_wrapped_lines_centre_across_the_box() {
2669 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2670 let (_, label) = fitted(
2671 &mut ui,
2672 "A long line that wraps, then short",
2673 150.0,
2674 Overflow::Wrap,
2675 true,
2676 );
2677 let last = label.layout.lines().last().unwrap();
2678 let Some(parley::PositionedLayoutItem::GlyphRun(run)) = last.items().next() else {
2679 panic!("no glyphs");
2680 };
2681 let room = 150.0 - 8.0 - last.metrics().advance;
2682 assert!(
2683 room > 20.0 && (run.offset() - room / 2.0).abs() < 1.0,
2684 "{}",
2685 run.offset()
2686 );
2687}
2688
2689#[test]
2690fn ellipsis_shortens_a_label_to_its_box_and_leaves_one_that_fits() {
2691 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2692 let text = "Meeting notes from the quarterly planning review";
2693 let (whole, label) = fitted(&mut ui, text, 1000.0, Overflow::Ellipsis, false);
2694 assert_eq!(label.key.0, text);
2695 for width in [200.0, 90.0, 30.0] {
2696 let (rect, label) = fitted(&mut ui, text, width, Overflow::Ellipsis, false);
2697 let (shown, lines) = (&label.key.0, advances(&label));
2698 assert!(shown.ends_with('…') && !shown.ends_with(" …"), "{shown}");
2699 assert!(text.starts_with(shown.trim_end_matches('…')));
2700 assert_eq!(lines.len(), 1);
2701 assert!(lines[0] <= width - 8.0 + 0.01, "{width}: {shown} {lines:?}");
2702 assert_eq!(rect[3] - rect[1], whole[3] - whole[1]);
2703 }
2704 let (_, label) = fitted(
2705 &mut ui,
2706 "東京都の天気は晴れのち曇り",
2707 80.0,
2708 Overflow::Ellipsis,
2709 false,
2710 );
2711 assert!(
2712 label.key.0.ends_with('…') && label.key.0.chars().count() > 1,
2713 "{}",
2714 label.key.0
2715 );
2716}
2717
2718#[test]
2719fn popups_ease_open_and_closed_then_stop_asking_for_frames() {
2720 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2721 let id = Id::ROOT.child("dialog");
2722 let build = |ui: &mut Ui| {
2723 if ui.popup_open(id) {
2724 ui.leaf(
2725 "dialog",
2726 Spec {
2727 size: [px(100.0), px(50.0)],
2728 fill: Some([1.0; 4]),
2729 anchor: Some(Anchor::Dialog),
2730 ..Spec::default()
2731 },
2732 );
2733 }
2734 };
2735 let motion = |ui: &Ui| {
2736 ui.layers().iter().find_map(|layer| match layer {
2737 Layer::Primitives(primitives) => primitives.motion,
2738 Layer::Custom { .. } => None,
2739 })
2740 };
2741 frame(&mut ui, |ui| {
2742 ui.open_popup(id);
2743 build(ui);
2744 });
2745 let first = motion(&ui).unwrap();
2746 assert!(first.opacity < 0.1 && first.zoom < 0.96 && first.tilt > 0.15);
2747 assert_eq!(
2748 first.pivot,
2749 [200.0, 300.0 / 8.0],
2750 "the dialog swings from its top centre, an eighth down"
2751 );
2752 for _ in 0..16 {
2753 frame(&mut ui, build);
2754 }
2755 assert_eq!(motion(&ui).map(|motion| motion.opacity), Some(1.0));
2756 assert!(!ui.wants_frame());
2757 ui.close_popup(id);
2758 frame(&mut ui, build);
2759 frame(&mut ui, build);
2760 let closing = motion(&ui).unwrap();
2761 assert!(closing.opacity < 1.0 && closing.opacity > 0.0);
2762 assert!(ui.wants_frame());
2763 for _ in 0..10 {
2764 frame(&mut ui, build);
2765 }
2766 assert_eq!(motion(&ui), None);
2767 assert!(!ui.wants_frame());
2768}
2769
2770#[test]
2771fn a_dragged_box_lands_past_the_middles_it_crossed_and_the_rest_slide_aside() {
2772 let spans = [[0.0, 40.0], [40.0, 60.0], [100.0, 20.0], [120.0, 50.0]];
2773 // Still over its own place, box 1 stays; past box 2's middle, it lands after it.
2774 assert_eq!(drop_slot(&spans, 1, 70.0), 1);
2775 assert_eq!(drop_slot(&spans, 1, 111.0), 2);
2776 assert_eq!(drop_slot(&spans, 1, 500.0), 3);
2777 assert_eq!(drop_slot(&spans, 3, -10.0), 0);
2778 assert_eq!(
2779 (0..4)
2780 .map(|index| slide(index, 1, 3, 60.0))
2781 .collect::<Vec<_>>(),
2782 [0.0, 0.0, -60.0, -60.0]
2783 );
2784 assert_eq!(
2785 (0..4)
2786 .map(|index| slide(index, 3, 1, 50.0))
2787 .collect::<Vec<_>>(),
2788 [0.0, 50.0, 50.0, 0.0]
2789 );
2790}
2791
2792/// Clicks the middle of `id`'s box as laid out now, where it must be hit.
2793fn click_box(ui: &mut Ui, id: Id) {
2794 let rect = ui.laid_out(id).unwrap();
2795 let middle = [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0];
2796 assert_eq!(
2797 ui.box_at(middle),
2798 Some(id),
2799 "the box is hit where it is laid out"
2800 );
2801 click(ui, middle);
2802}
2803
2804/// What a dialog's controls reported in a frame.
2805#[derive(Default)]
2806struct Reported {
2807 checked: bool,
2808 ok: bool,
2809 scheme: Option<usize>,
2810}
2811
2812/// A dialog holding a check box, a button, a field and a combo opening a menu of schemes.
2813fn dialog_frame(ui: &mut Ui, text: &mut String) -> Reported {
2814 let mut reported = Reported::default();
2815 frame(ui, |ui| {
2816 let dialog = Id::ROOT.child("dialog");
2817 if !ui.popup_open(dialog) {
2818 return;
2819 }
2820 ui.open_as(
2821 dialog,
2822 Spec {
2823 axis: Axis::Y,
2824 size: [px(240.0), children()],
2825 fill: Some([1.0; 4]),
2826 pad: [8.0, 8.0],
2827 anchor: Some(Anchor::Dialog),
2828 ..Spec::default()
2829 },
2830 );
2831 reported.checked = check_box(ui, "check", "Dark pages", false).clicked;
2832 reported.ok = button(ui, "ok", "OK").clicked;
2833 text_field(
2834 ui,
2835 Id::ROOT.child("name"),
2836 text,
2837 "",
2838 Spec {
2839 size: [fill(), px(26.0)],
2840 ..Spec::default()
2841 },
2842 );
2843 let combo = ui.id("combo");
2844 shell::combo(
2845 ui,
2846 "combo",
2847 "Color scheme",
2848 "Light",
2849 120.0,
2850 Id::ROOT.child("schemes"),
2851 true,
2852 );
2853 let items = ["System", "Light", "Dark"].map(|text| popup::Item {
2854 text,
2855 ..popup::Item::default()
2856 });
2857 let anchor = Anchor::Below(combo);
2858 reported.scheme = popup::menu(ui, Id::ROOT.child("schemes"), anchor, &items, None);
2859 ui.close();
2860 });
2861 reported
2862}
2863
2864#[test]
2865fn every_control_in_an_open_dialog_takes_clicks_where_it_rests() {
2866 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2867 let mut text = String::new();
2868 let dialog = Id::ROOT.child("dialog");
2869 dialog_frame(&mut ui, &mut text);
2870 ui.open_popup(dialog);
2871 let settle = |ui: &mut Ui, text: &mut String| {
2872 for _ in 0..20 {
2873 dialog_frame(ui, text);
2874 }
2875 assert!(!ui.wants_frame());
2876 };
2877 settle(&mut ui, &mut text);
2878 click_box(&mut ui, dialog.child("check"));
2879 assert!(dialog_frame(&mut ui, &mut text).checked);
2880 click_box(&mut ui, dialog.child("ok"));
2881 assert!(dialog_frame(&mut ui, &mut text).ok);
2882 click_box(&mut ui, Id::ROOT.child("name"));
2883 dialog_frame(&mut ui, &mut text);
2884 assert_eq!(ui.focused(), Some(Id::ROOT.child("name")));
2885 ui.event(typed("a"));
2886 dialog_frame(&mut ui, &mut text);
2887 assert_eq!(text, "a");
2888 click_box(&mut ui, dialog.child("combo"));
2889 dialog_frame(&mut ui, &mut text);
2890 let schemes = Id::ROOT.child("schemes");
2891 assert!(ui.popup_open(schemes) && ui.popup_open(dialog));
2892 settle(&mut ui, &mut text);
2893 click_box(&mut ui, schemes.child("rows").child(2_u64));
2894 assert_eq!(dialog_frame(&mut ui, &mut text).scheme, Some(2));
2895 assert!(!ui.popup_open(schemes) && ui.popup_open(dialog));
2896}
2897
2898#[test]
2899fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_clicks() {
2900 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2901 let fonts = fonts();
2902 // Inset, as a popup keeps off the window's edges.
2903 let combo = Id::ROOT.child("inset").child("combo");
2904 let mut chosen = None;
2905 let mut build = |ui: &mut Ui| {
2906 frame(ui, |ui| {
2907 ui.open(
2908 "inset",
2909 Spec {
2910 size: [children(), children()],
2911 pad: [10.0, 10.0],
2912 ..Spec::default()
2913 },
2914 );
2915 shell::combo(ui, "combo", "Font", "Calibri", 120.0, menu_id(), true);
2916 ui.close();
2917 chosen = popup::menu(ui, menu_id(), Anchor::Over(combo), &fonts, Some("Font"));
2918 });
2919 chosen
2920 };
2921 build(&mut ui);
2922 ui.open_popup(menu_id());
2923 build(&mut ui);
2924 let field = menu_id().child("filter");
2925 let [box_rect, first] = [ui.laid_out(combo).unwrap(), ui.laid_out(field).unwrap()];
2926 assert_eq!(first[..2], box_rect[..2], "the field starts on the box");
2927 assert!(first[2] - first[0] < box_rect[2] - box_rect[0] + 20.0);
2928 let shown = ui.laid_out(menu_id()).unwrap();
2929 let row = menu_id().child("rows").child(3_u64);
2930 let opening = ui.laid_out(row).unwrap();
2931 assert!(
2932 opening[2] > shown[2],
2933 "the rows lie out past the popup as it opens"
2934 );
2935 let layers = ui.layers();
2936 let moving: Vec<_> = layers
2937 .iter()
2938 .filter_map(|layer| match layer {
2939 Layer::Primitives(layer) => Some(layer),
2940 Layer::Custom { .. } => None,
2941 })
2942 .skip_while(|layer| layer.motion.is_none())
2943 .collect();
2944 let split = moving
2945 .iter()
2946 .position(|layer| layer.motion.is_none())
2947 .unwrap();
2948 assert!(
2949 moving[split..].iter().all(|layer| layer.motion.is_none()),
2950 "the field paints over the popup, which appears as one"
2951 );
2952 assert!(
2953 moving[..split]
2954 .iter()
2955 .filter(|layer| layer.round.is_some())
2956 .all(|layer| layer.round == Some((shown, ui.theme.menu().radius))),
2957 "the rows show only inside the popup's outline"
2958 );
2959 drop(layers);
2960 for _ in 0..20 {
2961 build(&mut ui);
2962 }
2963 assert!(!ui.wants_frame());
2964 let [panel, last] = [ui.laid_out(menu_id()).unwrap(), ui.laid_out(field).unwrap()];
2965 assert_eq!(last[2], panel[2] - 4.0, "the field widens across the popup");
2966 assert_eq!(ui.laid_out(row), Some(opening), "the rows never move");
2967 click_box(&mut ui, menu_id().child("rows").child(3_u64));
2968 assert_eq!(build(&mut ui), Some(3));
2969}
2970
2971#[test]
2972fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
2973 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2974 let fonts = fonts();
2975 let build = |ui: &mut Ui| {
2976 frame(ui, |ui| {
2977 let anchor = Anchor::Over(spot(ui, [330.0, 10.0, 390.0, 30.0]));
2978 _ = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"))
2979 })
2980 };
2981 build(&mut ui);
2982 ui.open_popup(menu_id());
2983 build(&mut ui);
2984 let field = menu_id().child("filter");
2985 let edges = |ui: &Ui| {
2986 [ui.laid_out(field).unwrap(), ui.laid_out(menu_id()).unwrap()].map(|rect| rect[2])
2987 };
2988 let first = ui.laid_out(field).unwrap();
2989 let row = menu_id().child("rows").child(3_u64);
2990 let opening = ui.laid_out(row).unwrap();
2991 assert_eq!(
2992 edges(&ui),
2993 [390.0, 394.0],
2994 "the trailing edges start on the box"
2995 );
2996 assert!(first[0] > 290.0, "the field starts near the box");
2997 for _ in 0..20 {
2998 build(&mut ui);
2999 }
3000 assert_eq!(edges(&ui), [390.0, 394.0], "the trailing edges hold");
3001 assert!(
3002 ui.laid_out(field).unwrap()[0] < first[0] - 30.0,
3003 "the field widens leftward"
3004 );
3005 assert_eq!(ui.laid_out(row), Some(opening), "the rows never move");
3006}
3007
3008fn built(ui: &Ui, id: Id) -> &Built {
3009 ui.nodes.iter().find(|node| node.id == id).unwrap()
3010}
3011
3012/// A split button at (100, 100) opening `split_menu()`; its signal.
3013fn split_frame(ui: &mut Ui) -> Signal {
3014 let mut signal = Signal::default();
3015 frame(ui, |ui| {
3016 ui.open(
3017 "bar",
3018 Spec {
3019 flags: Flags::FLOAT,
3020 size: [px(200.0), px(shell::TOOL)],
3021 position: [100.0, 100.0],
3022 ..Spec::default()
3023 },
3024 );
3025 signal = shell::split_button(ui, "split", "Split", CHECK_ICON, None, None, split_menu());
3026 let item = popup::Item {
3027 text: "Item",
3028 ..Default::default()
3029 };
3030 let anchor = below(ui);
3031 popup::menu(ui, split_menu(), anchor, &[item], None);
3032 ui.close();
3033 });
3034 signal
3035}
3036
3037const CHECK_ICON: &[&str] = popup::CHECK;
3038
3039fn split_menu() -> Id {
3040 Id::ROOT.child("split menu")
3041}
3042
3043fn split_part(part: &str) -> Id {
3044 Id::ROOT.child("bar").child("split").child(part)
3045}
3046
3047fn center(rect: [f32; 4]) -> [f32; 2] {
3048 [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0]
3049}
3050
3051#[test]
3052fn a_split_button_fills_its_button_alone_and_outlines_both_halves_from_its_arrow() {
3053 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3054 split_frame(&mut ui);
3055 let [button, arrow] = [split_part("button"), split_part("menu")];
3056 let face = button.child("face");
3057 let ring = split_part("ring");
3058 let settle = |ui: &mut Ui| {
3059 for _ in 0..60 {
3060 split_frame(ui);
3061 }
3062 };
3063 ui.event(Event::PointerMoved(center(ui.laid_out(button).unwrap())));
3064 settle(&mut ui);
3065 // The fill rounds past the button's clip, so it meets the arrow square.
3066 let [left, top, right, bottom] = ui.laid_out(button).unwrap();
3067 assert!(built(&ui, face).fill.is_some());
3068 assert_eq!(ui.laid_out(face), Some([left, top, right + 4.0, bottom]));
3069 assert!(built(&ui, button).flags.contains(Flags::CLIP));
3070 assert!(built(&ui, ring).border.is_none());
3071 ui.event(Event::PointerMoved(center(ui.laid_out(arrow).unwrap())));
3072 settle(&mut ui);
3073 assert!(built(&ui, face).fill.is_none());
3074 assert!(built(&ui, ring).border.is_some());
3075 assert_eq!(
3076 ui.laid_out(ring),
3077 Some([left, top, ui.laid_out(arrow).unwrap()[2], bottom])
3078 );
3079 // The arrow opens the menu, and the outline stays while it is open.
3080 let point = center(ui.laid_out(arrow).unwrap());
3081 click(&mut ui, point);
3082 assert!(!split_frame(&mut ui).clicked);
3083 assert!(ui.popup_open(split_menu()));
3084 ui.event(Event::PointerMoved([390.0, 290.0]));
3085 settle(&mut ui);
3086 assert!(built(&ui, ring).border.is_some());
3087 ui.close_popup(split_menu());
3088 settle(&mut ui);
3089 let point = center(ui.laid_out(button).unwrap());
3090 click(&mut ui, point);
3091 assert!(split_frame(&mut ui).clicked);
3092 assert!(!ui.popup_open(split_menu()));
3093}
3094
3095#[test]
3096fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {
3097 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3098 let items = [
3099 popup::Item {
3100 text: "Align left",
3101 icon: Some(CHECK_ICON),
3102 ..Default::default()
3103 },
3104 popup::Item {
3105 text: "Center",
3106 icon: Some(CHECK_ICON),
3107 current: true,
3108 ..Default::default()
3109 },
3110 ];
3111 let build = |ui: &mut Ui| {
3112 frame(ui, |ui| {
3113 ui.open(
3114 "bar",
3115 Spec {
3116 flags: Flags::FLOAT,
3117 size: [px(200.0), px(shell::TOOL)],
3118 position: [100.0, 100.0],
3119 ..Spec::default()
3120 },
3121 );
3122 let anchor = shell::menu_button(ui, "align", CHECK_ICON, None, menu_id());
3123 popup::menu(ui, menu_id(), anchor, &items, None);
3124 ui.close();
3125 });
3126 };
3127 build(&mut ui);
3128 let button = ui.laid_out(Id::ROOT.child("bar").child("align")).unwrap();
3129 // One target: the arrow opens the menu as the icon does.
3130 click(&mut ui, [button[2] - 3.0, center(button)[1]]);
3131 for _ in 0..40 {
3132 build(&mut ui);
3133 }
3134 assert!(ui.popup_open(menu_id()));
3135 let icon = ui
3136 .laid_out(
3137 menu_id()
3138 .child("rows")
3139 .child(0_usize)
3140 .child("item")
3141 .child("icon"),
3142 )
3143 .unwrap();
3144 assert_eq!(icon[0], button[0] + (shell::TOOL - ICON) / 2.0);
3145}
3146
3147#[test]
3148fn galleries_choose_across_their_groups_by_keys_and_clicks() {
3149 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3150 let gallery = Id::ROOT.child("gallery");
3151 let groups = [("Recent", 2), ("Library", 5)].map(|(heading, cells)| popup::Group {
3152 heading,
3153 ruled: false,
3154 cells,
3155 columns: 3,
3156 size: [30.0, 30.0],
3157 });
3158 let build = |ui: &mut Ui| {
3159 let mut chosen = None;
3160 frame(ui, |ui| {
3161 let anchor = below(ui);
3162 chosen = popup::gallery(ui, gallery, anchor, &groups, &[3], |ui, index| {
3163 ui.leaf(
3164 "label",
3165 Spec {
3166 size: [fill(), fill()],
3167 text: Some(["a", "b", "c", "d", "e", "f", "g"][index]),
3168 ..Spec::default()
3169 },
3170 );
3171 });
3172 });
3173 chosen
3174 };
3175 build(&mut ui);
3176 ui.open_popup(gallery);
3177 build(&mut ui);
3178 // Keys start from the current cell, the library's second.
3179 for named in [NamedKey::ArrowDown, NamedKey::ArrowLeft, NamedKey::Enter] {
3180 ui.event(key(named));
3181 }
3182 assert_eq!(build(&mut ui), Some(5));
3183 ui.open_popup(gallery);
3184 for _ in 0..40 {
3185 build(&mut ui);
3186 }
3187 let cell = |index: usize| ui.laid_out(gallery.child(("cell", index))).unwrap();
3188 // The library starts a row of its own under its heading.
3189 assert_eq!(cell(2)[0], cell(0)[0]);
3190 assert!(cell(2)[1] > cell(0)[3]);
3191 let point = center(cell(1));
3192 click(&mut ui, point);
3193 assert_eq!(build(&mut ui), Some(1));
3194}
3195
3196#[test]
3197fn desktop_menus_cut_or_fade_without_growing() {
3198 let motion = |ui: &Ui| {
3199 ui.layers().iter().find_map(|layer| match layer {
3200 Layer::Primitives(primitives) => primitives.motion,
3201 Layer::Custom { .. } => None,
3202 })
3203 };
3204 let at = |_: &mut Ui| Anchor::Point([50.0, 50.0]);
3205 let styled = |motion| {
3206 let mut theme = Theme::light();
3207 theme.desktop_menu = Some(Menu {
3208 motion,
3209 ..theme.menu()
3210 });
3211 let mut ui = Ui::new(theme, DOUBLE_CLICK);
3212 menu_frame(&mut ui, at, None);
3213 ui.open_popup(menu_id());
3214 menu_frame(&mut ui, at, None);
3215 ui
3216 };
3217
3218 let mut ui = styled(PopupMotion::Cut);
3219 let shown = motion(&ui).unwrap();
3220 assert_eq!([shown.opacity, shown.zoom, shown.tilt], [1.0, 1.0, 0.0]);
3221 ui.close_popup(menu_id());
3222 menu_frame(&mut ui, at, None);
3223 assert_eq!(motion(&ui), None, "gone the frame it closes");
3224 assert!(!ui.wants_frame());
3225
3226 let mut ui = styled(PopupMotion::Fade([0.15, 0.6]));
3227 let first = motion(&ui).unwrap();
3228 assert!(first.opacity < 0.2 && first.zoom == 1.0 && first.tilt == 0.0);
3229 for _ in 0..10 {
3230 menu_frame(&mut ui, at, None);
3231 }
3232 assert_eq!(motion(&ui).map(|motion| motion.opacity), Some(1.0));
3233 ui.close_popup(menu_id());
3234 // 96 ms into the fade out, OutQuart leaves about half.
3235 for _ in 0..7 {
3236 menu_frame(&mut ui, at, None);
3237 }
3238 let closing = motion(&ui).unwrap();
3239 assert!((closing.opacity - (1.0 - 0.096 / 0.6_f32).powi(4)).abs() < 0.01);
3240 assert!(ui.wants_frame());
3241}
3242
3243/// A row 200 wide of twelve section tabs with `active` open; returns the row's id.
3244fn tab_row(ui: &mut Ui, active: usize) -> Id {
3245 let names = [
3246 "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven",
3247 "Twelve",
3248 ];
3249 let tabs: Vec<_> = names
3250 .iter()
3251 .map(|name| (*name, [0.5, 0.6, 0.9, 1.0], false))
3252 .collect();
3253 let section = ui.theme.section([0.5, 0.6, 0.9, 1.0]);
3254 let row = Id::ROOT.child("tabs");
3255 frame(ui, |ui| {
3256 ui.open(
3257 "tab row",
3258 Spec {
3259 size: [px(200.0), px(28.0)],
3260 ..Spec::default()
3261 },
3262 );
3263 shell::section_tabs(
3264 ui, row, &tabs, active, None, None, None, &section, 28.0, [0.0; 4],
3265 );
3266 ui.close();
3267 });
3268 row
3269}
3270
3271/// Frames until the tabs' easing settles.
3272fn settle_tabs(ui: &mut Ui, active: usize) -> Id {
3273 let mut row = tab_row(ui, active);
3274 for _ in 0..60 {
3275 row = tab_row(ui, active);
3276 }
3277 row
3278}
3279
3280#[test]
3281fn overflowing_tabs_scroll_the_open_one_into_view() {
3282 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3283 let row = settle_tabs(&mut ui, 0);
3284 let view = ui.laid_out(row).unwrap();
3285 assert_eq!(view[2] - view[0], 200.0);
3286 let row = settle_tabs(&mut ui, 11);
3287 let last = ui.laid_out(shell::tab_id(row, 11)).unwrap();
3288 assert!(
3289 last[0] >= view[0] && last[2] <= view[2],
3290 "{last:?} in {view:?}"
3291 );
3292}
3293
3294#[test]
3295fn the_wheel_scrolls_overflowing_tabs_sideways_either_way_it_turns() {
3296 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3297 let row = settle_tabs(&mut ui, 0);
3298 let first = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3299 ui.event(Event::PointerMoved([100.0, 14.0]));
3300 ui.event(Event::Wheel([0.0, -40.0]));
3301 tab_row(&mut ui, 0);
3302 tab_row(&mut ui, 0);
3303 let scrolled = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3304 assert_eq!(first - scrolled, 40.0, "at once, as on the page");
3305 ui.event(Event::Wheel([30.0, 0.0]));
3306 tab_row(&mut ui, 0);
3307 tab_row(&mut ui, 0);
3308 let back = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3309 assert_eq!(back - scrolled, 30.0);
3310}
3311
3312#[test]
3313fn a_colour_picker_applies_its_colour_and_keeps_it_through_hsl() {
3314 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3315 let picker = Id::ROOT.child("picker");
3316 let build = |ui: &mut Ui| {
3317 let mut chosen = None;
3318 frame(ui, |ui| {
3319 let anchor = below(ui);
3320 chosen = popup::color_picker(
3321 ui,
3322 picker,
3323 anchor,
3324 "Custom Color",
3325 [0xd4, 0xf9, 0xf2],
3326 |_| [1.0; 4],
3327 );
3328 });
3329 chosen
3330 };
3331 build(&mut ui);
3332 ui.open_popup(picker);
3333 assert_eq!(build(&mut ui), None);
3334 ui.event(key(NamedKey::Enter));
3335 assert_eq!(build(&mut ui), Some([0xd4, 0xf9, 0xf2]));
3336 assert!(!ui.popup_open(picker));
3337}
3338
3339#[test]
3340fn password_fields_edit_the_characters_behind_their_bullets() {
3341 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3342 let mut text = String::new();
3343 let edit = |ui: &mut Ui, text: &mut String| {
3344 frame(ui, |ui| {
3345 password_field(
3346 ui,
3347 field(),
3348 text,
3349 "",
3350 Spec {
3351 size: [px(200.0), px(26.0)],
3352 pad: [6.0, 0.0],
3353 ..Spec::default()
3354 },
3355 );
3356 })
3357 };
3358 edit(&mut ui, &mut text);
3359 ui.set_focus(Some(field()));
3360 for typed_text in ["p", "ä", "ß"] {
3361 ui.event(typed(typed_text));
3362 }
3363 edit(&mut ui, &mut text);
3364 ui.event(key(NamedKey::ArrowLeft));
3365 ui.event(key(NamedKey::ArrowLeft));
3366 ui.event(typed("x"));
3367 ui.event(key(NamedKey::Delete));
3368 edit(&mut ui, &mut text);
3369 assert_eq!(text, "pxß");
3370 ui.event(key(NamedKey::Backspace));
3371 edit(&mut ui, &mut text);
3372 assert_eq!(text, "pß");
3373}
3374
3375#[test]
3376fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_list() {
3377 let dialog = Id::ROOT.child("dialog");
3378 let side = dialog.child("body").child("side");
3379 let list = dialog.child("body").child("list");
3380 let loose = Extent {
3381 size: Size::Children,
3382 strictness: 0.0,
3383 };
3384 let build = |ui: &mut Ui, rows: usize| {
3385 frame(ui, |ui| {
3386 ui.open_as(
3387 dialog,
3388 Spec {
3389 axis: Axis::Y,
3390 size: [px(300.0), loose],
3391 anchor: Some(Anchor::Dialog),
3392 ..Spec::default()
3393 },
3394 );
3395 ui.leaf(
3396 "header",
3397 Spec {
3398 size: [fill(), px(30.0)],
3399 ..Spec::default()
3400 },
3401 );
3402 ui.open(
3403 "body",
3404 Spec {
3405 size: [fill(), loose],
3406 ..Spec::default()
3407 },
3408 );
3409 ui.leaf(
3410 "side",
3411 Spec {
3412 size: [px(100.0), fill()],
3413 ..Spec::default()
3414 },
3415 );
3416 ui.open(
3417 "list",
3418 Spec {
3419 flags: Flags::SCROLL | Flags::CLIP,
3420 axis: Axis::Y,
3421 size: [fill(), loose],
3422 ..Spec::default()
3423 },
3424 );
3425 for row in 0..rows {
3426 ui.leaf(
3427 row,
3428 Spec {
3429 size: [fill(), px(30.0)],
3430 ..Spec::default()
3431 },
3432 );
3433 }
3434 ui.close();
3435 ui.close();
3436 ui.leaf(
3437 "footer",
3438 Spec {
3439 size: [fill(), px(30.0)],
3440 ..Spec::default()
3441 },
3442 );
3443 ui.close();
3444 });
3445 };
3446 let height = |ui: &Ui, id: Id| ui.laid_out(id).map(|rect| rect[3] - rect[1]).unwrap();
3447 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3448 build(&mut ui, 2);
3449 ui.open_popup(dialog);
3450 for _ in 0..40 {
3451 build(&mut ui, 2);
3452 }
3453 assert_eq!(height(&ui, dialog), 120.0, "as tall as what it holds");
3454 assert_eq!(height(&ui, side), 60.0, "the side as tall as the list");
3455
3456 for _ in 0..40 {
3457 build(&mut ui, 20);
3458 }
3459 assert_eq!(
3460 height(&ui, dialog),
3461 225.0,
3462 "no taller than the window lets a dialog be"
3463 );
3464 assert_eq!(height(&ui, list), 165.0, "the list gives way");
3465 assert_eq!(height(&ui, side), 165.0);
3466
3467 ui.scroll_to(list, list.child(10_usize));
3468 for _ in 0..60 {
3469 build(&mut ui, 20);
3470 }
3471 let [top, row] = [list, list.child(10_usize)].map(|id| ui.laid_out(id).unwrap()[1]);
3472 assert!(
3473 (row - top).abs() < 0.5,
3474 "row 10 at {row}, the list's top at {top}"
3475 );
3476}
3477
3478#[test]
3479fn a_box_squeezed_past_its_inset_paints_empty_not_inverted() {
3480 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3481 frame(&mut ui, |ui| {
3482 ui.leaf(
3483 "row",
3484 Spec {
3485 size: [px(100.0), px(4.0)],
3486 fill: Some([1.0; 4]),
3487 gradient: Some([0.5, 0.5, 0.5, 1.0]),
3488 inset: [0.0, 0.0, 0.0, 10.0],
3489 ..Spec::default()
3490 },
3491 );
3492 });
3493 let rect = ui.display.iter().find_map(|item| match item {
3494 Display::Rect { rect, .. } => Some(*rect),
3495 _ => None,
3496 });
3497 let [left, top, right, bottom] = rect.expect("the row paints");
3498 assert!(
3499 right >= left && bottom >= top,
3500 "{:?}",
3501 [left, top, right, bottom]
3502 );
3503}
3504
3505#[test]
3506fn a_box_has_no_rectangle_until_it_is_first_laid_out() {
3507 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3508 let build = |ui: &mut Ui| {
3509 ui.leaf(
3510 "new",
3511 Spec {
3512 size: [px(40.0), px(20.0)],
3513 ..Spec::default()
3514 },
3515 );
3516 ui.laid_out(Id::ROOT.child("new"))
3517 };
3518 let mut seen = Vec::new();
3519 frame(&mut ui, |ui| seen.push(build(ui)));
3520 frame(&mut ui, |ui| seen.push(build(ui)));
3521 assert_eq!(
3522 seen[0], None,
3523 "a box placed against nothing would sit at the origin"
3524 );
3525 assert_eq!(seen[1], Some([0.0, 0.0, 40.0, 20.0]));
3526}