| 1 | use super::*; |
| 2 | use std::time::Duration; |
| 3 | use winit::keyboard::NamedKey; |
| 4 | |
| 5 | const DOUBLE_CLICK: Duration = Duration::from_millis(500); |
| 6 | /// The key that carries shortcuts: Command on macOS, Control elsewhere. |
| 7 | const SHORTCUT: ModifiersState = if cfg!(target_vendor = "apple") { |
| 8 | ModifiersState::SUPER |
| 9 | } else { |
| 10 | ModifiersState::CONTROL |
| 11 | }; |
| 12 | |
| 13 | thread_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. |
| 19 | fn 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. |
| 24 | fn 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] |
| 36 | fn 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] |
| 70 | fn 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] |
| 100 | fn 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. |
| 138 | fn 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] |
| 198 | fn 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] |
| 213 | fn 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] |
| 225 | fn 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] |
| 236 | fn 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] |
| 309 | fn 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] |
| 347 | fn 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 | |
| 360 | fn 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] |
| 377 | fn 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] |
| 421 | fn 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] |
| 453 | fn 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] |
| 487 | fn 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] |
| 506 | fn 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] |
| 586 | fn 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 | |
| 635 | fn 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 | |
| 653 | fn field() -> Id { |
| 654 | Id::ROOT.child("filter") |
| 655 | } |
| 656 | |
| 657 | fn key(named: NamedKey) -> Event { |
| 658 | Event::Key { |
| 659 | key: Key::Named(named), |
| 660 | text: None, |
| 661 | } |
| 662 | } |
| 663 | |
| 664 | fn 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`. |
| 672 | fn 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`. |
| 681 | fn 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 | |
| 691 | fn 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] |
| 700 | fn 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] |
| 726 | fn 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")] |
| 741 | fn 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")] |
| 796 | fn 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 | )] |
| 819 | fn 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")] |
| 857 | fn 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] |
| 876 | fn 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] |
| 902 | fn 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] |
| 931 | fn 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] |
| 988 | fn 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] |
| 1018 | fn 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 | |
| 1104 | fn 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 | |
| 1123 | fn 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. |
| 1129 | fn 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. |
| 1152 | fn 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. |
| 1169 | fn 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`. |
| 1174 | fn 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. |
| 1185 | fn 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 | |
| 1190 | fn 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. |
| 1199 | fn 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. |
| 1221 | fn 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] |
| 1230 | fn 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] |
| 1270 | fn 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] |
| 1312 | fn 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] |
| 1320 | fn 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] |
| 1349 | fn 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] |
| 1440 | fn 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] |
| 1460 | fn 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] |
| 1472 | fn 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] |
| 1487 | fn 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] |
| 1501 | fn 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] |
| 1511 | fn 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] |
| 1533 | fn 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] |
| 1548 | fn 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] |
| 1590 | fn 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] |
| 1627 | fn 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] |
| 1642 | fn 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] |
| 1674 | fn 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] |
| 1706 | fn 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] |
| 1743 | fn 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] |
| 1807 | fn 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] |
| 1844 | fn 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] |
| 1869 | fn 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] |
| 1898 | fn 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] |
| 1933 | fn 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] |
| 1963 | fn 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. |
| 2022 | struct Keyed { |
| 2023 | keys: Vec<u64>, |
| 2024 | at: HashMap<u64, usize>, |
| 2025 | lookups: std::cell::Cell<usize>, |
| 2026 | } |
| 2027 | |
| 2028 | impl 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 | |
| 2044 | impl 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 | |
| 2060 | const LIST_ROW: f32 = 26.0; |
| 2061 | const VIEW: f32 = 10.0 * LIST_ROW; |
| 2062 | |
| 2063 | fn list_id() -> Id { |
| 2064 | Id::ROOT.child("list") |
| 2065 | } |
| 2066 | |
| 2067 | /// Builds a frame of a list over `rows`, ten rows in view. |
| 2068 | fn 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 | |
| 2100 | fn 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. |
| 2105 | fn 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] |
| 2113 | fn 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] |
| 2129 | fn 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] |
| 2160 | fn 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] |
| 2194 | fn 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] |
| 2225 | fn 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] |
| 2266 | fn 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] |
| 2308 | fn 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 | |
| 2341 | const 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 | |
| 2390 | fn 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 | |
| 2400 | fn 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] |
| 2406 | fn 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] |
| 2448 | fn 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] |
| 2459 | fn 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] |
| 2481 | fn 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. |
| 2530 | fn 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 | |
| 2565 | fn advances(label: &Label) -> Vec<f32> { |
| 2566 | label |
| 2567 | .layout |
| 2568 | .lines() |
| 2569 | .map(|line| line.metrics().advance) |
| 2570 | .collect() |
| 2571 | } |
| 2572 | |
| 2573 | #[test] |
| 2574 | fn 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] |
| 2601 | fn 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] |
| 2615 | fn 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] |
| 2637 | fn 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] |
| 2666 | fn 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] |
| 2718 | fn 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. |
| 2740 | fn 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)] |
| 2753 | struct 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. |
| 2760 | fn 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] |
| 2812 | fn 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] |
| 2846 | fn 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] |
| 2919 | fn 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 | |
| 2955 | fn 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. |
| 2960 | fn 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 | |
| 2984 | const CHECK_ICON: &[&str] = popup::CHECK; |
| 2985 | |
| 2986 | fn split_menu() -> Id { |
| 2987 | Id::ROOT.child("split menu") |
| 2988 | } |
| 2989 | |
| 2990 | fn split_part(part: &str) -> Id { |
| 2991 | Id::ROOT.child("bar").child("split").child(part) |
| 2992 | } |
| 2993 | |
| 2994 | fn center(rect: [f32; 4]) -> [f32; 2] { |
| 2995 | [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0] |
| 2996 | } |
| 2997 | |
| 2998 | #[test] |
| 2999 | fn 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] |
| 3043 | fn 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] |
| 3095 | fn 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] |
| 3144 | fn 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. |
| 3191 | fn 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. |
| 3219 | fn 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] |
| 3228 | fn 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] |
| 3242 | fn 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] |
| 3260 | fn 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] |
| 3287 | fn 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] |
| 3323 | fn 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] |
| 3426 | fn 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] |
| 3453 | fn 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 | } |