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 the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {
1643 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1644 let palette = Id::ROOT.child("palette");
1645 let commands = items();
1646 let build = |ui: &mut Ui| {
1647 let mut chosen = None;
1648 frame(ui, |ui| {
1649 chosen = popup::palette(ui, palette, &[("", &commands)], "Search commands")
1650 });
1651 match chosen {
1652 Some(popup::Pick::Run(_, index)) => Some(index),
1653 _ => None,
1654 }
1655 };
1656 build(&mut ui);
1657 ui.open_popup(palette);
1658 for _ in 0..40 {
1659 build(&mut ui);
1660 }
1661 let rect = ui.laid_out(palette).unwrap();
1662 assert_eq!(rect[0] + rect[2], 400.0, "centred");
1663 assert_eq!(rect[1], 300.0 / 8.0, "an eighth down");
1664 ui.event(typed("a"));
1665 ui.event(key(NamedKey::Enter));
1666 assert_eq!(
1667 build(&mut ui),
1668 Some(3),
1669 "`all` begins a word; `Paste` is disabled"
1670 );
1671}
1672
1673#[test]
1674fn enter_runs_no_worse_match_past_a_disabled_best_one() {
1675 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1676 let palette = Id::ROOT.child("palette");
1677 let commands = [
1678 popup::Item {
1679 text: "Hide Spelling Errors",
1680 ..popup::Item::default()
1681 },
1682 popup::Item {
1683 text: "Spelling",
1684 disabled: true,
1685 ..popup::Item::default()
1686 },
1687 ];
1688 let build = |ui: &mut Ui| {
1689 let mut chosen = None;
1690 frame(ui, |ui| {
1691 chosen = popup::palette(ui, palette, &[("", &commands)], "Search commands")
1692 });
1693 chosen
1694 };
1695 build(&mut ui);
1696 ui.open_popup(palette);
1697 build(&mut ui);
1698 ui.event(typed("Spelling"));
1699 build(&mut ui);
1700 ui.event(key(NamedKey::Enter));
1701 assert_eq!(build(&mut ui), None);
1702 assert!(ui.popup_open(palette));
1703}
1704
1705#[test]
1706fn a_prefix_typed_in_the_palette_switches_what_it_lists() {
1707 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1708 let palette = Id::ROOT.child("palette");
1709 let [commands, places] = [items(), fonts()];
1710 let build = |ui: &mut Ui| {
1711 let mut chosen = None;
1712 frame(ui, |ui| {
1713 let modes: [(&str, &[popup::Item]); 2] = [(">", &commands), ("", &places)];
1714 chosen = popup::palette(ui, palette, &modes, "Go to");
1715 });
1716 chosen
1717 };
1718 build(&mut ui);
1719 popup::open_with(&mut ui, palette, ">");
1720 build(&mut ui);
1721 ui.event(typed("sel"));
1722 ui.event(key(NamedKey::Enter));
1723 assert_eq!(
1724 build(&mut ui),
1725 Some(popup::Pick::Run(0, 3)),
1726 "typed after the prefix"
1727 );
1728 popup::open_with(&mut ui, palette, ">");
1729 build(&mut ui);
1730 ui.event(key(NamedKey::Backspace));
1731 build(&mut ui);
1732 assert_eq!(popup::query(&ui, palette), Some(""));
1733 ui.event(typed("tnr"));
1734 ui.event(key(NamedKey::Enter));
1735 assert_eq!(
1736 build(&mut ui),
1737 Some(popup::Pick::Run(1, 42)),
1738 "back among the places"
1739 );
1740}
1741
1742#[test]
1743fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {
1744 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1745 let palette = Id::ROOT.child("palette");
1746 let actions = popup::actions(palette);
1747 let mut places = items();
1748 // The first repeats an item further on, as a recent page does.
1749 places[0].repeated = true;
1750 let build = |ui: &mut Ui| {
1751 let mut picked = None;
1752 frame(ui, |ui| {
1753 picked = popup::palette(ui, palette, &[("", &places)], "Go to");
1754 popup::menu(
1755 ui,
1756 actions,
1757 Anchor::Point([0.0; 2]),
1758 &items(),
1759 Some("Actions"),
1760 );
1761 });
1762 picked
1763 };
1764 build(&mut ui);
1765 ui.open_popup(palette);
1766 build(&mut ui);
1767 ui.event(key(NamedKey::ArrowDown));
1768 build(&mut ui);
1769 ui.event(Event::Modifiers(SHORTCUT));
1770 ui.event(typed("k"));
1771 assert_eq!(build(&mut ui), Some(popup::Pick::Actions(0, 0)));
1772 assert!(ui.popup_open(actions));
1773 assert_eq!(
1774 popup::query(&ui, palette),
1775 Some(""),
1776 "the chord types nothing"
1777 );
1778 ui.event(Event::Modifiers(ModifiersState::empty()));
1779 build(&mut ui);
1780 ui.event(key(NamedKey::Escape));
1781 build(&mut ui);
1782 assert!(
1783 ui.popup_open(palette) && !ui.popup_open(actions),
1784 "Escape goes back to the palette"
1785 );
1786 let row = ui.laid_out(palette.child("rows").child(3_u64)).unwrap();
1787 ui.event(Event::PointerMoved([row[0] + 4.0, row[1] + 4.0]));
1788 ui.event(Event::Button {
1789 button: MouseButton::Right,
1790 pressed: true,
1791 at: Instant::now(),
1792 });
1793 assert_eq!(build(&mut ui), Some(popup::Pick::Actions(0, 3)));
1794 assert!(ui.popup_open(actions));
1795 ui.event(key(NamedKey::Escape));
1796 build(&mut ui);
1797 ui.event(typed("c"));
1798 build(&mut ui);
1799 assert_eq!(
1800 ui.laid_out(palette.child("rows").child(0_u64)),
1801 None,
1802 "a repeated item shows only while unfiltered"
1803 );
1804}
1805
1806#[test]
1807fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {
1808 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1809 let grid = Id::ROOT.child("colours");
1810 let swatches: Vec<([f32; 4], &str)> = (0..6)
1811 .map(|index| ([index as f32, 0.0, 0.0, 1.0], ""))
1812 .collect();
1813 let build = |ui: &mut Ui| {
1814 let mut chosen = None;
1815 frame(ui, |ui| {
1816 let anchor = below(ui);
1817 chosen = popup::colors(ui, grid, anchor, "Automatic", &swatches, 3);
1818 });
1819 chosen
1820 };
1821 build(&mut ui);
1822 ui.open_popup(grid);
1823 build(&mut ui);
1824 for named in [
1825 NamedKey::ArrowDown,
1826 NamedKey::ArrowDown,
1827 NamedKey::ArrowDown,
1828 NamedKey::ArrowRight,
1829 NamedKey::Enter,
1830 ] {
1831 ui.event(key(named));
1832 }
1833 assert_eq!(build(&mut ui), Some(Some(swatches[4].0)));
1834 ui.open_popup(grid);
1835 for _ in 0..40 {
1836 build(&mut ui);
1837 }
1838 let rect = ui.laid_out(grid.child(("cell", 0_usize))).unwrap();
1839 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
1840 assert_eq!(build(&mut ui), Some(None));
1841}
1842
1843#[test]
1844fn colour_grids_highlight_the_swatch_pointed_at_until_the_pointer_leaves() {
1845 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1846 let grid = Id::ROOT.child("colours");
1847 let swatches = [([1.0, 0.0, 0.0, 1.0], ""), ([0.0, 1.0, 0.0, 1.0], "")];
1848 let build = |ui: &mut Ui| {
1849 frame(ui, |ui| {
1850 let anchor = below(ui);
1851 popup::colors(ui, grid, anchor, "Automatic", &swatches, 2);
1852 });
1853 };
1854 build(&mut ui);
1855 ui.open_popup(grid);
1856 for _ in 0..40 {
1857 build(&mut ui);
1858 }
1859 let rect = ui.laid_out(grid.child(("cell", 2_usize))).unwrap();
1860 ui.event(Event::PointerMoved([rect[0] + 4.0, rect[1] + 4.0]));
1861 build(&mut ui);
1862 assert_eq!(popup::highlight(&ui, grid), Some(2));
1863 ui.event(Event::PointerMoved([399.0, 299.0]));
1864 build(&mut ui);
1865 assert_eq!(popup::highlight(&ui, grid), None);
1866}
1867
1868#[test]
1869fn docked_popups_lie_inside_their_box_along_its_trailing_edge() {
1870 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1871 let dock = Id::ROOT.child("dock");
1872 let build = |ui: &mut Ui| {
1873 frame(ui, |ui| {
1874 let page = spot(ui, [20.0, 40.0, 380.0, 290.0]);
1875 if ui.popup_open(dock) {
1876 ui.open_as(
1877 dock,
1878 Spec {
1879 size: [px(100.0), px(200.0)],
1880 anchor: Some(Anchor::Dock(page)),
1881 ..Spec::default()
1882 },
1883 );
1884 ui.close();
1885 }
1886 });
1887 };
1888 build(&mut ui);
1889 ui.open_popup(dock);
1890 for _ in 0..40 {
1891 build(&mut ui);
1892 }
1893 let [left, top, right, bottom] = ui.laid_out(dock).unwrap();
1894 assert_eq!([left, top, right, bottom], [276.0, 44.0, 376.0, 244.0]);
1895}
1896
1897#[test]
1898fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {
1899 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1900 let grid = Id::ROOT.child("table");
1901 let build = |ui: &mut Ui| {
1902 let mut chosen = None;
1903 frame(ui, |ui| {
1904 let anchor = below(ui);
1905 chosen = popup::table_picker(ui, grid, anchor, [4, 3]);
1906 });
1907 chosen
1908 };
1909 build(&mut ui);
1910 ui.open_popup(grid);
1911 build(&mut ui);
1912 for named in [
1913 NamedKey::ArrowDown,
1914 NamedKey::ArrowRight,
1915 NamedKey::ArrowRight,
1916 NamedKey::ArrowDown,
1917 NamedKey::ArrowDown,
1918 NamedKey::Enter,
1919 ] {
1920 ui.event(key(named));
1921 }
1922 assert_eq!(build(&mut ui), Some([3, 3]));
1923 ui.open_popup(grid);
1924 for _ in 0..40 {
1925 build(&mut ui);
1926 }
1927 let rect = ui.laid_out(grid.child(("cell", 5_usize))).unwrap();
1928 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
1929 assert_eq!(build(&mut ui), Some([2, 2]));
1930}
1931
1932#[test]
1933fn popups_opened_within_a_popup_stay_above_it() {
1934 let mut ui = open_menu(None);
1935 let submenu = Id::ROOT.child("submenu");
1936 let build = |ui: &mut Ui, open: bool| {
1937 frame(ui, |ui| {
1938 ui.open_as(
1939 menu_id(),
1940 Spec {
1941 size: [px(100.0), px(100.0)],
1942 anchor: Some(Anchor::Point([20.0, 44.0])),
1943 ..Spec::default()
1944 },
1945 );
1946 if open {
1947 ui.open_popup(submenu);
1948 }
1949 ui.close();
1950 let anchor = Anchor::Right(spot(ui, [100.0, 50.0, 120.0, 70.0]));
1951 popup::menu(ui, submenu, anchor, &items(), None);
1952 })
1953 };
1954 build(&mut ui, true);
1955 build(&mut ui, false);
1956 assert!(ui.popup_open(menu_id()) && ui.popup_open(submenu));
1957 ui.event(key(NamedKey::Escape));
1958 build(&mut ui, false);
1959 assert!(ui.popup_open(menu_id()) && !ui.popup_open(submenu));
1960}
1961
1962#[test]
1963fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_choice() {
1964 let submenu = Id::ROOT.child("submenu");
1965 let mut entries = items();
1966 entries[1].submenu = true;
1967 let build = |ui: &mut Ui| {
1968 let mut chosen = None;
1969 frame(ui, |ui| {
1970 let anchor = below(ui);
1971 chosen = popup::menu(ui, menu_id(), anchor, &entries, None);
1972 popup::submenus(ui, menu_id(), &entries, |item| {
1973 (item == 1).then_some(submenu)
1974 });
1975 popup::menu(ui, submenu, anchor, &items(), None);
1976 });
1977 chosen
1978 };
1979 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1980 build(&mut ui);
1981 ui.open_popup(menu_id());
1982 for _ in 0..40 {
1983 build(&mut ui);
1984 }
1985 ui.event(Event::PointerMoved(row(&ui, 1)));
1986 for _ in 0..10 {
1987 build(&mut ui);
1988 }
1989 assert!(!ui.popup_open(submenu), "not while the pointer passes");
1990 for _ in 0..30 {
1991 build(&mut ui);
1992 }
1993 assert!(ui.popup_open(submenu), "once it rests");
1994 let [menu, beside] = [menu_id(), submenu].map(|id| ui.laid_out(id).unwrap());
1995 assert!(
1996 beside[0] >= menu[2] - 1.0,
1997 "beside the menu, not below the anchor"
1998 );
1999 ui.event(key(NamedKey::Escape));
2000 for _ in 0..40 {
2001 build(&mut ui);
2002 }
2003 assert!(
2004 ui.popup_open(menu_id()) && !ui.popup_open(submenu),
2005 "Escape closes only the submenu, which stays shut while the pointer rests"
2006 );
2007 let point = row(&ui, 1);
2008 click(&mut ui, point);
2009 assert_eq!(build(&mut ui), None, "a click opens it, not a choice");
2010 build(&mut ui);
2011 assert!(ui.popup_open(menu_id()) && ui.popup_open(submenu));
2012 let first = ui.laid_out(submenu.child("rows").child(0_u64)).unwrap();
2013 click(&mut ui, [first[0] + 4.0, first[1] + 4.0]);
2014 build(&mut ui);
2015 assert!(
2016 !ui.popup_open(menu_id()) && !ui.popup_open(submenu),
2017 "a choice ends both"
2018 );
2019}
2020
2021/// Keys in order, counting the lookups a list makes.
2022struct Keyed {
2023 keys: Vec<u64>,
2024 at: HashMap<u64, usize>,
2025 lookups: std::cell::Cell<usize>,
2026}
2027
2028impl Keyed {
2029 fn new(keys: impl IntoIterator<Item = u64>) -> Self {
2030 let keys: Vec<u64> = keys.into_iter().collect();
2031 let at = keys
2032 .iter()
2033 .enumerate()
2034 .map(|(index, key)| (*key, index))
2035 .collect();
2036 Self {
2037 keys,
2038 at,
2039 lookups: Default::default(),
2040 }
2041 }
2042}
2043
2044impl Rows for Keyed {
2045 fn count(&self) -> usize {
2046 self.keys.len()
2047 }
2048
2049 fn key(&self, index: usize) -> u64 {
2050 self.lookups.set(self.lookups.get() + 1);
2051 self.keys[index]
2052 }
2053
2054 fn find(&self, key: u64) -> Option<usize> {
2055 self.lookups.set(self.lookups.get() + 1);
2056 self.at.get(&key).copied()
2057 }
2058}
2059
2060const LIST_ROW: f32 = 26.0;
2061const VIEW: f32 = 10.0 * LIST_ROW;
2062
2063fn list_id() -> Id {
2064 Id::ROOT.child("list")
2065}
2066
2067/// Builds a frame of a list over `rows`, ten rows in view.
2068fn list_frame(
2069 ui: &mut Ui,
2070 rows: &Keyed,
2071 selected: &mut Option<u64>,
2072 keys: &[NamedKey],
2073) -> Option<usize> {
2074 let mut clicked = None;
2075 frame(ui, |ui| {
2076 let list = List {
2077 rows,
2078 row: LIST_ROW,
2079 keys,
2080 hover_selects: false,
2081 };
2082 let spec = Spec {
2083 size: [px(200.0), px(VIEW)],
2084 ..Spec::default()
2085 };
2086 clicked = crate::list(ui, list_id(), spec, list, selected, |ui, row| {
2087 ui.leaf(
2088 "label",
2089 Spec {
2090 size: [fill(), fill()],
2091 fill: row.selected.then_some([1.0; 4]),
2092 ..Spec::default()
2093 },
2094 );
2095 });
2096 });
2097 clicked
2098}
2099
2100fn row_top(ui: &Ui, key: u64) -> Option<f32> {
2101 ui.laid_out(list_id().child(key)).map(|rect| rect[1])
2102}
2103
2104/// Frames until the list's animations end.
2105fn settle_list(ui: &mut Ui, rows: &Keyed, selected: &mut Option<u64>) {
2106 for _ in 0..40 {
2107 list_frame(ui, rows, selected, &[]);
2108 }
2109 assert!(!ui.wants_frame());
2110}
2111
2112#[test]
2113fn lists_build_only_the_rows_in_view() {
2114 let mut boxes = Vec::new();
2115 for count in [20, 200_000] {
2116 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2117 let rows = Keyed::new(0..count);
2118 let mut selected = None;
2119 settle_list(&mut ui, &rows, &mut selected);
2120 rows.lookups.set(0);
2121 list_frame(&mut ui, &rows, &mut selected, &[]);
2122 assert!(rows.lookups.get() < 100, "{} lookups", rows.lookups.get());
2123 boxes.push(ui.nodes.len());
2124 }
2125 assert_eq!(boxes[0], boxes[1]);
2126}
2127
2128#[test]
2129fn lists_hold_the_selection_still_as_items_arrive_and_leave_above() {
2130 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2131 let mut rows = Keyed::new(0..1000);
2132 let mut selected = Some(500);
2133 settle_list(&mut ui, &rows, &mut selected);
2134 let at = row_top(&ui, 500).unwrap();
2135 assert!(
2136 (0.0..VIEW).contains(&at),
2137 "a new selection scrolls into view"
2138 );
2139 rows = Keyed::new((10_000..10_100).chain(0..1000));
2140 list_frame(&mut ui, &rows, &mut selected, &[]);
2141 assert_eq!(row_top(&ui, 500), Some(at), "a hundred rows arrived above");
2142 rows = Keyed::new((0..1000).filter(|key| key % 2 == 1 || *key >= 500));
2143 list_frame(&mut ui, &rows, &mut selected, &[]);
2144 assert_eq!(row_top(&ui, 500), Some(at), "half the rows above left");
2145 ui.event(Event::PointerMoved([50.0, 50.0]));
2146 ui.event(Event::Wheel([0.0, -60.0]));
2147 list_frame(&mut ui, &rows, &mut selected, &[]);
2148 rows = Keyed::new((20_000..20_050).chain(rows.keys.iter().copied()));
2149 list_frame(&mut ui, &rows, &mut selected, &[]);
2150 assert_eq!(
2151 row_top(&ui, 500),
2152 Some(at - 60.0),
2153 "the wheel moves the rows at once, and they hold through arrivals"
2154 );
2155 settle_list(&mut ui, &rows, &mut selected);
2156 assert_eq!(row_top(&ui, 500), Some(at - 60.0));
2157}
2158
2159#[test]
2160fn keys_move_the_selection_and_the_view_eases_after_it() {
2161 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2162 let rows = Keyed::new(0..1000);
2163 let mut selected = None;
2164 settle_list(&mut ui, &rows, &mut selected);
2165 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::ArrowDown]);
2166 assert_eq!(selected, Some(0), "from nothing, the first row in view");
2167 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::PageDown]);
2168 assert_eq!(selected, Some(10));
2169 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::End]);
2170 assert_eq!(selected, Some(999));
2171 assert!(
2172 ui.wants_frame() && row_top(&ui, 999) != Some(VIEW - LIST_ROW),
2173 "the view eases towards it"
2174 );
2175 settle_list(&mut ui, &rows, &mut selected);
2176 assert_eq!(
2177 row_top(&ui, 999),
2178 Some(VIEW - LIST_ROW),
2179 "the last row at the bottom"
2180 );
2181 list_frame(
2182 &mut ui,
2183 &rows,
2184 &mut selected,
2185 &[NamedKey::PageUp, NamedKey::ArrowUp],
2186 );
2187 assert_eq!(selected, Some(988));
2188 list_frame(&mut ui, &rows, &mut selected, &[NamedKey::Home]);
2189 settle_list(&mut ui, &rows, &mut selected);
2190 assert_eq!((selected, row_top(&ui, 0)), (Some(0), Some(0.0)));
2191}
2192
2193#[test]
2194fn a_menu_runs_to_the_window_edge_before_it_scrolls() {
2195 let names: Vec<String> = (0..20).map(|index| format!("Item {index}")).collect();
2196 let items: Vec<_> = names
2197 .iter()
2198 .map(|text| popup::Item {
2199 text,
2200 ..popup::Item::default()
2201 })
2202 .collect();
2203 let build = |ui: &mut Ui, height: f32| {
2204 sized_frame(ui, [400.0, height], |ui| {
2205 let anchor = below(ui);
2206 popup::menu(ui, menu_id(), anchor, &items, None);
2207 })
2208 };
2209 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2210 build(&mut ui, 800.0);
2211 ui.open_popup(menu_id());
2212 for _ in 0..3 {
2213 build(&mut ui, 800.0);
2214 }
2215 let bar = |ui: &Ui| ui.laid_out(menu_id().child("rows").child("bar"));
2216 assert!(bar(&ui).is_none(), "twenty rows fit a tall window whole");
2217 assert!(ui.laid_out(menu_id().child("rows").child(19u64)).is_some());
2218 for _ in 0..3 {
2219 build(&mut ui, 300.0);
2220 }
2221 assert!(bar(&ui).is_some(), "a short window scrolls them");
2222}
2223
2224#[test]
2225fn a_long_menu_scrolls_by_dragging_its_thumb() {
2226 let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect();
2227 let items: Vec<_> = names
2228 .iter()
2229 .map(|text| popup::Item {
2230 text,
2231 ..popup::Item::default()
2232 })
2233 .collect();
2234 let build = |ui: &mut Ui| {
2235 frame(ui, |ui| {
2236 let anchor = below(ui);
2237 popup::menu(ui, menu_id(), anchor, &items, None);
2238 })
2239 };
2240 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2241 build(&mut ui);
2242 ui.open_popup(menu_id());
2243 build(&mut ui);
2244 build(&mut ui);
2245 let thumb = ui.laid_out(menu_id().child("rows").child("bar")).unwrap();
2246 let top = ui
2247 .laid_out(menu_id().child("rows").child(0u64))
2248 .map(|rect| rect[1]);
2249 let at = Instant::now();
2250 let x = (thumb[0] + thumb[2]) / 2.0;
2251 ui.event(Event::PointerMoved([x, thumb[1] + 2.0]));
2252 press(&mut ui, at, true);
2253 build(&mut ui);
2254 ui.event(Event::PointerMoved([x, thumb[1] + 60.0]));
2255 build(&mut ui);
2256 build(&mut ui);
2257 assert!(ui.popup_open(menu_id()));
2258 assert_ne!(
2259 ui.laid_out(menu_id().child("rows").child(0u64))
2260 .map(|rect| rect[1]),
2261 top
2262 );
2263}
2264
2265#[test]
2266fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end() {
2267 let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect();
2268 let items: Vec<_> = names
2269 .iter()
2270 .map(|text| popup::Item {
2271 text,
2272 ..popup::Item::default()
2273 })
2274 .collect();
2275 let build = |ui: &mut Ui| {
2276 frame(ui, |ui| {
2277 let anchor = below(ui);
2278 popup::menu(ui, menu_id(), anchor, &items, None);
2279 })
2280 };
2281 let rows = menu_id().child("rows");
2282 let fade = |ui: &Ui| ui.nodes.iter().find(|node| node.id == rows).unwrap().fade;
2283 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2284 build(&mut ui);
2285 ui.open_popup(menu_id());
2286 build(&mut ui);
2287 build(&mut ui);
2288 assert_eq!(fade(&ui), [0.0, 12.0], "cut only at the bottom");
2289 let thumb = ui.laid_out(rows.child("bar")).unwrap();
2290 let x = (thumb[0] + thumb[2]) / 2.0;
2291 ui.event(Event::PointerMoved([x, thumb[1] + 2.0]));
2292 press(&mut ui, Instant::now(), true);
2293 build(&mut ui);
2294 ui.event(Event::PointerMoved([x, 1000.0]));
2295 build(&mut ui);
2296 build(&mut ui);
2297 assert_eq!(fade(&ui), [12.0, 0.0], "cut only at the top");
2298 let list = ui.laid_out(rows).unwrap();
2299 let thumb = ui.laid_out(rows.child("bar")).unwrap();
2300 assert_eq!(
2301 thumb[3],
2302 list[3] - 4.0,
2303 "the thumb ends as far from the bottom as it starts"
2304 );
2305}
2306
2307#[test]
2308fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() {
2309 let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect();
2310 let rows = menu_id().child("rows");
2311 for picker in [false, true] {
2312 let items: Vec<_> = names
2313 .iter()
2314 .enumerate()
2315 .map(|(index, text)| popup::Item {
2316 text,
2317 checked: Some(index == 30),
2318 current: picker && index == 30,
2319 ..popup::Item::default()
2320 })
2321 .collect();
2322 let build = |ui: &mut Ui| {
2323 frame(ui, |ui| {
2324 let anchor = below(ui);
2325 popup::menu(ui, menu_id(), anchor, &items, None);
2326 })
2327 };
2328 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2329 build(&mut ui);
2330 ui.open_popup(menu_id());
2331 for _ in 0..3 {
2332 build(&mut ui);
2333 }
2334 assert_eq!(ui.popups[0].highlight, picker.then_some(30));
2335 let top = ui.laid_out(rows).unwrap()[1];
2336 let first = ui.laid_out(rows.child(0u64)).map(|rect| rect[1]);
2337 assert_eq!(first == Some(top), !picker, "{first:?} under {top}");
2338 }
2339}
2340
2341const FONTS: [&str; 46] = [
2342 "American Typewriter",
2343 "Andale Mono",
2344 "Apple Braille",
2345 "Apple Chancery",
2346 "Arial",
2347 "Arial Black",
2348 "Arial Hebrew",
2349 "Arial Narrow",
2350 "Arial Rounded MT Bold",
2351 "Arial Unicode MS",
2352 "Avenir",
2353 "Baskerville",
2354 "Big Caslon",
2355 "Brush Script MT",
2356 "Chalkboard",
2357 "Charter",
2358 "Cochin",
2359 "Comic Sans MS",
2360 "Courier",
2361 "Courier New",
2362 "Didot",
2363 "Futura",
2364 "Geneva",
2365 "Georgia",
2366 "Gill Sans",
2367 "Helvetica",
2368 "Helvetica Neue",
2369 "Herculanum",
2370 "Hoefler Text",
2371 "Impact",
2372 "Lucida Grande",
2373 "Marker Felt",
2374 "Menlo",
2375 "Monaco",
2376 "Optima",
2377 "Palatino",
2378 "Papyrus",
2379 "Rockwell",
2380 "Savoye LET",
2381 "Skia",
2382 "Tahoma",
2383 "Times",
2384 "Times New Roman",
2385 "Trebuchet MS",
2386 "Verdana",
2387 "Zapfino",
2388];
2389
2390fn fonts() -> Vec<popup::Item<'static>> {
2391 FONTS
2392 .iter()
2393 .map(|text| popup::Item {
2394 text,
2395 ..popup::Item::default()
2396 })
2397 .collect()
2398}
2399
2400fn ranked(items: &[popup::Item], query: &str) -> Vec<&'static str> {
2401 let order = popup::Matches::new(items, query, 9.0).order;
2402 order.into_iter().map(|index| FONTS[index]).collect()
2403}
2404
2405#[test]
2406fn fuzzy_matches_rank_whole_words_first_and_ties_keep_their_order() {
2407 let fonts = fonts();
2408 let family = [
2409 "Arial",
2410 "Arial Black",
2411 "Arial Hebrew",
2412 "Arial Narrow",
2413 "Arial Rounded MT Bold",
2414 "Arial Unicode MS",
2415 ];
2416 assert_eq!(ranked(&fonts, "Arial"), family);
2417 assert_eq!(ranked(&fonts, "rial"), family, "case aside, mid-word");
2418 let ari = ranked(&fonts, "ari");
2419 assert_eq!(ari[..6], family, "letters together before letters apart");
2420 assert_eq!(
2421 ari[6..],
2422 ["American Typewriter", "Apple Braille", "Baskerville"]
2423 );
2424 assert_eq!(
2425 ranked(&fonts, "tnr"),
2426 ["Times New Roman"],
2427 "words' initials"
2428 );
2429 assert_eq!(
2430 ranked(&fonts, "ar bl")[0],
2431 "Arial Black",
2432 "words in any order"
2433 );
2434 assert_eq!(
2435 ranked(&fonts, "ms"),
2436 [
2437 "Arial Unicode MS",
2438 "Comic Sans MS",
2439 "Trebuchet MS",
2440 "Times",
2441 "Times New Roman"
2442 ]
2443 );
2444 assert!(ranked(&fonts, "Cafe").is_empty());
2445}
2446
2447#[test]
2448fn a_fallback_shows_only_when_a_query_leaves_nothing_else() {
2449 let mut items = fonts();
2450 let last = items.len() - 1;
2451 items[last].fallback = true;
2452 let order = |query| popup::Matches::new(&items, query, 9.0).order;
2453 assert!(!order("").contains(&last), "unfiltered");
2454 assert!(!order("Arial").contains(&last), "others match");
2455 assert_eq!(order("Cafe"), [last], "nothing else matches");
2456}
2457
2458#[test]
2459fn typing_more_never_reveals_what_a_shorter_query_filtered_out() {
2460 let fonts = fonts();
2461 for query in [
2462 "arial black",
2463 "times new roman",
2464 "helvetica neue",
2465 "comic sans ms",
2466 "tnr",
2467 ] {
2468 let mut shown = ranked(&fonts, "");
2469 for end in 1..=query.len() {
2470 let narrower = ranked(&fonts, &query[..end]);
2471 for font in &narrower {
2472 assert!(shown.contains(font), "{:?} revealed {font}", &query[..end]);
2473 }
2474 shown = narrower;
2475 }
2476 assert!(!shown.is_empty(), "{query}");
2477 }
2478}
2479
2480#[test]
2481fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {
2482 let fonts = fonts();
2483 let build = |ui: &mut Ui| {
2484 frame(ui, |ui| {
2485 let anchor = below(ui);
2486 popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"));
2487 })
2488 };
2489 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
2490 build(&mut ui);
2491 ui.open_popup(menu_id());
2492 for _ in 0..12 {
2493 build(&mut ui);
2494 }
2495 let results = menu_id().child("rows");
2496 let height = |ui: &Ui| {
2497 let rect = ui.laid_out(results).unwrap();
2498 rect[3] - rect[1]
2499 };
2500 assert!(
2501 !ui.wants_frame() && height(&ui) > 5.0 * popup::MENU_ROW,
2502 "open at its full height"
2503 );
2504 ui.event(key(NamedKey::End));
2505 for _ in 0..40 {
2506 build(&mut ui);
2507 }
2508 // Each query shows its results where they settle in the frame it is typed.
2509 for query in ["t", "nr"] {
2510 ui.event(typed(query));
2511 build(&mut ui);
2512 let shown = [ui.laid_out(results), ui.laid_out(results.child(42_u64))];
2513 build(&mut ui);
2514 assert!(!ui.wants_frame(), "nothing eases");
2515 assert_eq!(
2516 [ui.laid_out(results), ui.laid_out(results.child(42_u64))],
2517 shown
2518 );
2519 }
2520 let row = ui.laid_out(results.child(42_u64)).unwrap();
2521 assert_eq!(
2522 row[1],
2523 ui.laid_out(results).unwrap()[1],
2524 "Times New Roman at the top"
2525 );
2526 assert_eq!(height(&ui), popup::MENU_ROW);
2527}
2528
2529/// A column `width` wide holding one label: its box, and the label as laid out.
2530fn fitted(
2531 ui: &mut Ui,
2532 text: &str,
2533 width: f32,
2534 overflow: Overflow,
2535 center: bool,
2536) -> ([f32; 4], Rc<Label>) {
2537 let mut id = None;
2538 frame(ui, |ui| {
2539 ui.open(
2540 "column",
2541 Spec {
2542 axis: Axis::Y,
2543 size: [px(width), children()],
2544 ..Spec::default()
2545 },
2546 );
2547 id = Some(ui.open(
2548 "label",
2549 Spec {
2550 size: [fill(), fit()],
2551 text: Some(text),
2552 overflow,
2553 center,
2554 pad: [4.0, 2.0],
2555 ..Spec::default()
2556 },
2557 ));
2558 ui.close();
2559 ui.close();
2560 });
2561 let node = ui.nodes.iter().find(|node| Some(node.id) == id).unwrap();
2562 (node.rect, node.label.clone().unwrap())
2563}
2564
2565fn advances(label: &Label) -> Vec<f32> {
2566 label
2567 .layout
2568 .lines()
2569 .map(|line| line.metrics().advance)
2570 .collect()
2571}
2572
2573#[test]
2574fn wrapped_labels_break_to_their_width_and_the_box_grows_to_their_height() {
2575 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2576 let text = "Conflicting changes are highlighted in red. This page cannot be edited.";
2577 let (_, label) = fitted(&mut ui, text, 1000.0, Overflow::Wrap, false);
2578 assert_eq!(
2579 advances(&label).len(),
2580 1,
2581 "a label that fits stays one line"
2582 );
2583 let line = label.size[1];
2584 let mut previous = 2;
2585 for width in [320.0, 200.0, 120.0, 60.0] {
2586 let (rect, label) = fitted(&mut ui, text, width, Overflow::Wrap, false);
2587 let lines = advances(&label);
2588 assert!(lines.len() >= previous, "{width}: {lines:?}");
2589 assert!(
2590 lines.iter().all(|line| *line <= width - 8.0 + 0.01),
2591 "{width}: {lines:?}"
2592 );
2593 assert!((rect[3] - rect[1] - 4.0 - label.size[1]).abs() <= 0.5);
2594 assert!((label.size[1] / line - lines.len() as f32).abs() < 0.1);
2595 assert_eq!(rect[2] - rect[0], width);
2596 previous = lines.len();
2597 }
2598}
2599
2600#[test]
2601fn wrapping_breaks_cjk_emoji_and_words_longer_than_the_line() {
2602 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2603 for text in [
2604 "東京都の天気は晴れのち曇り、明日は雨が降るでしょう",
2605 "🎉🎈🎂🎁🎊🎉🎈🎂🎁🎊🎉🎈🎂🎁🎊",
2606 "Supercalifragilisticexpialidocious-antidisestablishmentarianism",
2607 ] {
2608 let lines = advances(&fitted(&mut ui, text, 90.0, Overflow::Wrap, false).1);
2609 assert!(lines.len() > 2, "{text}: {lines:?}");
2610 assert!(lines.iter().all(|line| *line <= 82.01), "{text}: {lines:?}");
2611 }
2612}
2613
2614#[test]
2615fn centred_wrapped_lines_centre_across_the_box() {
2616 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2617 let (_, label) = fitted(
2618 &mut ui,
2619 "A long line that wraps, then short",
2620 150.0,
2621 Overflow::Wrap,
2622 true,
2623 );
2624 let last = label.layout.lines().last().unwrap();
2625 let Some(parley::PositionedLayoutItem::GlyphRun(run)) = last.items().next() else {
2626 panic!("no glyphs");
2627 };
2628 let room = 150.0 - 8.0 - last.metrics().advance;
2629 assert!(
2630 room > 20.0 && (run.offset() - room / 2.0).abs() < 1.0,
2631 "{}",
2632 run.offset()
2633 );
2634}
2635
2636#[test]
2637fn ellipsis_shortens_a_label_to_its_box_and_leaves_one_that_fits() {
2638 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2639 let text = "Meeting notes from the quarterly planning review";
2640 let (whole, label) = fitted(&mut ui, text, 1000.0, Overflow::Ellipsis, false);
2641 assert_eq!(label.key.0, text);
2642 for width in [200.0, 90.0, 30.0] {
2643 let (rect, label) = fitted(&mut ui, text, width, Overflow::Ellipsis, false);
2644 let (shown, lines) = (&label.key.0, advances(&label));
2645 assert!(shown.ends_with('…') && !shown.ends_with(" …"), "{shown}");
2646 assert!(text.starts_with(shown.trim_end_matches('…')));
2647 assert_eq!(lines.len(), 1);
2648 assert!(lines[0] <= width - 8.0 + 0.01, "{width}: {shown} {lines:?}");
2649 assert_eq!(rect[3] - rect[1], whole[3] - whole[1]);
2650 }
2651 let (_, label) = fitted(
2652 &mut ui,
2653 "東京都の天気は晴れのち曇り",
2654 80.0,
2655 Overflow::Ellipsis,
2656 false,
2657 );
2658 assert!(
2659 label.key.0.ends_with('…') && label.key.0.chars().count() > 1,
2660 "{}",
2661 label.key.0
2662 );
2663}
2664
2665#[test]
2666fn popups_ease_open_and_closed_then_stop_asking_for_frames() {
2667 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2668 let id = Id::ROOT.child("dialog");
2669 let build = |ui: &mut Ui| {
2670 if ui.popup_open(id) {
2671 ui.leaf(
2672 "dialog",
2673 Spec {
2674 size: [px(100.0), px(50.0)],
2675 fill: Some([1.0; 4]),
2676 anchor: Some(Anchor::Dialog),
2677 ..Spec::default()
2678 },
2679 );
2680 }
2681 };
2682 let motion = |ui: &Ui| {
2683 ui.layers().iter().find_map(|layer| match layer {
2684 Layer::Primitives(primitives) => primitives.motion,
2685 Layer::Custom { .. } => None,
2686 })
2687 };
2688 frame(&mut ui, |ui| {
2689 ui.open_popup(id);
2690 build(ui);
2691 });
2692 let first = motion(&ui).unwrap();
2693 assert!(first.opacity < 0.1 && first.zoom < 0.96 && first.tilt > 0.15);
2694 assert_eq!(
2695 first.pivot,
2696 [200.0, 300.0 / 8.0],
2697 "the dialog swings from its top centre, an eighth down"
2698 );
2699 for _ in 0..16 {
2700 frame(&mut ui, build);
2701 }
2702 assert_eq!(motion(&ui).map(|motion| motion.opacity), Some(1.0));
2703 assert!(!ui.wants_frame());
2704 ui.close_popup(id);
2705 frame(&mut ui, build);
2706 frame(&mut ui, build);
2707 let closing = motion(&ui).unwrap();
2708 assert!(closing.opacity < 1.0 && closing.opacity > 0.0);
2709 assert!(ui.wants_frame());
2710 for _ in 0..10 {
2711 frame(&mut ui, build);
2712 }
2713 assert_eq!(motion(&ui), None);
2714 assert!(!ui.wants_frame());
2715}
2716
2717#[test]
2718fn a_dragged_box_lands_past_the_middles_it_crossed_and_the_rest_slide_aside() {
2719 let spans = [[0.0, 40.0], [40.0, 60.0], [100.0, 20.0], [120.0, 50.0]];
2720 // Still over its own place, box 1 stays; past box 2's middle, it lands after it.
2721 assert_eq!(drop_slot(&spans, 1, 70.0), 1);
2722 assert_eq!(drop_slot(&spans, 1, 111.0), 2);
2723 assert_eq!(drop_slot(&spans, 1, 500.0), 3);
2724 assert_eq!(drop_slot(&spans, 3, -10.0), 0);
2725 assert_eq!(
2726 (0..4)
2727 .map(|index| slide(index, 1, 3, 60.0))
2728 .collect::<Vec<_>>(),
2729 [0.0, 0.0, -60.0, -60.0]
2730 );
2731 assert_eq!(
2732 (0..4)
2733 .map(|index| slide(index, 3, 1, 50.0))
2734 .collect::<Vec<_>>(),
2735 [0.0, 50.0, 50.0, 0.0]
2736 );
2737}
2738
2739/// Clicks the middle of `id`'s box as laid out now, where it must be hit.
2740fn click_box(ui: &mut Ui, id: Id) {
2741 let rect = ui.laid_out(id).unwrap();
2742 let middle = [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0];
2743 assert_eq!(
2744 ui.box_at(middle),
2745 Some(id),
2746 "the box is hit where it is laid out"
2747 );
2748 click(ui, middle);
2749}
2750
2751/// What a dialog's controls reported in a frame.
2752#[derive(Default)]
2753struct Reported {
2754 checked: bool,
2755 ok: bool,
2756 scheme: Option<usize>,
2757}
2758
2759/// A dialog holding a check box, a button, a field and a combo opening a menu of schemes.
2760fn dialog_frame(ui: &mut Ui, text: &mut String) -> Reported {
2761 let mut reported = Reported::default();
2762 frame(ui, |ui| {
2763 let dialog = Id::ROOT.child("dialog");
2764 if !ui.popup_open(dialog) {
2765 return;
2766 }
2767 ui.open_as(
2768 dialog,
2769 Spec {
2770 axis: Axis::Y,
2771 size: [px(240.0), children()],
2772 fill: Some([1.0; 4]),
2773 pad: [8.0, 8.0],
2774 anchor: Some(Anchor::Dialog),
2775 ..Spec::default()
2776 },
2777 );
2778 reported.checked = check_box(ui, "check", "Dark pages", false).clicked;
2779 reported.ok = button(ui, "ok", "OK").clicked;
2780 text_field(
2781 ui,
2782 Id::ROOT.child("name"),
2783 text,
2784 "",
2785 Spec {
2786 size: [fill(), px(26.0)],
2787 ..Spec::default()
2788 },
2789 );
2790 let combo = ui.id("combo");
2791 shell::combo(
2792 ui,
2793 "combo",
2794 "Color scheme",
2795 "Light",
2796 120.0,
2797 Id::ROOT.child("schemes"),
2798 true,
2799 );
2800 let items = ["System", "Light", "Dark"].map(|text| popup::Item {
2801 text,
2802 ..popup::Item::default()
2803 });
2804 let anchor = Anchor::Below(combo);
2805 reported.scheme = popup::menu(ui, Id::ROOT.child("schemes"), anchor, &items, None);
2806 ui.close();
2807 });
2808 reported
2809}
2810
2811#[test]
2812fn every_control_in_an_open_dialog_takes_clicks_where_it_rests() {
2813 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2814 let mut text = String::new();
2815 let dialog = Id::ROOT.child("dialog");
2816 dialog_frame(&mut ui, &mut text);
2817 ui.open_popup(dialog);
2818 let settle = |ui: &mut Ui, text: &mut String| {
2819 for _ in 0..20 {
2820 dialog_frame(ui, text);
2821 }
2822 assert!(!ui.wants_frame());
2823 };
2824 settle(&mut ui, &mut text);
2825 click_box(&mut ui, dialog.child("check"));
2826 assert!(dialog_frame(&mut ui, &mut text).checked);
2827 click_box(&mut ui, dialog.child("ok"));
2828 assert!(dialog_frame(&mut ui, &mut text).ok);
2829 click_box(&mut ui, Id::ROOT.child("name"));
2830 dialog_frame(&mut ui, &mut text);
2831 assert_eq!(ui.focused(), Some(Id::ROOT.child("name")));
2832 ui.event(typed("a"));
2833 dialog_frame(&mut ui, &mut text);
2834 assert_eq!(text, "a");
2835 click_box(&mut ui, dialog.child("combo"));
2836 dialog_frame(&mut ui, &mut text);
2837 let schemes = Id::ROOT.child("schemes");
2838 assert!(ui.popup_open(schemes) && ui.popup_open(dialog));
2839 settle(&mut ui, &mut text);
2840 click_box(&mut ui, schemes.child("rows").child(2_u64));
2841 assert_eq!(dialog_frame(&mut ui, &mut text).scheme, Some(2));
2842 assert!(!ui.popup_open(schemes) && ui.popup_open(dialog));
2843}
2844
2845#[test]
2846fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_clicks() {
2847 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2848 let fonts = fonts();
2849 // Inset, as a popup keeps off the window's edges.
2850 let combo = Id::ROOT.child("inset").child("combo");
2851 let mut chosen = None;
2852 let mut build = |ui: &mut Ui| {
2853 frame(ui, |ui| {
2854 ui.open(
2855 "inset",
2856 Spec {
2857 size: [children(), children()],
2858 pad: [10.0, 10.0],
2859 ..Spec::default()
2860 },
2861 );
2862 shell::combo(ui, "combo", "Font", "Calibri", 120.0, menu_id(), true);
2863 ui.close();
2864 chosen = popup::menu(ui, menu_id(), Anchor::Over(combo), &fonts, Some("Font"));
2865 });
2866 chosen
2867 };
2868 build(&mut ui);
2869 ui.open_popup(menu_id());
2870 build(&mut ui);
2871 let field = menu_id().child("filter");
2872 let [box_rect, first] = [ui.laid_out(combo).unwrap(), ui.laid_out(field).unwrap()];
2873 assert_eq!(first[..2], box_rect[..2], "the field starts on the box");
2874 assert!(first[2] - first[0] < box_rect[2] - box_rect[0] + 20.0);
2875 let shown = ui.laid_out(menu_id()).unwrap();
2876 let row = menu_id().child("rows").child(3_u64);
2877 let opening = ui.laid_out(row).unwrap();
2878 assert!(
2879 opening[2] > shown[2],
2880 "the rows lie out past the popup as it opens"
2881 );
2882 let layers = ui.layers();
2883 let moving: Vec<_> = layers
2884 .iter()
2885 .filter_map(|layer| match layer {
2886 Layer::Primitives(layer) => Some(layer),
2887 Layer::Custom { .. } => None,
2888 })
2889 .skip_while(|layer| layer.motion.is_none())
2890 .collect();
2891 let split = moving
2892 .iter()
2893 .position(|layer| layer.motion.is_none())
2894 .unwrap();
2895 assert!(
2896 moving[split..].iter().all(|layer| layer.motion.is_none()),
2897 "the field paints over the popup, which appears as one"
2898 );
2899 assert!(
2900 moving[..split]
2901 .iter()
2902 .filter(|layer| layer.round.is_some())
2903 .all(|layer| layer.round == Some((shown, ui.theme.menu().radius))),
2904 "the rows show only inside the popup's outline"
2905 );
2906 drop(layers);
2907 for _ in 0..20 {
2908 build(&mut ui);
2909 }
2910 assert!(!ui.wants_frame());
2911 let [panel, last] = [ui.laid_out(menu_id()).unwrap(), ui.laid_out(field).unwrap()];
2912 assert_eq!(last[2], panel[2] - 4.0, "the field widens across the popup");
2913 assert_eq!(ui.laid_out(row), Some(opening), "the rows never move");
2914 click_box(&mut ui, menu_id().child("rows").child(3_u64));
2915 assert_eq!(build(&mut ui), Some(3));
2916}
2917
2918#[test]
2919fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
2920 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2921 let fonts = fonts();
2922 let build = |ui: &mut Ui| {
2923 frame(ui, |ui| {
2924 let anchor = Anchor::Over(spot(ui, [330.0, 10.0, 390.0, 30.0]));
2925 _ = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"))
2926 })
2927 };
2928 build(&mut ui);
2929 ui.open_popup(menu_id());
2930 build(&mut ui);
2931 let field = menu_id().child("filter");
2932 let edges = |ui: &Ui| {
2933 [ui.laid_out(field).unwrap(), ui.laid_out(menu_id()).unwrap()].map(|rect| rect[2])
2934 };
2935 let first = ui.laid_out(field).unwrap();
2936 let row = menu_id().child("rows").child(3_u64);
2937 let opening = ui.laid_out(row).unwrap();
2938 assert_eq!(
2939 edges(&ui),
2940 [390.0, 394.0],
2941 "the trailing edges start on the box"
2942 );
2943 assert!(first[0] > 290.0, "the field starts near the box");
2944 for _ in 0..20 {
2945 build(&mut ui);
2946 }
2947 assert_eq!(edges(&ui), [390.0, 394.0], "the trailing edges hold");
2948 assert!(
2949 ui.laid_out(field).unwrap()[0] < first[0] - 30.0,
2950 "the field widens leftward"
2951 );
2952 assert_eq!(ui.laid_out(row), Some(opening), "the rows never move");
2953}
2954
2955fn built(ui: &Ui, id: Id) -> &Built {
2956 ui.nodes.iter().find(|node| node.id == id).unwrap()
2957}
2958
2959/// A split button at (100, 100) opening `split_menu()`; its signal.
2960fn split_frame(ui: &mut Ui) -> Signal {
2961 let mut signal = Signal::default();
2962 frame(ui, |ui| {
2963 ui.open(
2964 "bar",
2965 Spec {
2966 flags: Flags::FLOAT,
2967 size: [px(200.0), px(shell::TOOL)],
2968 position: [100.0, 100.0],
2969 ..Spec::default()
2970 },
2971 );
2972 signal = shell::split_button(ui, "split", "Split", CHECK_ICON, None, None, split_menu());
2973 let item = popup::Item {
2974 text: "Item",
2975 ..Default::default()
2976 };
2977 let anchor = below(ui);
2978 popup::menu(ui, split_menu(), anchor, &[item], None);
2979 ui.close();
2980 });
2981 signal
2982}
2983
2984const CHECK_ICON: &[&str] = popup::CHECK;
2985
2986fn split_menu() -> Id {
2987 Id::ROOT.child("split menu")
2988}
2989
2990fn split_part(part: &str) -> Id {
2991 Id::ROOT.child("bar").child("split").child(part)
2992}
2993
2994fn center(rect: [f32; 4]) -> [f32; 2] {
2995 [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0]
2996}
2997
2998#[test]
2999fn a_split_button_fills_its_button_alone_and_outlines_both_halves_from_its_arrow() {
3000 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3001 split_frame(&mut ui);
3002 let [button, arrow] = [split_part("button"), split_part("menu")];
3003 let face = button.child("face");
3004 let ring = split_part("ring");
3005 let settle = |ui: &mut Ui| {
3006 for _ in 0..60 {
3007 split_frame(ui);
3008 }
3009 };
3010 ui.event(Event::PointerMoved(center(ui.laid_out(button).unwrap())));
3011 settle(&mut ui);
3012 // The fill rounds past the button's clip, so it meets the arrow square.
3013 let [left, top, right, bottom] = ui.laid_out(button).unwrap();
3014 assert!(built(&ui, face).fill.is_some());
3015 assert_eq!(ui.laid_out(face), Some([left, top, right + 4.0, bottom]));
3016 assert!(built(&ui, button).flags.contains(Flags::CLIP));
3017 assert!(built(&ui, ring).border.is_none());
3018 ui.event(Event::PointerMoved(center(ui.laid_out(arrow).unwrap())));
3019 settle(&mut ui);
3020 assert!(built(&ui, face).fill.is_none());
3021 assert!(built(&ui, ring).border.is_some());
3022 assert_eq!(
3023 ui.laid_out(ring),
3024 Some([left, top, ui.laid_out(arrow).unwrap()[2], bottom])
3025 );
3026 // The arrow opens the menu, and the outline stays while it is open.
3027 let point = center(ui.laid_out(arrow).unwrap());
3028 click(&mut ui, point);
3029 assert!(!split_frame(&mut ui).clicked);
3030 assert!(ui.popup_open(split_menu()));
3031 ui.event(Event::PointerMoved([390.0, 290.0]));
3032 settle(&mut ui);
3033 assert!(built(&ui, ring).border.is_some());
3034 ui.close_popup(split_menu());
3035 settle(&mut ui);
3036 let point = center(ui.laid_out(button).unwrap());
3037 click(&mut ui, point);
3038 assert!(split_frame(&mut ui).clicked);
3039 assert!(!ui.popup_open(split_menu()));
3040}
3041
3042#[test]
3043fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {
3044 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3045 let items = [
3046 popup::Item {
3047 text: "Align left",
3048 icon: Some(CHECK_ICON),
3049 ..Default::default()
3050 },
3051 popup::Item {
3052 text: "Center",
3053 icon: Some(CHECK_ICON),
3054 current: true,
3055 ..Default::default()
3056 },
3057 ];
3058 let build = |ui: &mut Ui| {
3059 frame(ui, |ui| {
3060 ui.open(
3061 "bar",
3062 Spec {
3063 flags: Flags::FLOAT,
3064 size: [px(200.0), px(shell::TOOL)],
3065 position: [100.0, 100.0],
3066 ..Spec::default()
3067 },
3068 );
3069 let anchor = shell::menu_button(ui, "align", CHECK_ICON, None, menu_id());
3070 popup::menu(ui, menu_id(), anchor, &items, None);
3071 ui.close();
3072 });
3073 };
3074 build(&mut ui);
3075 let button = ui.laid_out(Id::ROOT.child("bar").child("align")).unwrap();
3076 // One target: the arrow opens the menu as the icon does.
3077 click(&mut ui, [button[2] - 3.0, center(button)[1]]);
3078 for _ in 0..40 {
3079 build(&mut ui);
3080 }
3081 assert!(ui.popup_open(menu_id()));
3082 let icon = ui
3083 .laid_out(
3084 menu_id()
3085 .child("rows")
3086 .child(0_usize)
3087 .child("item")
3088 .child("icon"),
3089 )
3090 .unwrap();
3091 assert_eq!(icon[0], button[0] + (shell::TOOL - ICON) / 2.0);
3092}
3093
3094#[test]
3095fn galleries_choose_across_their_groups_by_keys_and_clicks() {
3096 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3097 let gallery = Id::ROOT.child("gallery");
3098 let groups = [("Recent", 2), ("Library", 5)].map(|(heading, cells)| popup::Group {
3099 heading,
3100 ruled: false,
3101 cells,
3102 columns: 3,
3103 size: [30.0, 30.0],
3104 });
3105 let build = |ui: &mut Ui| {
3106 let mut chosen = None;
3107 frame(ui, |ui| {
3108 let anchor = below(ui);
3109 chosen = popup::gallery(ui, gallery, anchor, &groups, &[3], |ui, index| {
3110 ui.leaf(
3111 "label",
3112 Spec {
3113 size: [fill(), fill()],
3114 text: Some(["a", "b", "c", "d", "e", "f", "g"][index]),
3115 ..Spec::default()
3116 },
3117 );
3118 });
3119 });
3120 chosen
3121 };
3122 build(&mut ui);
3123 ui.open_popup(gallery);
3124 build(&mut ui);
3125 // Keys start from the current cell, the library's second.
3126 for named in [NamedKey::ArrowDown, NamedKey::ArrowLeft, NamedKey::Enter] {
3127 ui.event(key(named));
3128 }
3129 assert_eq!(build(&mut ui), Some(5));
3130 ui.open_popup(gallery);
3131 for _ in 0..40 {
3132 build(&mut ui);
3133 }
3134 let cell = |index: usize| ui.laid_out(gallery.child(("cell", index))).unwrap();
3135 // The library starts a row of its own under its heading.
3136 assert_eq!(cell(2)[0], cell(0)[0]);
3137 assert!(cell(2)[1] > cell(0)[3]);
3138 let point = center(cell(1));
3139 click(&mut ui, point);
3140 assert_eq!(build(&mut ui), Some(1));
3141}
3142
3143#[test]
3144fn desktop_menus_cut_or_fade_without_growing() {
3145 let motion = |ui: &Ui| {
3146 ui.layers().iter().find_map(|layer| match layer {
3147 Layer::Primitives(primitives) => primitives.motion,
3148 Layer::Custom { .. } => None,
3149 })
3150 };
3151 let at = |_: &mut Ui| Anchor::Point([50.0, 50.0]);
3152 let styled = |motion| {
3153 let mut theme = Theme::light();
3154 theme.desktop_menu = Some(Menu {
3155 motion,
3156 ..theme.menu()
3157 });
3158 let mut ui = Ui::new(theme, DOUBLE_CLICK);
3159 menu_frame(&mut ui, at, None);
3160 ui.open_popup(menu_id());
3161 menu_frame(&mut ui, at, None);
3162 ui
3163 };
3164
3165 let mut ui = styled(PopupMotion::Cut);
3166 let shown = motion(&ui).unwrap();
3167 assert_eq!([shown.opacity, shown.zoom, shown.tilt], [1.0, 1.0, 0.0]);
3168 ui.close_popup(menu_id());
3169 menu_frame(&mut ui, at, None);
3170 assert_eq!(motion(&ui), None, "gone the frame it closes");
3171 assert!(!ui.wants_frame());
3172
3173 let mut ui = styled(PopupMotion::Fade([0.15, 0.6]));
3174 let first = motion(&ui).unwrap();
3175 assert!(first.opacity < 0.2 && first.zoom == 1.0 && first.tilt == 0.0);
3176 for _ in 0..10 {
3177 menu_frame(&mut ui, at, None);
3178 }
3179 assert_eq!(motion(&ui).map(|motion| motion.opacity), Some(1.0));
3180 ui.close_popup(menu_id());
3181 // 96 ms into the fade out, OutQuart leaves about half.
3182 for _ in 0..7 {
3183 menu_frame(&mut ui, at, None);
3184 }
3185 let closing = motion(&ui).unwrap();
3186 assert!((closing.opacity - (1.0 - 0.096 / 0.6_f32).powi(4)).abs() < 0.01);
3187 assert!(ui.wants_frame());
3188}
3189
3190/// A row 200 wide of twelve section tabs with `active` open; returns the row's id.
3191fn tab_row(ui: &mut Ui, active: usize) -> Id {
3192 let names = [
3193 "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven",
3194 "Twelve",
3195 ];
3196 let tabs: Vec<_> = names
3197 .iter()
3198 .map(|name| (*name, [0.5, 0.6, 0.9, 1.0], false))
3199 .collect();
3200 let section = ui.theme.section([0.5, 0.6, 0.9, 1.0]);
3201 let row = Id::ROOT.child("tabs");
3202 frame(ui, |ui| {
3203 ui.open(
3204 "tab row",
3205 Spec {
3206 size: [px(200.0), px(28.0)],
3207 ..Spec::default()
3208 },
3209 );
3210 shell::section_tabs(
3211 ui, row, &tabs, active, None, None, None, &section, 28.0, [0.0; 4],
3212 );
3213 ui.close();
3214 });
3215 row
3216}
3217
3218/// Frames until the tabs' easing settles.
3219fn settle_tabs(ui: &mut Ui, active: usize) -> Id {
3220 let mut row = tab_row(ui, active);
3221 for _ in 0..60 {
3222 row = tab_row(ui, active);
3223 }
3224 row
3225}
3226
3227#[test]
3228fn overflowing_tabs_scroll_the_open_one_into_view() {
3229 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3230 let row = settle_tabs(&mut ui, 0);
3231 let view = ui.laid_out(row).unwrap();
3232 assert_eq!(view[2] - view[0], 200.0);
3233 let row = settle_tabs(&mut ui, 11);
3234 let last = ui.laid_out(shell::tab_id(row, 11)).unwrap();
3235 assert!(
3236 last[0] >= view[0] && last[2] <= view[2],
3237 "{last:?} in {view:?}"
3238 );
3239}
3240
3241#[test]
3242fn the_wheel_scrolls_overflowing_tabs_sideways_either_way_it_turns() {
3243 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3244 let row = settle_tabs(&mut ui, 0);
3245 let first = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3246 ui.event(Event::PointerMoved([100.0, 14.0]));
3247 ui.event(Event::Wheel([0.0, -40.0]));
3248 tab_row(&mut ui, 0);
3249 tab_row(&mut ui, 0);
3250 let scrolled = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3251 assert_eq!(first - scrolled, 40.0, "at once, as on the page");
3252 ui.event(Event::Wheel([30.0, 0.0]));
3253 tab_row(&mut ui, 0);
3254 tab_row(&mut ui, 0);
3255 let back = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3256 assert_eq!(back - scrolled, 30.0);
3257}
3258
3259#[test]
3260fn a_colour_picker_applies_its_colour_and_keeps_it_through_hsl() {
3261 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3262 let picker = Id::ROOT.child("picker");
3263 let build = |ui: &mut Ui| {
3264 let mut chosen = None;
3265 frame(ui, |ui| {
3266 let anchor = below(ui);
3267 chosen = popup::color_picker(
3268 ui,
3269 picker,
3270 anchor,
3271 "Custom Color",
3272 [0xd4, 0xf9, 0xf2],
3273 |_| [1.0; 4],
3274 );
3275 });
3276 chosen
3277 };
3278 build(&mut ui);
3279 ui.open_popup(picker);
3280 assert_eq!(build(&mut ui), None);
3281 ui.event(key(NamedKey::Enter));
3282 assert_eq!(build(&mut ui), Some([0xd4, 0xf9, 0xf2]));
3283 assert!(!ui.popup_open(picker));
3284}
3285
3286#[test]
3287fn password_fields_edit_the_characters_behind_their_bullets() {
3288 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3289 let mut text = String::new();
3290 let edit = |ui: &mut Ui, text: &mut String| {
3291 frame(ui, |ui| {
3292 password_field(
3293 ui,
3294 field(),
3295 text,
3296 "",
3297 Spec {
3298 size: [px(200.0), px(26.0)],
3299 pad: [6.0, 0.0],
3300 ..Spec::default()
3301 },
3302 );
3303 })
3304 };
3305 edit(&mut ui, &mut text);
3306 ui.set_focus(Some(field()));
3307 for typed_text in ["p", "ä", "ß"] {
3308 ui.event(typed(typed_text));
3309 }
3310 edit(&mut ui, &mut text);
3311 ui.event(key(NamedKey::ArrowLeft));
3312 ui.event(key(NamedKey::ArrowLeft));
3313 ui.event(typed("x"));
3314 ui.event(key(NamedKey::Delete));
3315 edit(&mut ui, &mut text);
3316 assert_eq!(text, "pxß");
3317 ui.event(key(NamedKey::Backspace));
3318 edit(&mut ui, &mut text);
3319 assert_eq!(text, "pß");
3320}
3321
3322#[test]
3323fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_list() {
3324 let dialog = Id::ROOT.child("dialog");
3325 let side = dialog.child("body").child("side");
3326 let list = dialog.child("body").child("list");
3327 let loose = Extent {
3328 size: Size::Children,
3329 strictness: 0.0,
3330 };
3331 let build = |ui: &mut Ui, rows: usize| {
3332 frame(ui, |ui| {
3333 ui.open_as(
3334 dialog,
3335 Spec {
3336 axis: Axis::Y,
3337 size: [px(300.0), loose],
3338 anchor: Some(Anchor::Dialog),
3339 ..Spec::default()
3340 },
3341 );
3342 ui.leaf(
3343 "header",
3344 Spec {
3345 size: [fill(), px(30.0)],
3346 ..Spec::default()
3347 },
3348 );
3349 ui.open(
3350 "body",
3351 Spec {
3352 size: [fill(), loose],
3353 ..Spec::default()
3354 },
3355 );
3356 ui.leaf(
3357 "side",
3358 Spec {
3359 size: [px(100.0), fill()],
3360 ..Spec::default()
3361 },
3362 );
3363 ui.open(
3364 "list",
3365 Spec {
3366 flags: Flags::SCROLL | Flags::CLIP,
3367 axis: Axis::Y,
3368 size: [fill(), loose],
3369 ..Spec::default()
3370 },
3371 );
3372 for row in 0..rows {
3373 ui.leaf(
3374 row,
3375 Spec {
3376 size: [fill(), px(30.0)],
3377 ..Spec::default()
3378 },
3379 );
3380 }
3381 ui.close();
3382 ui.close();
3383 ui.leaf(
3384 "footer",
3385 Spec {
3386 size: [fill(), px(30.0)],
3387 ..Spec::default()
3388 },
3389 );
3390 ui.close();
3391 });
3392 };
3393 let height = |ui: &Ui, id: Id| ui.laid_out(id).map(|rect| rect[3] - rect[1]).unwrap();
3394 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3395 build(&mut ui, 2);
3396 ui.open_popup(dialog);
3397 for _ in 0..40 {
3398 build(&mut ui, 2);
3399 }
3400 assert_eq!(height(&ui, dialog), 120.0, "as tall as what it holds");
3401 assert_eq!(height(&ui, side), 60.0, "the side as tall as the list");
3402
3403 for _ in 0..40 {
3404 build(&mut ui, 20);
3405 }
3406 assert_eq!(
3407 height(&ui, dialog),
3408 225.0,
3409 "no taller than the window lets a dialog be"
3410 );
3411 assert_eq!(height(&ui, list), 165.0, "the list gives way");
3412 assert_eq!(height(&ui, side), 165.0);
3413
3414 ui.scroll_to(list, list.child(10_usize));
3415 for _ in 0..60 {
3416 build(&mut ui, 20);
3417 }
3418 let [top, row] = [list, list.child(10_usize)].map(|id| ui.laid_out(id).unwrap()[1]);
3419 assert!(
3420 (row - top).abs() < 0.5,
3421 "row 10 at {row}, the list's top at {top}"
3422 );
3423}
3424
3425#[test]
3426fn a_box_squeezed_past_its_inset_paints_empty_not_inverted() {
3427 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3428 frame(&mut ui, |ui| {
3429 ui.leaf(
3430 "row",
3431 Spec {
3432 size: [px(100.0), px(4.0)],
3433 fill: Some([1.0; 4]),
3434 gradient: Some([0.5, 0.5, 0.5, 1.0]),
3435 inset: [0.0, 0.0, 0.0, 10.0],
3436 ..Spec::default()
3437 },
3438 );
3439 });
3440 let rect = ui.display.iter().find_map(|item| match item {
3441 Display::Rect { rect, .. } => Some(*rect),
3442 _ => None,
3443 });
3444 let [left, top, right, bottom] = rect.expect("the row paints");
3445 assert!(
3446 right >= left && bottom >= top,
3447 "{:?}",
3448 [left, top, right, bottom]
3449 );
3450}
3451
3452#[test]
3453fn a_box_has_no_rectangle_until_it_is_first_laid_out() {
3454 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
3455 let build = |ui: &mut Ui| {
3456 ui.leaf(
3457 "new",
3458 Spec {
3459 size: [px(40.0), px(20.0)],
3460 ..Spec::default()
3461 },
3462 );
3463 ui.laid_out(Id::ROOT.child("new"))
3464 };
3465 let mut seen = Vec::new();
3466 frame(&mut ui, |ui| seen.push(build(ui)));
3467 frame(&mut ui, |ui| seen.push(build(ui)));
3468 assert_eq!(
3469 seen[0], None,
3470 "a box placed against nothing would sit at the origin"
3471 );
3472 assert_eq!(seen[1], Some([0.0, 0.0, 40.0, 20.0]));
3473}