1//! Renders each popup control open, in both themes, to `/tmp/ui-popups-{dark,light}.png`,
2//! and frames of lists changing to `/tmp/ui-list-{keystroke,delete}-{dark,light}.png`: a
3//! combo's list after `cou` is typed into its filter, and a page list losing rows. Then times
4//! a list of 200 000 rows.
5
6use std::{
7 collections::HashMap,
8 time::{Duration, Instant},
9};
10use ui::{Anchor, Event, Id, List, Rows, Spec, Theme, Ui, fill, popup::Item, px, shell};
11use winit::keyboard::{Key, NamedKey};
12
13const PANEL: [f32; 2] = [420.0, 340.0];
14const SCALE: f32 = 2.0;
15
16macro_rules! art {
17 ($path:literal) => {
18 &[include_str!(concat!(
19 "../../snowbound/assets/",
20 $path,
21 ".svg"
22 ))]
23 };
24}
25
26fn main() {
27 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
28 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
29 let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
30 let mut renderer = draw::Renderer::new(
31 device.clone(),
32 queue.clone(),
33 wgpu::TextureFormat::Rgba8UnormSrgb,
34 );
35 for (name, theme) in [("dark", Theme::dark()), ("light", Theme::light())] {
36 let scenes: [fn(&mut Scene); 6] = [menu, fonts, colors, palette, tags, highlight];
37 let uis: Vec<Ui> = scenes
38 .iter()
39 .map(|build| {
40 let mut scene = Scene {
41 ui: Ui::new(theme.clone(), Duration::from_millis(500)),
42 start: Instant::now(),
43 frames: 0,
44 };
45 build(&mut scene);
46 scene.ui
47 })
48 .collect();
49 let panels = uis
50 .iter()
51 .map(|ui| paint(&device, &queue, &mut renderer, ui))
52 .collect();
53 let path = format!("/tmp/ui-popups-{name}.png");
54 render(panels, 3, &path);
55 println!("{path}");
56 let changes = [
57 (
58 "keystroke",
59 keystroke(&theme, &device, &queue, &mut renderer),
60 ),
61 ("delete", delete(&theme, &device, &queue, &mut renderer)),
62 ];
63 for (change, panels) in changes {
64 let path = format!("/tmp/ui-list-{change}-{name}.png");
65 render(panels, 5, &path);
66 println!("{path}");
67 }
68 let items = font_items();
69 let fonts = Id::ROOT.child("fonts");
70 let context = Id::ROOT.child("context");
71 let options = Id::ROOT.child("options");
72 let search = Id::ROOT.child("search");
73 let strips: [(&str, Id, &Build<'_>); 4] = [
74 ("menu", context, &|ui, _| {
75 ui::popup::menu(ui, context, Anchor::Point([140.0, 70.0]), &items[..8], None);
76 }),
77 ("combo", fonts, &|ui, [combo, _]| {
78 ui::popup::menu(ui, fonts, Anchor::Over(combo), &items, Some("Calibri"));
79 }),
80 ("search", search, &|ui, [combo, _]| {
81 results(ui, search, combo)
82 }),
83 ("dialog", options, &|ui, _| dialog(ui, options)),
84 ];
85 for (motion, id, build) in strips {
86 let panels = opening_and_closing(&theme, &device, &queue, &mut renderer, id, build);
87 let path = format!("/tmp/ui-motion-{motion}-{name}.png");
88 render(panels, 6, &path);
89 println!("{path}");
90 }
91 }
92 measure();
93}
94
95/// Builds a frame's popup, given the ids of the combo and the split button's arrow.
96type Build<'a> = dyn Fn(&mut Ui, [Id; 2]) + 'a;
97
98/// Popup `id` as `build` builds it, every 16 ms from the frame it opens until it has
99/// closed again; no frame is asked for once each settles.
100fn opening_and_closing(
101 theme: &Theme,
102 device: &wgpu::Device,
103 queue: &wgpu::Queue,
104 renderer: &mut draw::Renderer,
105 id: Id,
106 build: &Build<'_>,
107) -> Vec<Vec<u8>> {
108 let mut scene = Scene {
109 ui: Ui::new(theme.clone(), Duration::from_millis(500)),
110 start: Instant::now(),
111 frames: 0,
112 };
113 scene.frame(|_, _| {});
114 scene.frame(|ui, ids| {
115 ui.open_popup(id);
116 build(ui, ids);
117 });
118 let mut panels = vec![paint(device, queue, renderer, &scene.ui)];
119 for _ in 0..17 {
120 scene.frame(build);
121 panels.push(paint(device, queue, renderer, &scene.ui));
122 }
123 assert!(!scene.ui.wants_frame(), "settled open");
124 scene.ui.close_popup(id);
125 for _ in 0..12 {
126 scene.frame(build);
127 panels.push(paint(device, queue, renderer, &scene.ui));
128 }
129 assert!(!scene.ui.wants_frame(), "settled closed");
130 panels
131}
132
133/// A search's dropdown over the box at `combo`, as the app's search box opens: its field
134/// holds the box's place and widens while the panel comes in around it.
135fn results(ui: &mut Ui, id: Id, combo: Id) {
136 if !ui.popup_open(id) {
137 return;
138 }
139 let theme = ui.theme.clone();
140 ui.open_as(
141 id,
142 Spec {
143 axis: ui::Axis::Y,
144 size: [px(300.0), ui::children()],
145 fill: Some(theme.popup),
146 border: Some(theme.chip),
147 shadow: Some(theme.shadow),
148 radius: 6.0,
149 pad: [4.0, 4.0],
150 gap: 4.0,
151 anchor: Some(Anchor::Over(combo)),
152 ..Spec::default()
153 },
154 );
155 // Stands in for the combo, as tall, widening with the panel.
156 ui.leaf(
157 "box",
158 Spec {
159 flags: ui::Flags::STILL,
160 size: [fill(), px(0.0)],
161 text: Some("Search All Notebooks (⌘E)"),
162 color: Some(theme.text_dim),
163 fill: Some(theme.base),
164 border: Some(theme.accent),
165 radius: 4.0,
166 pad: [6.0, 0.0],
167 ..Spec::default()
168 },
169 );
170 for (index, page) in ["Meeting notes", "Quarterly planning", "Reading list"]
171 .iter()
172 .enumerate()
173 {
174 ui.leaf(
175 index,
176 Spec {
177 size: [fill(), px(22.0)],
178 text: Some(page),
179 pad: [8.0, 0.0],
180 ..Spec::default()
181 },
182 );
183 }
184 ui.close();
185}
186
187/// A small dialog, as Options opens over the window.
188fn dialog(ui: &mut Ui, id: Id) {
189 if !ui.popup_open(id) {
190 return;
191 }
192 let theme = ui.theme.clone();
193 ui.open_as(
194 id,
195 Spec {
196 axis: ui::Axis::Y,
197 size: [px(260.0), ui::children()],
198 fill: Some(theme.popup),
199 border: Some(theme.chip),
200 shadow: Some(theme.shadow),
201 radius: 8.0,
202 pad: [14.0, 12.0],
203 gap: 8.0,
204 anchor: Some(Anchor::Dialog),
205 ..Spec::default()
206 },
207 );
208 ui.leaf(
209 "title",
210 Spec {
211 size: [fill(), ui::fit()],
212 text: Some("Options"),
213 bold: true,
214 ..Spec::default()
215 },
216 );
217 ui.leaf(
218 "body",
219 Spec {
220 size: [fill(), ui::fit()],
221 text: Some("These choices apply to every notebook, and are kept when OK is chosen."),
222 overflow: ui::Overflow::Wrap,
223 color: Some(theme.text_dim),
224 ..Spec::default()
225 },
226 );
227 ui::button(ui, "ok", "OK");
228 ui.close();
229}
230
231/// Frames a change shows at: before it, then each of the 150 ms it takes.
232const FRAMES: [u32; 9] = [1, 2, 3, 4, 5, 6, 7, 8, 10];
233
234/// A font combo's list before `cou` is typed into its filter and at frames after.
235fn keystroke(
236 theme: &Theme,
237 device: &wgpu::Device,
238 queue: &wgpu::Queue,
239 renderer: &mut draw::Renderer,
240) -> Vec<Vec<u8>> {
241 let mut scene = Scene {
242 ui: Ui::new(theme.clone(), Duration::from_millis(500)),
243 start: Instant::now(),
244 frames: 0,
245 };
246 let items = font_items();
247 let id = Id::ROOT.child("fonts");
248 let build = |ui: &mut Ui, [combo, _]: [Id; 2]| {
249 ui::popup::menu(ui, id, Anchor::Over(combo), &items, Some("Calibri"));
250 };
251 scene.open(id, Vec::new(), build);
252 let mut panels = vec![paint(device, queue, renderer, &scene.ui)];
253 for event in typed("cou") {
254 scene.ui.event(event);
255 }
256 for frame in 1..=10 {
257 scene.frame(build);
258 if FRAMES.contains(&frame) {
259 panels.push(paint(device, queue, renderer, &scene.ui));
260 }
261 }
262 panels
263}
264
265/// A page list before rows 3 to 5 and 9 are deleted and at frames after.
266fn delete(
267 theme: &Theme,
268 device: &wgpu::Device,
269 queue: &wgpu::Queue,
270 renderer: &mut draw::Renderer,
271) -> Vec<Vec<u8>> {
272 let mut scene = Scene {
273 ui: Ui::new(theme.clone(), Duration::from_millis(500)),
274 start: Instant::now(),
275 frames: 0,
276 };
277 let names: Vec<String> = (0..30).map(|key| format!("Page {key}")).collect();
278 let mut selected = Some(7);
279 let mut build = |scene: &mut Scene, rows: &Keyed| {
280 scene.frame(|ui, _| {
281 let list = List {
282 rows,
283 row: 26.0,
284 keys: &[],
285 hover_selects: false,
286 };
287 let spec = Spec {
288 size: [px(240.0), px(26.0 * 11.0)],
289 fill: Some(ui.theme.panel),
290 ..Spec::default()
291 };
292 let theme = ui.theme.clone();
293 ui::list(
294 ui,
295 Id::ROOT.child("pages"),
296 spec,
297 list,
298 &mut selected,
299 |ui, row| {
300 ui.leaf(
301 "name",
302 Spec {
303 size: [fill(), fill()],
304 text: Some(&names[row.key as usize]),
305 fill: row.selected.then(|| theme.hover()),
306 radius: 4.0,
307 pad: [8.0, 0.0],
308 ..Spec::default()
309 },
310 );
311 },
312 );
313 });
314 };
315 let all = Keyed::new((0..30).collect());
316 for _ in 0..20 {
317 build(&mut scene, &all);
318 }
319 let mut panels = vec![paint(device, queue, renderer, &scene.ui)];
320 let kept = Keyed::new(
321 (0..30)
322 .filter(|key| !(3..=5).contains(key) && *key != 9)
323 .collect(),
324 );
325 for frame in 1..=10 {
326 build(&mut scene, &kept);
327 if FRAMES.contains(&frame) {
328 panels.push(paint(device, queue, renderer, &scene.ui));
329 }
330 }
331 panels
332}
333
334/// Keys in order, and where each is.
335struct Keyed {
336 keys: Vec<u64>,
337 at: HashMap<u64, usize>,
338}
339
340impl Keyed {
341 fn new(keys: Vec<u64>) -> Self {
342 let at = keys
343 .iter()
344 .enumerate()
345 .map(|(index, key)| (*key, index))
346 .collect();
347 Self { keys, at }
348 }
349}
350
351impl Rows for Keyed {
352 fn count(&self) -> usize {
353 self.keys.len()
354 }
355
356 fn key(&self, index: usize) -> u64 {
357 self.keys[index]
358 }
359
360 fn find(&self, key: u64) -> Option<usize> {
361 self.at.get(&key).copied()
362 }
363}
364
365/// Prints how long a frame of a list takes to build and lay out: with 20 rows, with
366/// 200 000, after a keystroke filters those, and while 10 000 arrive each frame.
367fn measure() {
368 let names: Vec<String> = (0..200_000u64)
369 .map(|key| format!("Page {:06}", key.wrapping_mul(2_654_435_761) % 1_000_000))
370 .collect();
371 let mut ui = Ui::new(Theme::dark(), Duration::from_millis(500));
372 let start = Instant::now();
373 let mut frames = 0;
374 let mut selected = Some(100_000);
375 let mut frame = |ui: &mut Ui, rows: &Keyed, selected: &mut Option<u64>| {
376 frames += 1;
377 let began = Instant::now();
378 ui.begin(
379 [400.0, 600.0],
380 2.0,
381 start + Duration::from_millis(16) * frames,
382 );
383 let list = List {
384 rows,
385 row: 24.0,
386 keys: &[],
387 hover_selects: false,
388 };
389 let spec = Spec {
390 size: [fill(), fill()],
391 ..Spec::default()
392 };
393 ui::list(
394 ui,
395 Id::ROOT.child("pages"),
396 spec,
397 list,
398 selected,
399 |ui, row| {
400 let text = &names[row.key as usize];
401 ui.leaf(
402 "name",
403 Spec {
404 size: [fill(), fill()],
405 text: Some(text),
406 fill: row.selected.then_some([0.2, 0.3, 0.4, 1.0]),
407 pad: [8.0, 0.0],
408 ..Spec::default()
409 },
410 );
411 },
412 );
413 ui.end();
414 drop(ui.layers());
415 began.elapsed()
416 };
417 let average = |times: Vec<Duration>| times.iter().sum::<Duration>() / times.len() as u32;
418 for count in [20, 200_000] {
419 let rows = Keyed::new((0..count).collect());
420 let times: Vec<_> = (0..60)
421 .map(|_| frame(&mut ui, &rows, &mut selected))
422 .collect();
423 println!("{count} rows: {:?} a frame", average(times[30..].to_vec()));
424 }
425 let filtering = Instant::now();
426 let rows = Keyed::new(
427 (0..200_000)
428 .filter(|key| names[*key as usize].contains('7'))
429 .collect(),
430 );
431 let filtering = filtering.elapsed();
432 let first = frame(&mut ui, &rows, &mut selected);
433 let times: Vec<_> = (0..12)
434 .map(|_| frame(&mut ui, &rows, &mut selected))
435 .collect();
436 println!(
437 "a keystroke filtering 200 000 to {}: {filtering:?} to filter, then {first:?} and {:?} a frame after",
438 rows.count(),
439 average(times)
440 );
441 let mut keys: Vec<u64> = (0..100_000).collect();
442 let (mut list, mut rebuild) = (Vec::new(), Vec::new());
443 for wave in 0..10u64 {
444 let rebuilding = Instant::now();
445 // Arrivals interleave with the rows already listed, above and below the view.
446 let arriving = (0..10_000).map(|index| 100_000 + wave * 10_000 + index);
447 keys = keys
448 .chunks(10)
449 .zip(arriving)
450 .flat_map(|(chunk, key)| chunk.iter().copied().chain([key]))
451 .collect();
452 let rows = Keyed::new(keys.clone());
453 rebuild.push(rebuilding.elapsed());
454 list.push(frame(&mut ui, &rows, &mut selected));
455 }
456 println!(
457 "10 000 arriving a frame, up to 200 000: {:?} a frame for the list, {:?} to rebuild the rows",
458 average(list),
459 average(rebuild)
460 );
461}
462
463struct Scene {
464 ui: Ui,
465 start: Instant,
466 frames: u32,
467}
468
469impl Scene {
470 /// Builds a frame over the panel's backdrop and toolbar, handing `build` the ids of the
471 /// combo and the split button's arrow.
472 fn frame(&mut self, build: impl FnOnce(&mut Ui, [Id; 2])) {
473 self.frames += 1;
474 let now = self.start + Duration::from_millis(16) * self.frames;
475 let ui = &mut self.ui;
476 let theme = ui.theme.clone();
477 ui.begin(PANEL, SCALE, now);
478 ui.open(
479 "backdrop",
480 Spec {
481 size: [fill(), fill()],
482 fill: Some(theme.base),
483 ..Spec::default()
484 },
485 );
486 ui.open(
487 "toolbar",
488 Spec {
489 size: [fill(), px(30.0)],
490 fill: Some(theme.strip),
491 pad: [8.0, 4.0],
492 gap: 6.0,
493 ..Spec::default()
494 },
495 );
496 let combo = ui.id("font");
497 shell::combo(
498 ui,
499 "font",
500 "Font",
501 "Calibri",
502 120.0,
503 Id::ROOT.child("fonts"),
504 true,
505 );
506 let split = ui.id("color");
507 shell::split_button(
508 ui,
509 "color",
510 "Font Color",
511 art!("icons/font-color"),
512 Some(draw::srgb(0xe8, 0x3a, 0x30)),
513 None,
514 Id::ROOT.child("font color"),
515 );
516 ui.close();
517 ui.close();
518 build(ui, [combo, split.child("menu")]);
519 ui.end();
520 }
521
522 /// Opens `id` and runs frames until it has faded in, feeding `events` once it shows.
523 fn open(&mut self, id: Id, events: Vec<Event>, build: impl Fn(&mut Ui, [Id; 2])) {
524 self.frame(|_, _| {});
525 self.frame(|ui, ids| {
526 ui.open_popup(id);
527 build(ui, ids);
528 });
529 for event in events {
530 self.ui.event(event);
531 }
532 for _ in 0..30 {
533 self.frame(&build);
534 }
535 }
536}
537
538fn key(named: NamedKey) -> Event {
539 Event::Key {
540 key: Key::Named(named),
541 text: None,
542 }
543}
544
545fn typed(text: &str) -> Vec<Event> {
546 text.chars()
547 .map(|character| Event::Key {
548 key: Key::Character(character.to_string().into()),
549 text: Some(character.to_string()),
550 })
551 .collect()
552}
553
554fn menu(scene: &mut Scene) {
555 let items = [
556 Item {
557 text: "Cut",
558 shortcut: "⌘X",
559 ..Item::default()
560 },
561 Item {
562 text: "Copy",
563 shortcut: "⌘C",
564 ..Item::default()
565 },
566 Item {
567 text: "Paste",
568 shortcut: "⌘V",
569 ..Item::default()
570 },
571 Item {
572 text: "Paste as plain text",
573 shortcut: "⌥⇧⌘V",
574 disabled: true,
575 ..Item::default()
576 },
577 Item {
578 text: "Bold",
579 icon: Some(art!("icons/bold")),
580 shortcut: "⌘B",
581 checked: Some(true),
582 separated: true,
583 ..Item::default()
584 },
585 Item {
586 text: "Italic",
587 icon: Some(art!("icons/italic")),
588 shortcut: "⌘I",
589 ..Item::default()
590 },
591 Item {
592 text: "Link…",
593 icon: Some(art!("icons/link")),
594 shortcut: "⌘K",
595 ..Item::default()
596 },
597 Item {
598 text: "Select all",
599 shortcut: "⌘A",
600 separated: true,
601 ..Item::default()
602 },
603 ];
604 let id = Id::ROOT.child("context");
605 let events = vec![key(NamedKey::ArrowDown), key(NamedKey::ArrowDown)];
606 scene.open(id, events, |ui, _| {
607 ui::popup::menu(ui, id, Anchor::Point([140.0, 70.0]), &items, None);
608 });
609}
610
611const FONTS: &[&str] = &[
612 "Aptos",
613 "Arial",
614 "Arial Black",
615 "Bahnschrift",
616 "Calibri",
617 "Cambria",
618 "Candara",
619 "Comic Sans MS",
620 "Consolas",
621 "Constantia",
622 "Corbel",
623 "Courier New",
624 "Georgia",
625 "Segoe UI",
626 "Tahoma",
627 "Times New Roman",
628 "Trebuchet MS",
629 "Verdana",
630];
631
632fn font_items() -> Vec<Item<'static>> {
633 FONTS
634 .iter()
635 .map(|text| Item {
636 text,
637 checked: Some(*text == "Calibri"),
638 ..Item::default()
639 })
640 .collect()
641}
642
643fn fonts(scene: &mut Scene) {
644 let items = font_items();
645 let id = Id::ROOT.child("fonts");
646 scene.open(id, typed("ca"), |ui, [combo, _]| {
647 ui::popup::menu(ui, id, Anchor::Over(combo), &items, Some("Calibri"));
648 });
649}
650
651fn office_colors() -> Vec<([f32; 4], &'static str)> {
652 let theme = [
653 (0xff, 0xff, 0xff),
654 (0x00, 0x00, 0x00),
655 (0xee, 0xec, 0xe1),
656 (0x1f, 0x49, 0x7d),
657 (0x4f, 0x81, 0xbd),
658 (0xc0, 0x50, 0x4d),
659 (0x9b, 0xbb, 0x59),
660 (0x80, 0x64, 0xa2),
661 (0x4b, 0xac, 0xc6),
662 (0xf7, 0x96, 0x46),
663 ]
664 .map(|(r, g, b)| draw::srgb(r, g, b));
665 let standard = [
666 (0xc0, 0x00, 0x00),
667 (0xff, 0x00, 0x00),
668 (0xff, 0xc0, 0x00),
669 (0xff, 0xff, 0x00),
670 (0x92, 0xd0, 0x50),
671 (0x00, 0xb0, 0x50),
672 (0x00, 0xb0, 0xf0),
673 (0x00, 0x70, 0xc0),
674 (0x00, 0x20, 0x60),
675 (0x70, 0x30, 0xa0),
676 ]
677 .map(|(r, g, b)| draw::srgb(r, g, b));
678 let mut colors = theme.to_vec();
679 for step in [0.15, 0.35, 0.6, -0.3, -0.6] {
680 colors.extend(theme.iter().map(|color| {
681 let toward = if step > 0.0 {
682 [1.0; 4]
683 } else {
684 [0.0, 0.0, 0.0, 1.0]
685 };
686 ui::mix(*color, toward, f32::abs(step))
687 }));
688 }
689 colors.extend(standard);
690 colors.into_iter().map(|color| (color, "")).collect()
691}
692
693fn colors(scene: &mut Scene) {
694 let swatches = office_colors();
695 let id = Id::ROOT.child("font color");
696 scene.open(id, Vec::new(), |ui, [_, split]| {
697 let anchor = Anchor::Below(split);
698 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);
699 });
700 let cell = scene.ui.laid_out(id.child(("cell", 15_usize))).unwrap();
701 scene
702 .ui
703 .event(Event::PointerMoved([cell[0] + 5.0, cell[1] + 5.0]));
704 for _ in 0..30 {
705 scene.frame(|ui, [_, split]| {
706 let anchor = Anchor::Below(split);
707 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);
708 });
709 }
710}
711
712fn highlight(scene: &mut Scene) {
713 let swatches = [
714 (0xff, 0xff, 0x00),
715 (0x00, 0xff, 0x00),
716 (0x00, 0xff, 0xff),
717 (0xff, 0x00, 0xff),
718 (0x00, 0x00, 0xff),
719 (0xff, 0x00, 0x00),
720 (0x00, 0x00, 0x80),
721 (0x00, 0x80, 0x80),
722 (0x00, 0x80, 0x00),
723 (0x80, 0x00, 0x80),
724 (0x80, 0x00, 0x00),
725 (0x80, 0x80, 0x00),
726 (0x80, 0x80, 0x80),
727 (0xc0, 0xc0, 0xc0),
728 (0x00, 0x00, 0x00),
729 ]
730 .map(|(r, g, b)| (draw::srgb(r, g, b), ""));
731 let id = Id::ROOT.child("highlight");
732 scene.open(id, vec![key(NamedKey::ArrowDown)], |ui, [_, split]| {
733 let anchor = Anchor::Below(split);
734 ui::popup::colors(ui, id, anchor, "No Color", &swatches, 5);
735 });
736}
737
738fn palette(scene: &mut Scene) {
739 let commands = [
740 ("New page", "⌘N"),
741 ("New section", "⌘T"),
742 ("New notebook", ""),
743 ("Open notebook…", "⌘O"),
744 ("Go to page…", "⌘G"),
745 ("Insert table", "⌥⌘T"),
746 ("Insert date", "⇧⌘D"),
747 ("Numbered list", "⌘/"),
748 ("Bulleted list", "⌘."),
749 ("Toggle page list", ""),
750 ("Zoom in", "⌘="),
751 ("Zoom out", "⌘-"),
752 ]
753 .map(|(text, shortcut)| Item {
754 text,
755 shortcut,
756 ..Item::default()
757 });
758 let id = Id::ROOT.child("palette");
759 scene.open(id, typed("ne"), |ui, _| {
760 ui::popup::palette(ui, id, &[("", &commands)], "Search commands");
761 });
762}
763
764fn tags(scene: &mut Scene) {
765 let tags = [
766 ("To Do", None::<&'static [&'static str]>, "⌘1"),
767 (
768 "Important",
769 Some(&[include_str!("../../canvas/assets/tags/star.svg")]),
770 "⌘2",
771 ),
772 ("Question", None, "⌘3"),
773 ("Remember for later", Some(art!("tags/remember")), "⌘4"),
774 ("Definition", Some(art!("tags/definition")), "⌘5"),
775 (
776 "Highlight",
777 Some(&[include_str!("../../canvas/assets/tags/highlight.svg")]),
778 "⌘6",
779 ),
780 (
781 "Contact",
782 Some(&[include_str!("../../canvas/assets/tags/contact.svg")]),
783 "⌘7",
784 ),
785 (
786 "Address",
787 Some(&[include_str!("../../canvas/assets/tags/address.svg")]),
788 "⌘8",
789 ),
790 (
791 "Phone number",
792 Some(&[include_str!("../../canvas/assets/tags/phone.svg")]),
793 "⌘9",
794 ),
795 ("Web site to visit", None, ""),
796 ("Idea", None, ""),
797 ("Password", None, ""),
798 ("Critical", None, ""),
799 ("Project A", None, ""),
800 ("Project B", None, ""),
801 ]
802 .map(|(text, icon, shortcut)| Item {
803 text,
804 icon,
805 tint: Some([1.0; 4]),
806 shortcut,
807 ..Item::default()
808 });
809 let id = Id::ROOT.child("tags");
810 let build = |ui: &mut Ui, [_, split]: [Id; 2]| {
811 ui::popup::menu(ui, id, Anchor::Below(split), &tags, Some("Filter tags"));
812 };
813 scene.open(id, Vec::new(), build);
814 let row = scene.ui.laid_out(id.child("rows").child(3_usize)).unwrap();
815 scene
816 .ui
817 .event(Event::PointerMoved([row[0] + 40.0, row[1] + 8.0]));
818 for _ in 0..3 {
819 scene.frame(build);
820 }
821}
822
823/// Lays out panels of `PANEL` in rows of `columns` and writes them to `path`.
824fn render(panels: Vec<Vec<u8>>, columns: usize, path: &str) {
825 let panel = PANEL.map(|length| (length * SCALE) as u32);
826 let gap = 16;
827 let rows = panels.len().div_ceil(columns) as u32;
828 let size = [
829 (panel[0] + gap) * columns as u32 + gap,
830 (panel[1] + gap) * rows + gap,
831 ];
832 let mut image = vec![0x80; (size[0] * size[1] * 4) as usize];
833 for (index, pixels) in panels.iter().enumerate() {
834 let [left, top] = [
835 gap + (index % columns) as u32 * (panel[0] + gap),
836 gap + (index / columns) as u32 * (panel[1] + gap),
837 ];
838 for (y, line) in pixels.chunks((panel[0] * 4) as usize).enumerate() {
839 let start = (((top + y as u32) * size[0] + left) * 4) as usize;
840 image[start..start + line.len()].copy_from_slice(line);
841 }
842 }
843 let file = std::fs::File::create(path).unwrap();
844 let mut encoder = png::Encoder::new(std::io::BufWriter::new(file), size[0], size[1]);
845 encoder.set_color(png::ColorType::Rgba);
846 encoder.set_depth(png::BitDepth::Eight);
847 encoder
848 .write_header()
849 .unwrap()
850 .write_image_data(&image)
851 .unwrap();
852}
853
854/// Paints `ui`'s frame, each on its own so the atlas holds one popup as the app's would,
855/// and reads it back.
856fn paint(
857 device: &wgpu::Device,
858 queue: &wgpu::Queue,
859 renderer: &mut draw::Renderer,
860 ui: &Ui,
861) -> Vec<u8> {
862 let size = PANEL.map(|length| (length * SCALE) as u32);
863 let interface = ui.layers();
864 let layers: Vec<_> = interface
865 .iter()
866 .filter_map(|layer| match layer {
867 ui::Layer::Primitives(primitives) => Some(primitives.layer(SCALE)),
868 ui::Layer::Custom { .. } => None,
869 })
870 .collect();
871 let texture = device.create_texture(&wgpu::TextureDescriptor {
872 label: Some("Popups"),
873 size: wgpu::Extent3d {
874 width: size[0],
875 height: size[1],
876 depth_or_array_layers: 1,
877 },
878 mip_level_count: 1,
879 sample_count: 1,
880 dimension: wgpu::TextureDimension::D2,
881 format: wgpu::TextureFormat::Rgba8UnormSrgb,
882 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
883 view_formats: &[],
884 });
885 renderer
886 .draw(
887 &texture.create_view(&Default::default()).into(),
888 size,
889 ui.theme.base,
890 &layers,
891 )
892 .unwrap();
893 let row = (size[0] * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
894 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
895 label: Some("Popups readback"),
896 size: u64::from(row) * u64::from(size[1]),
897 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
898 mapped_at_creation: false,
899 });
900 let mut encoder = device.create_command_encoder(&Default::default());
901 encoder.copy_texture_to_buffer(
902 texture.as_image_copy(),
903 wgpu::TexelCopyBufferInfo {
904 buffer: &buffer,
905 layout: wgpu::TexelCopyBufferLayout {
906 offset: 0,
907 bytes_per_row: Some(row),
908 rows_per_image: Some(size[1]),
909 },
910 },
911 wgpu::Extent3d {
912 width: size[0],
913 height: size[1],
914 depth_or_array_layers: 1,
915 },
916 );
917 queue.submit([encoder.finish()]);
918 buffer.map_async(wgpu::MapMode::Read, .., |result| result.unwrap());
919 device
920 .poll(wgpu::PollType::Wait {
921 submission_index: None,
922 timeout: Some(Duration::from_secs(5)),
923 })
924 .unwrap();
925 let mapped = buffer.get_mapped_range(..).unwrap();
926 mapped
927 .chunks(row as usize)
928 .flat_map(|line| &line[..size[0] as usize * 4])
929 .copied()
930 .collect()
931}