1//! Controls a notebook window's chrome is built from: OneNote's section tabs and the
2//! toolbar's compact buttons.
3
4use draw::{PathStyle, edit::SelectionUnit};
5
6use crate::{
7 Anchor, Corner, Display, Extent, Flags, Id, Section, Shape, Signal, Spec, Ui, children, fill,
8 mix, px,
9};
10use accesskit::{HasPopup, Role};
11use std::hash::Hash;
12
13/// A menu's downward arrow.
14pub const CHEVRON: &[&str] = &[include_str!("../assets/chevron-down.svg")];
15/// Rounding of a section tab's top leading corner; the trailing one takes half.
16const TAB_ROUNDING: f32 = 4.0;
17/// Side of a toolbar button.
18pub const TOOL: f32 = 22.0;
19/// Width of a menu arrow beside a toolbar button.
20const ARROW: f32 = 10.0;
21/// Width of a button showing only a menu arrow, which ends its group as tight as an icon does.
22const MORE: f32 = 14.0;
23/// Where a section tab's label starts inside it.
24pub const TAB_PAD: f32 = 10.0;
25/// Side of the dot marking something unread, as Mail marks unread mail.
26pub const DOT: f32 = 6.0;
27
28/// The dot marking something unread, in the accent, floating in the box being built at
29/// `position`, its top left corner.
30pub fn unread_dot(ui: &mut Ui, part: impl std::hash::Hash, position: [f32; 2]) {
31 ui.leaf(
32 part,
33 Spec {
34 flags: Flags::FLOAT,
35 size: [px(DOT), px(DOT)],
36 position,
37 fill: Some(ui.theme.accent),
38 radius: DOT / 2.0,
39 ..Spec::default()
40 },
41 );
42}
43/// How far section tabs fade out at an end of their row they are cut at, and how near it a
44/// dragged tab scrolls the row on.
45const EDGE: f32 = 20.0;
46
47/// Section tabs in a row `height` tall, as OneNote draws them: each leans over the next
48/// at 45°, and the open one lies on top in `section`'s colours, rising to meet the frame
49/// below as its outline and shadow fade in. `tabs` are names and section colours; `lit`
50/// shows as hovered, as a tab something is dragged onto. A `dragged` tab follows the
51/// pointer, lifted over the others, which slide aside to open the gap it would land in;
52/// each eases to its place once let go, and near the row's ends the row scrolls on.
53///
54/// The row is as wide as the tabs, and gives up all of that where its own row lacks room.
55/// Then the tabs scroll sideways under the wheel, either way it turns, fading out at the
56/// ends they are cut at into the row's `fill`, and the open tab scrolls into view as it
57/// opens. A tab given `true` after its colour shows the unread dot in its leading pad. The tab
58/// `renaming` holds what its builder builds inside it, given the tab's height.
59#[allow(clippy::too_many_arguments, clippy::type_complexity)]
60pub fn section_tabs(
61 ui: &mut Ui,
62 row: Id,
63 tabs: &[(&str, [f32; 4], bool)],
64 active: usize,
65 lit: Option<usize>,
66 dragged: Option<Dragged>,
67 mut renaming: Option<(usize, &mut dyn FnMut(&mut Ui, f32))>,
68 section: &Section,
69 height: f32,
70 fill: [f32; 4],
71) -> Tabs {
72 let theme = ui.theme.clone();
73 // Room for the first tab's outline and shadow, which the row would clip.
74 let mut left = crate::SHADOW[0];
75 let placed: Vec<_> = tabs
76 .iter()
77 .map(|&(name, ..)| {
78 let label = ui
79 .texts
80 .styled(name, ui.theme.font_size, false, false, None, ui.frame);
81 let width = TAB_PAD + label.size[0] + 4.0 + lean(height);
82 left += width;
83 (left - width, width)
84 })
85 .collect();
86 // The last tab's slant and shadow reach past its box.
87 let wide = left + lean(height) + crate::SHADOW[0];
88 let view = ui.laid_out(row).map_or(wide, |rect| rect[2] - rect[0]);
89 let most = (wide - view).max(0.0);
90 let [scroll, shown, shift] = [row.child("scroll"), row.child("shown"), row.child("offset")];
91 let held_value = |ui: &Ui, id: Id| ui.states.get(&id).and_then(|state| state.tween);
92 let mut target = held_value(ui, scroll).map_or(0.0, |[_, target]| target);
93 // The wheel moves the tabs at once from where they show, as on the page; only jumps ease.
94 let mut wheeled = false;
95 for event in ui.signal(row).events {
96 if let crate::Event::Wheel([x, y]) = event {
97 if !wheeled {
98 target = held_value(ui, shift).map_or(target, |[value, _]| value);
99 }
100 target -= x + y;
101 wheeled = true;
102 }
103 }
104 if held_value(ui, shown).is_none_or(|[_, was]| was != active as f32)
105 && let Some((x, width)) = placed.get(active)
106 {
107 target = target.min(*x).max(x + width + lean(height) - view);
108 }
109 ui.hold(shown, active as f32);
110 // A tab dragged near either end scrolls the row on, as long as it is held there.
111 let start = dragged.and_then(|dragged| {
112 let start = dragged.start?;
113 let width = placed[dragged.index].1;
114 let push = (start - EDGE).min(0.0) + (start + width - view + EDGE).max(0.0);
115 if push != 0.0 && (target > 0.0 || push > 0.0) && (target < most || push < 0.0) {
116 target += push * ui.dt * 8.0;
117 ui.animating = true;
118 }
119 Some(start)
120 });
121 let target = ui.hold(scroll, target.clamp(0.0, most));
122 let offset = if wheeled {
123 ui.hold(shift, target)
124 } else {
125 ui.animate(shift, target)
126 };
127 let fade = |cut: bool| if cut { EDGE } else { 0.0 };
128 ui.open_as(
129 row,
130 Spec {
131 flags: Flags::CLIP | Flags::SCROLL,
132 size: [
133 Extent {
134 size: crate::Size::Pixels(wide),
135 strictness: 0.0,
136 },
137 px(height),
138 ],
139 fill: Some(fill),
140 fade: [fade(offset > 0.5), fade(offset < most - 0.5)],
141 role: Some(Role::TabList),
142 ..Spec::default()
143 },
144 );
145 let [low, tallest] = [height - 6.0, height - 2.0];
146 let (mut clicked, mut context, mut held, mut renamed) = (None, None, None, None);
147 let spans: Vec<[f32; 2]> = placed.iter().map(|(x, width)| [*x, *width]).collect();
148 // The dragged tab's place along the tabs, however far they are scrolled.
149 let start = start.map(|start| start + offset);
150 let slot = dragged.and_then(|dragged| {
151 let width = placed[dragged.index].1;
152 Some(crate::drop_slot(
153 &spans,
154 dragged.index,
155 start? + width / 2.0,
156 ))
157 });
158 let lifted = dragged.map(|dragged| dragged.index);
159 let mut settled = true;
160 // Earlier tabs lie over later ones; the open tab over them, and a dragged one over all.
161 let order = (0..tabs.len())
162 .rev()
163 .filter(|index| *index != active && Some(*index) != lifted)
164 .chain((active < tabs.len() && Some(active) != lifted).then_some(active))
165 .chain(lifted);
166 for index in order {
167 let (name, color, unread) = tabs[index];
168 let (x, width) = placed[index];
169 // Keyed by name, so a tab eases from where it stood when the tabs are reordered.
170 let key = ui.id(("x", name));
171 let x = match (dragged, slot, start) {
172 (_, _, Some(start)) if lifted == Some(index) => ui.hold(key, start),
173 (Some(dragged), Some(slot), _) => ui.animate(
174 key,
175 x + crate::slide(index, dragged.index, slot, placed[dragged.index].1),
176 ),
177 _ => {
178 let eased = ui.animate(key, x);
179 settled &= lifted != Some(index) || (eased - x).abs() < 0.5;
180 eased
181 }
182 };
183 let lift = f32::from(u8::from(lifted == Some(index)));
184 let open = ui.animate(
185 ui.id(("open", name)),
186 f32::from(u8::from(index == active || lifted == Some(index))),
187 );
188 // Whole device pixels, so a rising tab reuses its rasterized outlines.
189 let tall = ((low + (tallest - low) * open) * ui.scale()).round() / ui.scale();
190 let colors = theme.section(color);
191 // A lifted tab rises in its own colour; only the open one takes the frame's.
192 let top = if index == active { section } else { &colors };
193 let fill = mix(colors.tab, top.frame[0], open);
194 let fade = |color: [f32; 4], alpha: f32| [color[0], color[1], color[2], color[3] * alpha];
195 let id = ui.open(
196 ("tab", index),
197 Spec {
198 flags: Flags::CLICKABLE | Flags::FLOAT,
199 size: [px(width), px(tall)],
200 position: [x - offset, height - tall],
201 text: Some(name),
202 color: Some(mix(mix(theme.ink, fill, 0.2), theme.ink, open)),
203 fill: Some(if lit == Some(index) {
204 mix(colors.frame[0], [1.0; 4], 0.08)
205 } else {
206 mix(fill, [1.0; 4], 0.08)
207 }),
208 gradient: Some(fill),
209 hover_fill: (index != active).then(|| mix(colors.frame[0], [1.0; 4], 0.08)),
210 // A tab not open is outlined in its own hue, fainter than the open one's.
211 border: Some(mix(mix(colors.tab, colors.edge, 0.55), top.edge, open)),
212 shadow: (open > 0.0).then(|| fade([0.0, 0.0, 0.0, 0.35 + 0.15 * lift], open)),
213 radius: TAB_ROUNDING,
214 shape: Shape::Tab { lean: lean(height) },
215 pad: [TAB_PAD, 0.0],
216 role: Some(Role::Tab),
217 ..Spec::default()
218 },
219 );
220 if let Some((_, build)) = renaming.as_mut().filter(|(at, _)| *at == index) {
221 build(ui, tall);
222 }
223 ui.close();
224 let signal = ui.signal(id);
225 if unread {
226 let position = [
227 x - offset + (TAB_PAD - DOT) / 2.0,
228 height - tall + (tall - DOT) / 2.0,
229 ];
230 unread_dot(ui, ("unread", index), position);
231 }
232 if let Some(node) = ui.access(tab_id(row, index)) {
233 node.set_selected(index == active);
234 }
235 if signal.clicked && index != active {
236 clicked = Some(index);
237 }
238 if let Some(point) = signal.context {
239 context = Some((index, point));
240 }
241 if signal.dragging {
242 held = Some(index);
243 }
244 if signal.pressed && signal.unit != SelectionUnit::Grapheme {
245 renamed = Some(index);
246 }
247 }
248 let open_tab = ui.id(("tab", active));
249 ui.close();
250 Tabs {
251 clicked,
252 context,
253 held,
254 renamed,
255 slot,
256 settled,
257 open: open_tab,
258 }
259}
260
261/// A section tab being dragged: its index, and its leading edge along the row while it
262/// follows the pointer; none once let go, as it eases into its place over the others.
263#[derive(Clone, Copy, Debug)]
264pub struct Dragged {
265 pub index: usize,
266 pub start: Option<f32>,
267}
268
269/// The id of tab `index` of the section tabs built as `row`.
270pub fn tab_id(row: Id, index: usize) -> Id {
271 row.child(("tab", index))
272}
273
274/// What the section tabs were asked this frame.
275pub struct Tabs {
276 pub clicked: Option<usize>,
277 /// The tab whose context menu was asked for, and where.
278 pub context: Option<(usize, [f32; 2])>,
279 /// The tab held down, which a drag moves.
280 pub held: Option<usize>,
281 /// The tab pressed twice in a row, which OneNote renames in place.
282 pub renamed: Option<usize>,
283 /// Where the dragged tab would land, as an index in the tabs' new order.
284 pub slot: Option<usize>,
285 /// The dragged tab, let go, stands in its place.
286 pub settled: bool,
287 /// The open tab's id, for `tab_base`.
288 pub open: Id,
289}
290
291/// Where the tab laid out at `tab` in a row `height` tall meets the edge below it: from its
292/// leading edge to the foot of its slant.
293pub fn tab_base(tab: [f32; 4], height: f32) -> [f32; 2] {
294 [tab[0], tab[2] - lean(height) + (tab[3] - tab[1])]
295}
296
297/// How far inside a tab's box its slant starts: half the open tab's height.
298fn lean(height: f32) -> f32 {
299 (height - 2.0) / 2.0
300}
301
302/// A square button showing `icon` tinted by `tint`, a toggle lit while `on` is `Some(true)`
303/// or a plain button where it is `None`; artwork in its own colours takes white. A tooltip
304/// names it.
305pub fn tool_button(
306 ui: &mut Ui,
307 part: impl Hash,
308 icon: &'static [&'static str],
309 tint: [f32; 4],
310 on: Option<bool>,
311) -> Signal {
312 let hover = ui.theme.hover();
313 let id = ui.open(
314 part,
315 Spec {
316 flags: Flags::CLICKABLE,
317 size: [px(TOOL), px(TOOL)],
318 icon: Some(icon),
319 color: Some(tint),
320 fill: (on == Some(true)).then_some(hover).or(rest(&ui.theme)),
321 hover_fill: Some(hover),
322 radius: 4.0,
323 center: true,
324 role: Some(Role::Button),
325 ..Spec::default()
326 },
327 );
328 press_state(ui, id, on);
329 ui.close();
330 ui.signal(id)
331}
332
333/// A tool button's face at rest, where the theme gives it one.
334fn rest(theme: &crate::Theme) -> Option<[f32; 4]> {
335 (theme.tool[3] > 0.0).then_some(theme.tool)
336}
337
338/// Shows button `id` as a toggle, pressed or not, where `on` is given.
339fn press_state(ui: &mut Ui, id: Id, on: Option<bool>) {
340 if let Some(on) = on
341 && let Some(node) = ui.access(id)
342 {
343 node.set_toggled(on.into());
344 }
345}
346
347/// Marks control `id` as opening popup `menu`, and whether it is open.
348fn opens(ui: &mut Ui, id: Id, menu: Id) {
349 let open = ui.popup_open(menu);
350 if let Some(node) = ui.access(id) {
351 node.set_has_popup(HasPopup::Menu);
352 node.set_expanded(open);
353 }
354}
355
356/// `color` as a control that does not apply now shows it.
357fn faded([red, green, blue, alpha]: [f32; 4]) -> [f32; 4] {
358 [red, green, blue, alpha * 0.35]
359}
360
361/// A tool button, with a menu arrow where `menu`, whose command does not apply now: `icon`
362/// tinted by `tint` shows faded, and it takes no clicks.
363pub fn unavailable(
364 ui: &mut Ui,
365 part: impl Hash,
366 icon: &'static [&'static str],
367 tint: [f32; 4],
368 menu: bool,
369) {
370 let arrow = faded(ui.theme.text_dim);
371 let id = ui.open(
372 part,
373 Spec {
374 size: [children(), px(TOOL)],
375 role: Some(Role::Button),
376 ..Spec::default()
377 },
378 );
379 if let Some(node) = ui.access(id) {
380 node.set_disabled();
381 if menu {
382 node.set_has_popup(HasPopup::Menu);
383 }
384 }
385 ui.leaf(
386 "icon",
387 Spec {
388 size: [px(TOOL), px(TOOL)],
389 icon: Some(icon),
390 color: Some(faded(tint)),
391 center: true,
392 ..Spec::default()
393 },
394 );
395 if menu {
396 ui.leaf(
397 "arrow",
398 Spec {
399 size: [px(ARROW), px(TOOL)],
400 icon: Some(CHEVRON),
401 color: Some(arrow),
402 center: true,
403 ..Spec::default()
404 },
405 );
406 }
407 ui.close();
408}
409
410/// A button `name`d and showing `icon`, a toggle lit while `on` as `tool_button`'s, joined
411/// to an arrow that opens popup `menu`, with the colour it applies as a bar under the icon
412/// when given. Hovering the button fills it alone; hovering the arrow, or its menu being
413/// open, outlines both as one control. The arrow is named "`name` Options", as Office names
414/// its split buttons' arrows. Returns the button's signal.
415#[allow(clippy::too_many_arguments)]
416pub fn split_button(
417 ui: &mut Ui,
418 part: impl Hash,
419 name: &str,
420 icon: &'static [&'static str],
421 bar: Option<[f32; 4]>,
422 on: Option<bool>,
423 menu: Id,
424) -> Signal {
425 const RADIUS: f32 = 4.0;
426 let hover = ui.theme.hover();
427 let fade =
428 |alpha: f32| (alpha > 0.0).then_some([hover[0], hover[1], hover[2], hover[3] * alpha]);
429 let split = ui.id(part);
430 let [button, arrow] = [split.child("button"), split.child("menu")];
431 let lit = ui.animate(
432 split.child("lit"),
433 f32::from(u8::from(on == Some(true) || ui.hover == Some(button))),
434 );
435 let outlined = ui.hover == Some(arrow) || ui.popup_open(menu);
436 let ring = ui.animate(split.child("ring"), f32::from(u8::from(outlined)));
437 ui.open_as(
438 split,
439 Spec {
440 size: [children(), px(TOOL)],
441 ..Spec::default()
442 },
443 );
444 ui.open_as(
445 button,
446 Spec {
447 flags: Flags::CLICKABLE | Flags::CLIP,
448 size: [px(TOOL), px(TOOL)],
449 role: Some(Role::Button),
450 ..Spec::default()
451 },
452 );
453 press_state(ui, button, on);
454 if let Some(node) = ui.access(button) {
455 node.set_label(name);
456 }
457 // Rounded past the clip, so the fill meets the arrow square.
458 ui.leaf(
459 "face",
460 Spec {
461 flags: Flags::FLOAT,
462 size: [px(TOOL + RADIUS), px(TOOL)],
463 fill: fade(lit).or(rest(&ui.theme)),
464 radius: RADIUS,
465 ..Spec::default()
466 },
467 );
468 ui.open(
469 "icon",
470 Spec {
471 size: [px(TOOL), px(TOOL)],
472 icon: Some(icon),
473 color: Some(ui.theme.text),
474 center: true,
475 ..Spec::default()
476 },
477 );
478 if let Some(color) = bar {
479 // The icon leaves its lowest three units for the bar.
480 let top = (TOOL - 16.0) / 2.0;
481 ui.mark([top, top + 13.0, top + 16.0, top + 16.0], color, 0.0);
482 }
483 ui.close();
484 ui.close();
485 ui.open_as(
486 arrow,
487 Spec {
488 flags: Flags::CLICKABLE,
489 size: [px(ARROW), px(TOOL)],
490 icon: Some(CHEVRON),
491 color: Some(ui.theme.text_dim),
492 center: true,
493 role: Some(Role::Button),
494 ..Spec::default()
495 },
496 );
497 opens(ui, arrow, menu);
498 if let Some(node) = ui.access(arrow) {
499 node.set_label(format!("{name} Options"));
500 }
501 ui.close();
502 if ui.signal(arrow).pressed {
503 ui.open_popup(menu);
504 }
505 ui.leaf(
506 "ring",
507 Spec {
508 flags: Flags::FLOAT,
509 size: [px(TOOL + ARROW), px(TOOL)],
510 border: fade(ring),
511 radius: RADIUS,
512 ..Spec::default()
513 },
514 );
515 ui.close();
516 ui.signal(button)
517}
518
519/// A narrow button showing only a menu arrow, which opens popup `menu` of more of a group's
520/// commands at the group's end; where not `enabled` it shows faded and takes no clicks.
521pub fn more_button(ui: &mut Ui, part: impl Hash, menu: Id, enabled: bool) -> Anchor {
522 let theme = ui.theme.clone();
523 let open = ui.popup_open(menu);
524 let id = ui.open(
525 part,
526 Spec {
527 flags: if enabled {
528 Flags::CLICKABLE
529 } else {
530 Flags::default()
531 },
532 size: [px(MORE), px(TOOL)],
533 icon: Some(CHEVRON),
534 color: Some(if enabled {
535 theme.text_dim
536 } else {
537 faded(theme.text_dim)
538 }),
539 fill: open.then(|| theme.hover()).or(rest(&theme)),
540 hover_fill: enabled.then(|| theme.hover()),
541 radius: 4.0,
542 center: true,
543 role: Some(Role::Button),
544 ..Spec::default()
545 },
546 );
547 if let Some(node) = ui.access(id) {
548 node.set_has_popup(HasPopup::Menu);
549 node.set_expanded(open);
550 if !enabled {
551 node.set_disabled();
552 }
553 }
554 ui.close();
555 if enabled && ui.signal(id).pressed {
556 ui.open_popup(menu);
557 }
558 Anchor::Below(id)
559}
560
561/// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle
562/// lit while `on` as `tool_button`'s. Returns where the menu opens so its icons line up
563/// under the button's.
564pub fn menu_button(
565 ui: &mut Ui,
566 part: impl Hash,
567 icon: &'static [&'static str],
568 on: Option<bool>,
569 menu: Id,
570) -> Anchor {
571 let theme = ui.theme.clone();
572 let id = ui.open(
573 part,
574 Spec {
575 flags: Flags::CLICKABLE,
576 size: [px(TOOL + ARROW), px(TOOL)],
577 fill: (ui.popup_open(menu) || on == Some(true))
578 .then(|| theme.hover())
579 .or(rest(&theme)),
580 hover_fill: Some(theme.hover()),
581 radius: 4.0,
582 role: Some(Role::Button),
583 ..Spec::default()
584 },
585 );
586 opens(ui, id, menu);
587 press_state(ui, id, on);
588 for (part, icon, width, color) in [
589 ("icon", icon, TOOL, theme.text),
590 ("arrow", CHEVRON, ARROW, theme.text_dim),
591 ] {
592 ui.leaf(
593 part,
594 Spec {
595 size: [px(width), px(TOOL)],
596 icon: Some(icon),
597 color: Some(color),
598 center: true,
599 ..Spec::default()
600 },
601 );
602 }
603 // The menu opens from the button shifted so its icons line up under the button's.
604 let style = ui.theme.menu();
605 let from = ui.id("menu");
606 ui.leaf(
607 "menu",
608 Spec {
609 flags: Flags::FLOAT,
610 size: [px(TOOL + ARROW), px(TOOL)],
611 position: [(TOOL - crate::ICON) / 2.0 - style.pad - style.row_pad, 0.0],
612 ..Spec::default()
613 },
614 );
615 ui.close();
616 if ui.signal(id).pressed {
617 ui.open_popup(menu);
618 }
619 Anchor::Below(from)
620}
621
622/// A drop-down box `name`d, `width` wide, showing `text`, that opens popup `menu`; where
623/// not `enabled` it shows faded and takes no clicks or focus, as `unavailable` buttons do.
624pub fn combo(
625 ui: &mut Ui,
626 part: impl Hash,
627 name: &str,
628 text: &str,
629 width: impl Into<Extent>,
630 menu: Id,
631 enabled: bool,
632) {
633 let theme = ui.theme.clone();
634 let faded = |color| if enabled { color } else { faded(color) };
635 let id = ui.open(
636 part,
637 Spec {
638 flags: if enabled {
639 Flags::CLICKABLE
640 } else {
641 Flags::default()
642 },
643 size: [width.into(), px(TOOL)],
644 fill: Some(theme.base),
645 hover_border: enabled.then_some(theme.accent),
646 border: Some(faded(theme.chip)),
647 radius: 4.0,
648 pad: [8.0, 0.0],
649 role: Some(Role::ComboBox),
650 ..Spec::default()
651 },
652 );
653 opens(ui, id, menu);
654 if let Some(node) = ui.access(id) {
655 node.set_label(name);
656 node.set_value(text);
657 if !enabled {
658 node.set_disabled();
659 }
660 }
661 ui.leaf(
662 "text",
663 Spec {
664 size: [fill(), px(TOOL)],
665 text: Some(text),
666 color: Some(faded(theme.text)),
667 ..Spec::default()
668 },
669 );
670 ui.leaf(
671 "arrow",
672 Spec {
673 size: [px(12.0), px(TOOL)],
674 icon: Some(CHEVRON),
675 color: Some(faded(theme.text_dim)),
676 center: true,
677 ..Spec::default()
678 },
679 );
680 ui.close();
681 if enabled && ui.signal(id).pressed {
682 ui.open_popup(menu);
683 }
684}
685
686impl Ui {
687 /// Paints `item` over the frame's boxes but under its popups.
688 fn beneath_popups(&mut self, item: Display) {
689 self.display.insert(self.popups_painted, item);
690 self.popups_painted += 1;
691 }
692
693 /// Draws a 1 px border along `points`, each with the radius its corner rounds by,
694 /// joining the last back to the first when `closed`. Called between `end` and
695 /// `layers`, it paints over the frame's boxes where they were just laid out.
696 pub fn border(&mut self, points: &[([f32; 2], f32)], closed: bool, color: [f32; 4]) {
697 let count = points.len();
698 let corners: Vec<_> = (0..count)
699 .map(|index| {
700 let open_end = !closed && (index == 0 || index == count - 1);
701 (!open_end && points[index].1 > 0.0).then(|| Corner::new(points, index))
702 })
703 .collect();
704 for index in 0..count - usize::from(!closed) {
705 let next = (index + 1) % count;
706 let from = corners[index]
707 .as_ref()
708 .map_or(points[index].0, |corner| corner.end);
709 let to = corners[next]
710 .as_ref()
711 .map_or(points[next].0, |corner| corner.start);
712 self.beneath_popups(Display::Segment { from, to, color });
713 }
714 for corner in corners.into_iter().flatten() {
715 let start = [
716 corner.start[0] - corner.point[0],
717 corner.start[1] - corner.point[1],
718 ];
719 self.beneath_popups(Display::Path {
720 data: format!("M{} {}{}", start[0], start[1], corner.curve(corner.point)),
721 origin: corner.point,
722 style: PathStyle::Stroke(1.0),
723 colors: [color; 2],
724 });
725 }
726 }
727
728 /// Rounds the corners of the clockwise outline `points`, as `border` does: paints
729 /// `outside(y)` beyond each outward corner at height `y`, and `inside` into each
730 /// inward one; a transparent colour cuts the corner out to the window's backdrop.
731 /// Called between `end` and `layers`.
732 pub fn round_corners(
733 &mut self,
734 points: &[([f32; 2], f32)],
735 inside: [f32; 4],
736 outside: impl Fn(f32) -> [f32; 4],
737 ) {
738 for index in 0..points.len() {
739 if points[index].1 <= 0.0 {
740 continue;
741 }
742 let corner = Corner::new(points, index);
743 let start = [
744 corner.start[0] - corner.point[0],
745 corner.start[1] - corner.point[1],
746 ];
747 let color = if corner.outward {
748 outside(corner.point[1])
749 } else {
750 inside
751 };
752 let (style, color) = if color[3] == 0.0 {
753 (PathStyle::Erase, [0.0, 0.0, 0.0, 1.0])
754 } else {
755 (PathStyle::Fill, color)
756 };
757 self.beneath_popups(Display::Path {
758 data: format!(
759 "M{} {}{}L0 0Z",
760 start[0],
761 start[1],
762 corner.curve(corner.point)
763 ),
764 origin: corner.point,
765 style,
766 colors: [color; 2],
767 });
768 }
769 }
770}