1//! An immediate-mode interface kit. Builder code declares boxes every frame from
2//! application state; a cache keyed by stable ids keeps hover, press, focus, scroll and
3//! animation state. Input is answered with the previous frame's layout, so a frame's
4//! events are routed before building and its layout is solved after.
5
6mod access;
7mod layout;
8mod list;
9pub mod popup;
10pub mod shell;
11mod text;
12mod theme;
13mod widgets;
14
15pub use list::{List, Row, Rows, list};
16pub use theme::{Menu, PopupMotion, Section, Shades, Shadow, Theme};
17pub use widgets::{
18 PaintedScroller, Scroller, ScrollerPart, Scrollers, Segments, badge, badge_width, button,
19 check_box, dialog_buttons, edit_key, edit_modifiers, password_field, scrollbar, segmented,
20 text_field,
21};
22
23use draw::{
24 PathStyle, Primitive, RasterImage, Stroke,
25 edit::{Clicks, SelectionUnit},
26};
27use parley::editing::Selection;
28use std::{
29 collections::HashMap,
30 hash::{DefaultHasher, Hash, Hasher},
31 ops::BitOr,
32 rc::Rc,
33 time::Duration,
34};
35use text::{Label, Texts};
36use web_time::Instant;
37use winit::{
38 event::{Ime, MouseButton},
39 keyboard::{Key, ModifiersState, NamedKey},
40 window::CursorIcon,
41};
42
43/// Seconds for an animated value to close half of its remaining distance.
44const HALF_LIFE: f32 = 0.03;
45/// How far a box's shadow spreads, in logical pixels, and how far below the box it
46/// falls; a popup's, floating higher, spreads and falls further.
47pub const SHADOW: [f32; 2] = [3.0, 0.0];
48const POPUP_SHADOW: [f32; 2] = [12.0, 4.0];
49/// Logical size of a box's icon, and its distance from the label.
50const ICON: f32 = 16.0;
51const ICON_GAP: f32 = 6.0;
52/// Seconds a dialog and other popups take to open and to close.
53const DIALOG: [f32; 2] = [0.24, 0.16];
54const POPUP: [f32; 2] = [0.16, 0.12];
55/// Seconds the pointer rests on a control before its tooltip shows, as Windows' tooltips
56/// wait a double-click interval; after one shows, others show at once for `TIP_WARM`
57/// seconds, and one showing cold fades in over `TIP_FADE`.
58const TIP_DELAY: f32 = 0.5;
59const TIP_WARM: f32 = 0.5;
60const TIP_FADE: f32 = 0.1;
61/// How far outside a control the ring the keyboard's focus draws round it lies, and its width.
62const FOCUS_RING: f32 = 2.0;
63
64/// A box's identity across frames: its parent's id combined with a builder-chosen part.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66pub struct Id(u64);
67
68impl Id {
69 pub const ROOT: Id = Id(0);
70
71 pub fn child(self, part: impl Hash) -> Id {
72 let mut hasher = DefaultHasher::new();
73 self.0.hash(&mut hasher);
74 part.hash(&mut hasher);
75 Id(hasher.finish())
76 }
77
78 /// The box's node in the interface's accessibility tree.
79 pub fn node(self) -> accesskit::NodeId {
80 accesskit::NodeId(self.0)
81 }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub enum Size {
86 /// Logical pixels.
87 Pixels(f32),
88 /// The label and padding.
89 Text,
90 /// A fraction of the nearest ancestor not sized by its children; across a parent sized by
91 /// its children, a fraction of what the parent's other children make it.
92 Fraction(f32),
93 /// The children laid out along this axis, and padding.
94 Children,
95}
96
97/// A size on one axis and the share of it a box keeps when its siblings overflow.
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct Extent {
100 pub size: Size,
101 pub strictness: f32,
102}
103
104impl Default for Extent {
105 fn default() -> Self {
106 children()
107 }
108}
109
110/// Logical pixels, as `px`.
111impl From<f32> for Extent {
112 fn from(pixels: f32) -> Self {
113 px(pixels)
114 }
115}
116
117pub fn px(pixels: f32) -> Extent {
118 Extent {
119 size: Size::Pixels(pixels),
120 strictness: 1.0,
121 }
122}
123
124pub fn fit() -> Extent {
125 Extent {
126 size: Size::Text,
127 strictness: 1.0,
128 }
129}
130
131/// All the room the parent has, yielding to stricter siblings.
132pub fn fill() -> Extent {
133 Extent {
134 size: Size::Fraction(1.0),
135 strictness: 0.0,
136 }
137}
138
139pub fn children() -> Extent {
140 Extent {
141 size: Size::Children,
142 strictness: 1.0,
143 }
144}
145
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum Axis {
148 #[default]
149 X,
150 Y,
151}
152
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
154pub struct Flags(u32);
155
156impl Flags {
157 /// Takes pointer presses, clicks and drags, and blocks boxes beneath it.
158 pub const CLICKABLE: Flags = Flags(1);
159 /// A press gives it keyboard focus.
160 pub const FOCUSABLE: Flags = Flags(2);
161 /// The wheel scrolls its children vertically; they may overflow it.
162 pub const SCROLL: Flags = Flags(4);
163 /// Children paint only within it.
164 pub const CLIP: Flags = Flags(8);
165 /// Placed at its `position` in the parent instead of in the parent's flow.
166 pub const FLOAT: Flags = Flags(16);
167 /// The host paints it and receives the events routed to it.
168 pub const CUSTOM: Flags = Flags(32);
169 /// Paints in place while the popup around it opens, as a field standing in for the one
170 /// it opened from, and as tall as that box.
171 pub const STILL: Flags = Flags(64);
172
173 pub(crate) fn contains(self, other: Flags) -> bool {
174 self.0 & other.0 == other.0
175 }
176
177 fn intersects(self, other: Flags) -> bool {
178 self.0 & other.0 != 0
179 }
180}
181
182impl BitOr for Flags {
183 type Output = Flags;
184
185 fn bitor(self, other: Flags) -> Flags {
186 Flags(self.0 | other.0)
187 }
188}
189
190/// How a box's fill and border are outlined; `radius` rounds the named corners.
191#[derive(Clone, Copy, Debug, Default, PartialEq)]
192pub enum Shape {
193 #[default]
194 Rounded,
195 /// A section tab: the top leading corner rounded and the top trailing corner `lean`
196 /// pixels inside the box's width, from where the trailing edge leans out at 45°, so
197 /// neighbours overlap and a taller tab only reaches further along the bottom.
198 Tab { lean: f32 },
199}
200
201/// What a label does where it is wider than its box.
202#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
203pub enum Overflow {
204 /// Runs on as one line, cut at the box's edge.
205 #[default]
206 Clip,
207 /// Breaks into lines across the box, which grows to their height when sized by its text.
208 Wrap,
209 /// Stays one line, ending in an ellipsis where it is cut.
210 Ellipsis,
211}
212
213/// The least a popup keeps between itself and each of the window's edges.
214const POPUP_MARGIN: f32 = 4.0;
215
216/// The longest a popup may be in a window `room` long; one longer scrolls or squeezes.
217fn fitting(room: f32) -> f32 {
218 (room - 2.0 * POPUP_MARGIN).max(0.0)
219}
220
221/// Where a popup opens, flipping to the far side of its anchor where the window ends first.
222/// The box it opens beside is placed where layout puts it in the same frame.
223#[derive(Clone, Copy, Debug, PartialEq)]
224pub enum Anchor {
225 /// Under the box, from its leading edge, as a drop-down opens; at least as wide as it.
226 Below(Id),
227 /// Past the trailing edge of the box, or of the popup holding it, level with the box's
228 /// top, as a submenu opens.
229 Right(Id),
230 /// Over the box from its corner, the popup's contents covering it, as a combo box opens
231 /// into its own list; at least as wide as it.
232 Over(Id),
233 /// Under the box, centred on it, as a tooltip shows.
234 Tip(Id),
235 /// At a point, as a context menu opens.
236 Point([f32; 2]),
237 /// Centred across the window near its top, over the interface, which dims, as a dialog
238 /// opens.
239 Dialog,
240 /// Where a dialog opens, swinging in as one does but leaving the interface undimmed, as a
241 /// command palette opens.
242 Top,
243 /// Inside the box along its trailing edge, a dialog leaving the rest of the box in view and
244 /// the interface undimmed, as a side panel docks.
245 Dock(Id),
246}
247
248impl Anchor {
249 /// Where a popup `size` long on `axis` starts in a window `room` long: past `around`, the
250 /// rectangle it opens beside, on the axis it opens along, level with it otherwise. Shown
251 /// only `shown` long as it opens, it keeps the edge it would have at full size.
252 fn place(self, around: [f32; 4], axis: usize, size: f32, shown: f32, room: f32) -> f32 {
253 let size = size.min(fitting(room));
254 let shown = shown.min(size);
255 let most = (room - POPUP_MARGIN - size).max(POPUP_MARGIN);
256 let along = match self {
257 Anchor::Below(_) | Anchor::Point(_) => Some(1),
258 Anchor::Right(_) => Some(0),
259 Anchor::Over(_) => None,
260 Anchor::Tip(_) if axis == 0 => {
261 return ((around[0] + around[2] - size) / 2.0).clamp(POPUP_MARGIN, most);
262 }
263 Anchor::Tip(_) => Some(1),
264 Anchor::Dialog | Anchor::Top if axis == 0 => {
265 return ((room - size) / 2.0).max(POPUP_MARGIN);
266 }
267 Anchor::Dialog | Anchor::Top => return (room / 8.0).clamp(POPUP_MARGIN, most),
268 Anchor::Dock(_) if axis == 0 => {
269 return (around[2] - size - POPUP_MARGIN).clamp(POPUP_MARGIN, most);
270 }
271 Anchor::Dock(_) => return (around[1] + POPUP_MARGIN).clamp(POPUP_MARGIN, most),
272 };
273 let [low, high] = [around[axis], around[axis + 2]];
274 let (first, second) = if along == Some(axis) {
275 (high, low - size)
276 } else {
277 (low, high - size)
278 };
279 if first + size <= room - POPUP_MARGIN {
280 first.max(POPUP_MARGIN)
281 } else if second >= POPUP_MARGIN {
282 second.min(most) + size - shown
283 } else {
284 first.clamp(POPUP_MARGIN, most) + size - shown
285 }
286 }
287
288 /// Seconds the popup takes to open and to close.
289 fn durations(self) -> [f32; 2] {
290 if matches!(self, Anchor::Dialog | Anchor::Top | Anchor::Dock(_)) {
291 DIALOG
292 } else {
293 POPUP
294 }
295 }
296}
297
298/// A box and what it holds scaled by `zoom` about `pivot`, leaned back `tilt` radians about
299/// the line through it, and faded; logical pixels.
300#[derive(Clone, Copy, Debug, PartialEq)]
301struct Motion {
302 zoom: f32,
303 pivot: [f32; 2],
304 tilt: f32,
305 opacity: f32,
306}
307
308impl Motion {
309 const NONE: Motion = Motion {
310 zoom: 1.0,
311 pivot: [0.0; 2],
312 tilt: 0.0,
313 opacity: 1.0,
314 };
315}
316
317/// What a box is this frame. Colours are linear RGBA.
318#[derive(Clone, Default)]
319pub struct Spec<'a> {
320 pub flags: Flags,
321 pub size: [Extent; 2],
322 /// The axis its children flow along.
323 pub axis: Axis,
324 pub text: Option<&'a str>,
325 /// The label's size in logical pixels; the theme's otherwise.
326 pub font_size: Option<f32>,
327 /// Shapes the label semibold.
328 pub bold: bool,
329 /// Shapes the label italic.
330 pub italic: bool,
331 /// The family the label is shaped in, where it has the glyphs; the interface's otherwise.
332 pub font: Option<&'a str>,
333 pub overflow: Overflow,
334 /// The label's colour; the theme's text colour otherwise.
335 pub color: Option<[f32; 4]>,
336 pub fill: Option<[f32; 4]>,
337 /// The fill's colour at the bottom, fading from `fill` at the top.
338 pub gradient: Option<[f32; 4]>,
339 /// A soft shadow of the box's outline, painted beneath it; a popup other than a dialog
340 /// casts the theme's menu shadows instead.
341 pub shadow: Option<[f32; 4]>,
342 /// How far the box draws inside its leading, top, trailing and bottom edges: fill,
343 /// border, shadow, icon and label; the pointer still finds the whole box.
344 pub inset: [f32; 4],
345 /// The fill under the pointer, blended in as hover animates.
346 pub hover_fill: Option<[f32; 4]>,
347 pub border: Option<[f32; 4]>,
348 pub hover_border: Option<[f32; 4]>,
349 pub radius: f32,
350 pub shape: Shape,
351 /// 16 px artwork before the label, tinted with `tint` or else the label's colour.
352 pub icon: Option<&'static [&'static str]>,
353 pub tint: Option<[f32; 4]>,
354 /// A 16 px picture in the icon's place beside a label; without one, the picture fills
355 /// the box inside its padding.
356 pub image: Option<&'a RasterImage>,
357 /// Inset of the label and children on each axis.
358 pub pad: [f32; 2],
359 /// Space between children along the flow.
360 pub gap: f32,
361 pub center: bool,
362 /// For floating boxes, the offset from the parent's corner.
363 pub position: [f32; 2],
364 /// How far the box and its children draw and take the pointer from where they are laid
365 /// out, the boxes around them keeping their places, as a row sliding aside does.
366 pub offset: [f32; 2],
367 pub cursor: Option<CursorIcon>,
368 /// Floats the box over all others beside a rectangle in the window, as a popup
369 /// `Ui::open_popup` opened; boxes beneath take no input while one is open.
370 pub anchor: Option<Anchor>,
371 /// Makes the box a group of a row in two forms, its first two children, both built every
372 /// frame: its full form, and the one it folds to where the row lacks room. The row folds
373 /// its groups, and those in rows of its own sized by their children, by ascending
374 /// priority until it fits, once the space sized by ancestors has
375 /// yielded and before any other box gives up room. The form not shown takes no room,
376 /// paint or input, and its boxes take the group's rectangle, so what opens from them
377 /// opens from the form shown.
378 pub fold: Option<u32>,
379 /// How far the children fade out into the box's fill towards its start and end along
380 /// its axis, as a row or list of them cut there does.
381 pub fade: [f32; 2],
382 /// What the box is to assistive technology; without one, its text shows as a label and
383 /// its children as its parent's. `Ui::access` adds the rest of what it says.
384 pub role: Option<accesskit::Role>,
385}
386
387/// Input the host forwards; positions and wheel distances are logical pixels.
388#[derive(Clone, Debug, PartialEq)]
389pub enum Event {
390 PointerMoved([f32; 2]),
391 /// A pen's pressure from 0 to 1 for the pointer events that follow; none for a mouse.
392 Pressure(Option<f32>),
393 PointerLeft,
394 Button {
395 button: MouseButton,
396 pressed: bool,
397 at: Instant,
398 },
399 Wheel([f32; 2]),
400 Key {
401 key: Key,
402 text: Option<String>,
403 },
404 Ime(Ime),
405 Modifiers(ModifiersState),
406 /// Assistive technology acting on a box of the tree `Ui::accessibility` built.
407 Access(accesskit::ActionRequest),
408}
409
410/// How the user acted on a box this frame.
411#[derive(Clone, Debug, Default)]
412pub struct Signal {
413 pub hovered: bool,
414 /// The primary button went down on the box.
415 pub pressed: bool,
416 /// What the press selects text by: a single, double or triple click.
417 pub unit: SelectionUnit,
418 /// The primary button came up over the box it went down on.
419 pub clicked: bool,
420 /// The primary button is held after pressing the box.
421 pub dragging: bool,
422 /// The secondary button went down on the box, here: where its context menu opens.
423 pub context: Option<[f32; 2]>,
424 pub focused: bool,
425 /// For custom boxes: every event routed to the box, in order. For focused boxes:
426 /// keys, text and composition. For scrolling boxes: the wheel.
427 pub events: Vec<Event>,
428}
429
430/// A frame's painting, in order: interface primitives, or a host-painted box.
431pub enum Layer<'a> {
432 Primitives(Primitives<'a>),
433 Custom {
434 id: Id,
435 /// The box's visible logical rectangle.
436 rect: [f32; 4],
437 },
438}
439
440/// Interface primitives under a clip, in logical pixels, perhaps part of the way through
441/// opening or closing.
442pub struct Primitives<'a> {
443 pub clip: Option<[f32; 4]>,
444 /// A rounded rectangle and its corner radius the primitives paint only inside.
445 round: Option<([f32; 4], f32)>,
446 motion: Option<Motion>,
447 pub primitives: Vec<Primitive<'a>>,
448}
449
450impl Primitives<'_> {
451 /// The layer the renderer paints at `scale` device pixels per logical pixel.
452 pub fn layer(&self, scale: f32) -> draw::Layer<'_> {
453 let Motion {
454 zoom,
455 pivot,
456 tilt,
457 opacity,
458 } = self.motion.unwrap_or(Motion::NONE);
459 let device = |point: [f32; 2]| point.map(|value| value * scale);
460 let moved = |point: [f32; 2]| {
461 device(std::array::from_fn(|axis| {
462 pivot[axis] + (point[axis] - pivot[axis]) * zoom
463 }))
464 };
465 let moved_rect = |[left, top, right, bottom]: [f32; 4]| {
466 let [[left, top], [right, bottom]] = [moved([left, top]), moved([right, bottom])];
467 [left, top, right, bottom]
468 };
469 draw::Layer {
470 scale: scale * zoom,
471 origin: moved([0.0; 2]),
472 clip: self.clip.map(moved_rect),
473 backdrop: None,
474 round: self
475 .round
476 .map(|(rect, radius)| (moved_rect(rect), radius * scale * zoom)),
477 motion: self.motion.map(|_| draw::Motion {
478 opacity,
479 tilt,
480 pivot: device(pivot),
481 }),
482 primitives: &self.primitives,
483 }
484 }
485}
486
487struct Built {
488 id: Id,
489 parent: usize,
490 children: Vec<usize>,
491 flags: Flags,
492 size: [Extent; 2],
493 axis: Axis,
494 label: Option<Rc<Label>>,
495 overflow: Overflow,
496 color: [f32; 4],
497 fill: Option<[f32; 4]>,
498 gradient: Option<[f32; 4]>,
499 shadow: Option<[f32; 4]>,
500 inset: [f32; 4],
501 hover_fill: Option<[f32; 4]>,
502 border: Option<[f32; 4]>,
503 hover_border: Option<[f32; 4]>,
504 radius: f32,
505 shape: Shape,
506 icon: Option<&'static [&'static str]>,
507 tint: [f32; 4],
508 image: Option<RasterImage>,
509 pad: [f32; 2],
510 gap: f32,
511 center: bool,
512 position: [f32; 2],
513 offset: [f32; 2],
514 cursor: Option<CursorIcon>,
515 anchor: Option<Anchor>,
516 /// The rectangle a popup opens beside, as layout resolves its anchor.
517 around: [f32; 4],
518 /// How far open a popup over a box is, as it widens out of the box.
519 open: f32,
520 motion: Option<Motion>,
521 /// Rectangles relative to the box, painted over its fill.
522 marks: Vec<([f32; 4], [f32; 4], f32)>,
523 fold: Option<u32>,
524 fade: [f32; 2],
525 access: Option<accesskit::Node>,
526 /// Folded away with the form it belongs to: it takes no room, paint or input, and takes
527 /// its group's rectangle.
528 hidden: bool,
529 computed: [f32; 2],
530 relative: [f32; 2],
531 rect: [f32; 4],
532 content: f32,
533}
534
535#[derive(Default)]
536struct State {
537 touched: u64,
538 hot: f32,
539 /// The box paints differently under the pointer, so its hover is worth easing.
540 hovers: bool,
541 scroll: f32,
542 scroll_target: f32,
543 /// Height of a scrolling box's children last frame.
544 content: f32,
545 /// As last laid out; none for a box built this frame for the first time.
546 rect: Option<[f32; 4]>,
547 /// A text field's selection, and the selection and unit of the press a drag extends.
548 selection: Selection,
549 press: (Selection, SelectionUnit),
550 /// The text a field selects when next built, from and to byte offsets clamped to its end.
551 select: Option<[usize; 2]>,
552 /// Where a dragged scrollbar thumb was taken, from its start.
553 grab: f32,
554 /// A system scroller's part held down, and when a held arrow or track next repeats.
555 held: Option<(ScrollerPart, Instant)>,
556 /// An animated value and its target, for `Ui::animate`.
557 tween: Option<[f32; 2]>,
558 /// When the condition `Ui::lasted` follows last became true.
559 since: Option<Instant>,
560}
561
562/// An open popup, above the one opened before it.
563struct Popup {
564 id: Id,
565 /// The focus before it opened, restored when it closes.
566 focus: Option<Id>,
567 /// Its filter field's text, and the key of the row the keyboard or pointer last chose.
568 query: String,
569 highlight: Option<u64>,
570 /// A colour picker's hue in degrees, saturation and lightness, once it has one.
571 picked: Option<[f32; 3]>,
572 opened: Instant,
573 /// As a submenu, the row of the menu below it that it opens beside, whatever its anchor.
574 beside: Option<Id>,
575 /// A menu's row whose submenu opens, and when; None once it has.
576 submenu: Option<(u64, Option<Instant>)>,
577}
578
579/// The tooltip of the box under the pointer, the last frame it was built, and when it
580/// shows: never while None, after a press or the wheel, until the pointer leaves the box.
581#[derive(Clone, Copy)]
582struct Tip {
583 id: Id,
584 frame: u64,
585 due: Option<Instant>,
586}
587
588/// A popup painted: its anchor, the rectangle it opens beside, and its own.
589#[derive(Clone, Copy)]
590struct Placed {
591 id: Id,
592 anchor: Anchor,
593 around: [f32; 4],
594 rect: [f32; 4],
595}
596
597impl Placed {
598 /// How the popup shows `open` of the way open: where it `grows`, it swings out of its
599 /// anchor's edge as it grows and fades in, and a dialog swings up into place.
600 fn motion(self, open: f32, grows: bool) -> Motion {
601 let Placed {
602 anchor,
603 around,
604 rect,
605 ..
606 } = self;
607 let pivot = match anchor {
608 Anchor::Below(_) | Anchor::Point(_) => [around[0], around[3]],
609 Anchor::Tip(_) => [(around[0] + around[2]) / 2.0, around[3]],
610 Anchor::Right(_) => [around[2], around[1]],
611 Anchor::Over(_) => [around[0], around[1]],
612 Anchor::Dialog | Anchor::Top => [(rect[0] + rect[2]) / 2.0, rect[1]],
613 Anchor::Dock(_) => [rect[2], (rect[1] + rect[3]) / 2.0],
614 };
615 let (from, tilt) = match anchor {
616 _ if !grows => (1.0, 0.0),
617 Anchor::Dialog | Anchor::Top => (0.95, 0.2),
618 Anchor::Dock(_) => (0.95, 0.0),
619 // Over a box, the popup widens out of it in layout instead of growing.
620 Anchor::Over(_) => (1.0, 0.2),
621 _ => (0.94, 0.2),
622 };
623 let pivot = [
624 pivot[0].clamp(rect[0], rect[2]),
625 pivot[1].clamp(rect[1], rect[3]),
626 ];
627 // A popup flipped above its anchor swings out of its bottom edge, its top leaning away.
628 let tilt = if pivot[1] > (rect[1] + rect[3]) / 2.0 {
629 -tilt
630 } else {
631 tilt
632 };
633 Motion {
634 zoom: from + (1.0 - from) * open,
635 pivot,
636 tilt: tilt * (1.0 - open),
637 opacity: open,
638 }
639 }
640}
641
642/// A popup's painting as it last showed, fading out since it closed.
643struct Closing {
644 placed: Placed,
645 display: Vec<Display>,
646 closed: Instant,
647}
648
649struct Hit {
650 id: Id,
651 rect: [f32; 4],
652 flags: Flags,
653 cursor: Option<CursorIcon>,
654}
655
656#[derive(Clone)]
657enum Display {
658 Clip(Option<[f32; 4]>),
659 /// A rounded rectangle and its corner radius, clipping what follows.
660 Round(Option<([f32; 4], f32)>),
661 Motion(Option<Motion>),
662 Rect {
663 rect: [f32; 4],
664 fill: [f32; 4],
665 /// The fill's colour at the bottom.
666 shade: Option<[f32; 4]>,
667 border: Option<[f32; 4]>,
668 radius: f32,
669 },
670 /// A hairline 1 px wide.
671 Segment {
672 from: [f32; 2],
673 to: [f32; 2],
674 color: [f32; 4],
675 },
676 Image {
677 image: RasterImage,
678 rect: [f32; 4],
679 },
680 /// A shaped outline, filled or stroked.
681 Path {
682 data: String,
683 origin: [f32; 2],
684 style: PathStyle,
685 /// Top and bottom.
686 colors: [[f32; 4]; 2],
687 },
688 Shadow {
689 rect: [f32; 4],
690 radius: f32,
691 blur: f32,
692 color: [f32; 4],
693 },
694 Icon {
695 sources: &'static [&'static str],
696 origin: [f32; 2],
697 tint: [f32; 4],
698 },
699 Text {
700 label: Rc<Label>,
701 origin: [f32; 2],
702 clip: [f32; 4],
703 color: [f32; 4],
704 },
705 Custom {
706 id: Id,
707 rect: [f32; 4],
708 },
709}
710
711pub struct Ui {
712 pub theme: Theme,
713 /// The colours icons' slots take.
714 pub icon_palette: draw::Palette,
715 /// Whether the window has keyboard focus; without it a field hides its caret and dims
716 /// its selection.
717 pub window_focused: bool,
718 /// The platform's scrollers, where it draws fixed ones beside the content; scrollbars
719 /// overlay the content otherwise.
720 pub scrollers: Option<Scrollers>,
721 pub segments: Segments,
722 frame: u64,
723 now: Instant,
724 scale: f32,
725 nodes: Vec<Built>,
726 stack: Vec<usize>,
727 states: HashMap<Id, State>,
728 texts: Texts,
729 queue: Vec<Event>,
730 signals: HashMap<Id, Signal>,
731 /// The previous frame's interactive boxes in paint order.
732 hits: Vec<Hit>,
733 /// Hits before this one lie beneath an open popup.
734 modal: usize,
735 popups: Vec<Popup>,
736 pointer: Option<[f32; 2]>,
737 /// The pointer moved this frame.
738 moved: bool,
739 /// Seconds since the previous frame.
740 dt: f32,
741 lists: HashMap<Id, list::State>,
742 hover: Option<Id>,
743 active: Option<Id>,
744 focus: Option<Id>,
745 modifiers: ModifiersState,
746 clicks: Clicks,
747 display: Vec<Display>,
748 /// The painting of what holds still in the popup being painted, which goes over it.
749 still: Vec<Display>,
750 /// Where the popups' painting starts in `display`, for edges drawn beneath them.
751 popups_painted: usize,
752 /// Each popup painted, and its painting's place after `popups_painted`.
753 painted: Vec<(Placed, std::ops::Range<usize>)>,
754 closing: Vec<Closing>,
755 tip: Option<Tip>,
756 /// When a tooltip last showed.
757 warm: Option<Instant>,
758 animating: bool,
759 /// This frame routed input, whose effects the builder may only have seen after boxes
760 /// built before it read their state; one more frame shows them.
761 routed: bool,
762 /// The field showing a caret, when its blink started and the frame it last showed.
763 caret: Option<(Id, Instant, u64)>,
764 /// When the next timed change is due, such as a caret's blink.
765 wake: Option<Instant>,
766 /// The controls the keyboard moves among, in the latest layout's order.
767 stops: Vec<access::Stop>,
768 /// The keyboard last moved the focus, so the focused control shows a ring.
769 focus_ring: bool,
770 /// Where Escape returns the focus from the controls the keyboard moved it among.
771 resume: Option<Id>,
772 /// The nodes last sent to assistive technology.
773 sent: HashMap<Id, accesskit::Node>,
774 /// Scrolling boxes to scroll to a box within each, once laid out.
775 scrolls: Vec<(Id, Id)>,
776}
777
778impl Ui {
779 /// `double_click` is the platform's double-click interval.
780 pub fn new(theme: Theme, double_click: Duration) -> Self {
781 Self {
782 theme,
783 icon_palette: draw::Palette::default(),
784 window_focused: true,
785 scrollers: None,
786 segments: Segments::default(),
787 frame: 0,
788 now: Instant::now(),
789 scale: 1.0,
790 nodes: Vec::new(),
791 stack: Vec::new(),
792 states: HashMap::new(),
793 texts: Texts::default(),
794 queue: Vec::new(),
795 signals: HashMap::new(),
796 hits: Vec::new(),
797 modal: 0,
798 popups: Vec::new(),
799 pointer: None,
800 moved: false,
801 dt: 0.0,
802 lists: HashMap::new(),
803 hover: None,
804 active: None,
805 focus: None,
806 modifiers: ModifiersState::empty(),
807 clicks: Clicks::new(double_click),
808 display: Vec::new(),
809 still: Vec::new(),
810 popups_painted: 0,
811 painted: Vec::new(),
812 closing: Vec::new(),
813 tip: None,
814 warm: None,
815 animating: false,
816 routed: false,
817 caret: None,
818 wake: None,
819 stops: Vec::new(),
820 focus_ring: false,
821 resume: None,
822 sent: HashMap::new(),
823 scrolls: Vec::new(),
824 }
825 }
826
827 /// Queues input for the next frame.
828 pub fn event(&mut self, event: Event) {
829 // A chord the host looks up before the next frame reads the modifiers held now.
830 if let Event::Modifiers(modifiers) = event {
831 self.modifiers = modifiers;
832 }
833 self.queue.push(event);
834 }
835
836 /// Whether queued input or animation needs another frame.
837 pub fn wants_frame(&self) -> bool {
838 self.animating || self.routed || !self.queue.is_empty()
839 }
840
841 /// When a timed change, such as a caret's blink, next needs a frame.
842 pub fn wake_at(&self) -> Option<Instant> {
843 self.wake
844 }
845
846 /// Asks for a frame `after` this one, as a running clock does.
847 pub fn wake_after(&mut self, after: Duration) {
848 self.wake_by(self.now + after);
849 }
850
851 fn wake_by(&mut self, due: Instant) {
852 self.wake = Some(self.wake.map_or(due, |wake| wake.min(due)));
853 }
854
855 pub fn scale(&self) -> f32 {
856 self.scale
857 }
858
859 pub fn modifiers(&self) -> ModifiersState {
860 self.modifiers
861 }
862
863 /// Where the pointer is, in logical pixels, while it is over the window.
864 pub fn pointer(&self) -> Option<[f32; 2]> {
865 self.pointer
866 }
867
868 pub fn focused(&self) -> Option<Id> {
869 self.focus
870 }
871
872 /// The focused text field, or the field `popup` will return focus to when it closes.
873 pub fn focused_field(&self, popup: Option<Id>) -> Option<Id> {
874 let focus = popup
875 .and_then(|id| self.popups.iter().find(|popup| popup.id == id))
876 .map_or(self.focus, |popup| popup.focus);
877 focus.filter(|focus| {
878 let flags = self.hit_flags(*focus);
879 flags.contains(Flags::FOCUSABLE) && !flags.contains(Flags::CUSTOM)
880 })
881 }
882
883 pub fn set_focus(&mut self, id: Option<Id>) {
884 self.focus = id;
885 }
886
887 /// Focuses text field `id` with all its text selected once it is built, as a field
888 /// opening to rename something does.
889 pub fn focus_all(&mut self, id: Id) {
890 let state = self.states.entry(id).or_default();
891 state.touched = self.frame;
892 state.select = Some([0, usize::MAX]);
893 self.focus = Some(id);
894 }
895
896 /// The clickable or custom box at `point` in the latest layout.
897 pub fn box_at(&self, point: [f32; 2]) -> Option<Id> {
898 self.hit(point, Flags::CLICKABLE | Flags::CUSTOM)
899 }
900
901 /// The least width the box's children fit in, with its padding, as built this frame or
902 /// last: each row's groups folded, and each box yielding all its strictness lets it.
903 pub fn narrowest(&self, id: Id) -> Option<f32> {
904 let index = self.nodes.iter().position(|node| node.id == id)?;
905 Some(layout::narrowest(&self.nodes, index))
906 }
907
908 /// Where box `id` lies in the latest layout: this frame's once `end` has run, otherwise
909 /// the previous frame's, which input is routed against; none for one not laid out yet.
910 /// For hosts painting, hit-testing or mapping input after layout: a box placed beside
911 /// another takes an `Anchor`, which layout resolves in the same frame.
912 pub fn laid_out(&self, id: Id) -> Option<[f32; 4]> {
913 self.states.get(&id).and_then(|state| state.rect)
914 }
915
916 /// The window's size in logical pixels this frame.
917 pub fn size(&self) -> [f32; 2] {
918 let root = self.nodes.first().map(|root| root.size);
919 root.unwrap_or_default().map(|extent| match extent.size {
920 Size::Pixels(pixels) => pixels,
921 _ => 0.0,
922 })
923 }
924
925 /// Scrolls `scroller`, a scrolling box, until `target` within it starts at its top, as
926 /// this frame lays them out, easing there.
927 pub fn scroll_to(&mut self, scroller: Id, target: Id) {
928 self.scrolls.push((scroller, target));
929 }
930
931 /// The pointer's cursor, or None over a custom box, whose host chooses.
932 pub fn cursor(&self) -> Option<CursorIcon> {
933 let hit = self
934 .hover
935 .and_then(|id| self.hits.iter().find(|hit| hit.id == id));
936 match hit {
937 Some(hit) if hit.flags.contains(Flags::CUSTOM) => None,
938 Some(hit) => Some(hit.cursor.unwrap_or_default()),
939 None => Some(CursorIcon::Default),
940 }
941 }
942
943 /// Starts a frame of `size` logical pixels drawn at `scale` device pixels each, and
944 /// routes the queued input through the previous frame's layout.
945 pub fn begin(&mut self, size: [f32; 2], scale: f32, now: Instant) {
946 self.frame += 1;
947 let dt = now
948 .saturating_duration_since(self.now)
949 .as_secs_f32()
950 .min(0.1);
951 self.now = now;
952 self.scale = scale;
953 self.nodes.clear();
954 self.nodes.push(Built::new(
955 Id::ROOT,
956 0,
957 Spec {
958 size: [px(size[0]), px(size[1])],
959 axis: Axis::Y,
960 ..Spec::default()
961 },
962 None,
963 self.theme.text,
964 ));
965 self.stack.clear();
966 self.stack.push(0);
967 self.signals.clear();
968 self.moved = false;
969 self.wake = None;
970 self.routed = !self.queue.is_empty();
971 for event in std::mem::take(&mut self.queue) {
972 self.route(event);
973 }
974 // Boxes that move under a still pointer, as reordered tabs or a scrolled list, take
975 // the hover from it.
976 if !self.moved
977 && self.active.is_none()
978 && let Some(point) = self.pointer
979 {
980 let hover = self.hit(point, Flags::CLICKABLE | Flags::CUSTOM);
981 if hover != self.hover {
982 self.custom_event(self.hover, Event::PointerLeft);
983 self.hover = hover;
984 }
985 }
986 self.ease(dt);
987 }
988
989 fn route(&mut self, event: Event) {
990 if matches!(event, Event::Button { pressed: true, .. } | Event::Wheel(_)) {
991 if let Some(tip) = &mut self.tip {
992 tip.due = None;
993 }
994 self.warm = None;
995 }
996 match event {
997 Event::PointerMoved(point) => {
998 self.pointer = Some(point);
999 self.moved = true;
1000 let hover = self.hit(point, Flags::CLICKABLE | Flags::CUSTOM);
1001 if hover != self.hover
1002 && self.active.is_none_or(|active| Some(active) != self.hover)
1003 {
1004 self.custom_event(self.hover, Event::PointerLeft);
1005 }
1006 self.hover = hover;
1007 self.custom_event(self.active.or(hover), event);
1008 }
1009 Event::Pressure(_) => self.custom_event(self.active.or(self.hover), event),
1010 Event::PointerLeft => {
1011 self.pointer = None;
1012 if self.active.is_none() {
1013 self.custom_event(self.hover, Event::PointerLeft);
1014 self.hover = None;
1015 }
1016 }
1017 Event::Button {
1018 button,
1019 pressed: true,
1020 at,
1021 } => {
1022 let Some(point) = self.pointer else {
1023 return;
1024 };
1025 self.focus_ring = false;
1026 if !self.popups.is_empty() {
1027 let under = self.popups.iter().rposition(|popup| {
1028 self.laid_out(popup.id)
1029 .is_some_and(|rect| contains(rect, point))
1030 });
1031 // A press outside every popup only dismisses them.
1032 let Some(under) = under else {
1033 self.close_from(0);
1034 return;
1035 };
1036 self.close_from(under + 1);
1037 }
1038 let target = self.hit(point, Flags::CLICKABLE | Flags::CUSTOM);
1039 if let Some(id) = target
1040 && button == MouseButton::Right
1041 {
1042 self.signals.entry(id).or_default().context = Some(point);
1043 }
1044 if let Some(id) = target
1045 && button == MouseButton::Left
1046 {
1047 self.active = Some(id);
1048 let signal = self.signals.entry(id).or_default();
1049 signal.pressed = true;
1050 signal.unit = self.clicks.press(at, point, 4.0);
1051 if self.hit_flags(id).contains(Flags::FOCUSABLE) {
1052 self.focus = Some(id);
1053 }
1054 }
1055 self.custom_event(target, event);
1056 }
1057 Event::Button { pressed: false, .. } => {
1058 let Some(active) = self.active else {
1059 self.custom_event(self.hover, event);
1060 return;
1061 };
1062 if self
1063 .pointer
1064 .and_then(|point| self.hit(point, Flags::CLICKABLE | Flags::CUSTOM))
1065 == Some(active)
1066 {
1067 self.signals.entry(active).or_default().clicked = true;
1068 }
1069 self.custom_event(Some(active), event);
1070 self.active = None;
1071 self.hover = self
1072 .pointer
1073 .and_then(|point| self.hit(point, Flags::CLICKABLE | Flags::CUSTOM));
1074 if self.hover != Some(active) {
1075 self.custom_event(Some(active), Event::PointerLeft);
1076 }
1077 }
1078 Event::Wheel(delta) => {
1079 let Some(point) = self.pointer else {
1080 return;
1081 };
1082 let target = self.hit(point, Flags::SCROLL | Flags::CUSTOM);
1083 match target {
1084 Some(id) if self.hit_flags(id).contains(Flags::CUSTOM) => {
1085 self.custom_event(Some(id), event)
1086 }
1087 Some(id) => {
1088 let state = self.states.entry(id).or_default();
1089 let shown = state.rect.map_or(0.0, |rect| rect[3] - rect[1]);
1090 let most = (state.content - shown).max(0.0);
1091 // The wheel moves the content at once, as on the page; only jumps ease.
1092 state.scroll = (state.scroll - delta[1]).clamp(0.0, most);
1093 state.scroll_target = state.scroll;
1094 self.signals.entry(id).or_default().events.push(event);
1095 }
1096 None => {}
1097 }
1098 }
1099 Event::Key {
1100 key: Key::Named(NamedKey::Escape),
1101 ..
1102 } if !self.popups.is_empty() => self.close_from(self.popups.len() - 1),
1103 Event::Key {
1104 key: Key::Named(key),
1105 ..
1106 } if self.traverse(key) => {}
1107 Event::Access(request) => self.act(request),
1108 Event::Key { .. } | Event::Ime(_) => {
1109 if let Some(focus) = self.focus {
1110 self.signals.entry(focus).or_default().events.push(event);
1111 }
1112 }
1113 Event::Modifiers(modifiers) => {
1114 self.modifiers = modifiers;
1115 let custom: Vec<_> = self
1116 .hits
1117 .iter()
1118 .filter(|hit| hit.flags.contains(Flags::CUSTOM))
1119 .map(|hit| hit.id)
1120 .collect();
1121 for id in custom {
1122 self.signals
1123 .entry(id)
1124 .or_default()
1125 .events
1126 .push(event.clone());
1127 }
1128 }
1129 }
1130 }
1131
1132 fn custom_event(&mut self, target: Option<Id>, event: Event) {
1133 if let Some(id) = target
1134 && self.hit_flags(id).contains(Flags::CUSTOM)
1135 {
1136 self.signals.entry(id).or_default().events.push(event);
1137 }
1138 }
1139
1140 fn hit_flags(&self, id: Id) -> Flags {
1141 self.hits
1142 .iter()
1143 .find(|hit| hit.id == id)
1144 .map_or(Flags::default(), |hit| hit.flags)
1145 }
1146
1147 /// The topmost box under `point` with any of `flags`.
1148 fn hit(&self, point: [f32; 2], flags: Flags) -> Option<Id> {
1149 self.hits[self.modal..]
1150 .iter()
1151 .rev()
1152 .find(|hit| hit.flags.intersects(flags) && contains(hit.rect, point))
1153 .map(|hit| hit.id)
1154 }
1155
1156 /// Opens popup `id`, which shows while built with its `Spec::anchor` each frame, and
1157 /// takes the keyboard. Popups open from within another stay above it; any other
1158 /// closes those open.
1159 pub fn open_popup(&mut self, id: Id) {
1160 if self.popup_open(id) {
1161 return;
1162 }
1163 let within = self
1164 .stack
1165 .iter()
1166 .filter_map(|index| {
1167 let id = self.nodes[*index].id;
1168 self.popups.iter().position(|popup| popup.id == id)
1169 })
1170 .max();
1171 self.push_popup(id, within.map_or(0, |within| within + 1), None);
1172 }
1173
1174 /// Opens popup `id` as the submenu of open popup `from`, beside its row `row`, in place of
1175 /// any other opened from it.
1176 pub fn open_submenu(&mut self, id: Id, from: Id, row: Id) {
1177 let Some(from) = self.popups.iter().position(|popup| popup.id == from) else {
1178 return;
1179 };
1180 if self.popups.get(from + 1).is_none_or(|open| open.id != id) {
1181 self.push_popup(id, from + 1, Some(row));
1182 }
1183 }
1184
1185 /// Closes the popups from `at` on and opens `id` above the rest.
1186 fn push_popup(&mut self, id: Id, at: usize, beside: Option<Id>) {
1187 self.close_from(at);
1188 self.popups.push(Popup {
1189 id,
1190 focus: self.focus,
1191 query: String::new(),
1192 highlight: None,
1193 picked: None,
1194 opened: self.now,
1195 beside,
1196 submenu: None,
1197 });
1198 self.focus = Some(id);
1199 self.states.entry(id).or_default().touched = self.frame;
1200 }
1201
1202 pub fn popup_open(&self, id: Id) -> bool {
1203 self.popups.iter().any(|popup| popup.id == id)
1204 }
1205
1206 /// Whether any menu, dialog or other popup is open.
1207 pub fn popups_open(&self) -> bool {
1208 !self.popups.is_empty()
1209 }
1210
1211 /// Closes popup `id` and those opened from it, returning the focus it took; a submenu
1212 /// closes the menus it opened from with it, as choosing from it ends them.
1213 pub fn close_popup(&mut self, id: Id) {
1214 if let Some(mut index) = self.popups.iter().position(|popup| popup.id == id) {
1215 while index > 0 && self.popups[index].beside.is_some() {
1216 index -= 1;
1217 }
1218 self.close_from(index);
1219 }
1220 }
1221
1222 fn close_from(&mut self, index: usize) {
1223 if let Some(popup) = self.popups.get(index) {
1224 self.focus = popup.focus;
1225 self.popups.truncate(index);
1226 }
1227 }
1228
1229 fn ease(&mut self, dt: f32) {
1230 self.dt = dt;
1231 let rate = 1.0 - 0.5_f32.powf(dt / HALF_LIFE);
1232 let mut animating = false;
1233 for (id, state) in &mut self.states {
1234 let hot = self.hover == Some(*id) && self.active.is_none_or(|active| active == *id);
1235 if !state.hovers {
1236 state.hot = f32::from(u8::from(hot));
1237 }
1238 let targets = [
1239 (&mut state.hot, f32::from(u8::from(hot)), 0.002),
1240 (&mut state.scroll, state.scroll_target, 0.25),
1241 ];
1242 let tween = state
1243 .tween
1244 .as_mut()
1245 .map(|[value, target]| (value, *target, 0.005));
1246 for (value, target, close) in targets.into_iter().chain(tween) {
1247 let delta = target - *value;
1248 if delta.abs() < close {
1249 *value = target;
1250 } else {
1251 *value += delta * rate;
1252 animating = true;
1253 }
1254 }
1255 }
1256 self.animating = animating;
1257 }
1258
1259 /// The box being built, which new boxes become children of.
1260 pub fn current(&self) -> Id {
1261 self.nodes[*self.stack.last().unwrap()].id
1262 }
1263
1264 /// The id a box built next under the current parent would take.
1265 pub fn id(&self, part: impl Hash) -> Id {
1266 self.current().child(part)
1267 }
1268
1269 /// Adds a box and makes it the parent of the boxes built until `close`.
1270 pub fn open(&mut self, part: impl Hash, spec: Spec<'_>) -> Id {
1271 self.open_as(self.id(part), spec)
1272 }
1273
1274 /// Opens a box under an id chosen outside the hierarchy, so the host can name it
1275 /// wherever it is built.
1276 pub fn open_as(&mut self, id: Id, spec: Spec<'_>) -> Id {
1277 let label = spec.text.map(|text| {
1278 let size = spec.font_size.unwrap_or(self.theme.font_size);
1279 self.texts
1280 .styled(text, size, spec.bold, spec.italic, spec.font, self.frame)
1281 });
1282 // Popups hang from the root, outside the clips and flow of where they are built.
1283 let parent = if spec.anchor.is_some() {
1284 0
1285 } else {
1286 *self.stack.last().unwrap()
1287 };
1288 let open = match spec.anchor {
1289 Some(anchor @ Anchor::Over(_)) if self.popup_motion(anchor) == PopupMotion::Grow => {
1290 self.opening(id, anchor).unwrap_or(1.0)
1291 }
1292 _ => 1.0,
1293 };
1294 let index = self.nodes.len();
1295 self.states.entry(id).or_default().touched = self.frame;
1296 self.nodes.push(Built {
1297 open,
1298 ..Built::new(id, parent, spec, label, self.theme.text)
1299 });
1300 self.nodes[parent].children.push(index);
1301 self.stack.push(index);
1302 id
1303 }
1304
1305 pub fn close(&mut self) {
1306 debug_assert!(self.stack.len() > 1, "close without open");
1307 self.stack.pop();
1308 }
1309
1310 /// Adds a box without children and reports how it was used.
1311 pub fn leaf(&mut self, part: impl Hash, spec: Spec<'_>) -> Signal {
1312 let id = self.open(part, spec);
1313 self.close();
1314 self.signal(id)
1315 }
1316
1317 /// Holds animated value `id` at `value` without easing, as a dragged box follows the
1318 /// pointer; it eases from there once animated again.
1319 pub fn hold(&mut self, id: Id, value: f32) -> f32 {
1320 let state = self.states.entry(id).or_default();
1321 state.touched = self.frame;
1322 state.tween = Some([value; 2]);
1323 value
1324 }
1325
1326 /// Eases towards `target` over the next frames, starting there the first time `id` asks.
1327 pub fn animate(&mut self, id: Id, target: f32) -> f32 {
1328 let state = self.states.entry(id).or_default();
1329 state.touched = self.frame;
1330 let [value, goal] = state.tween.get_or_insert([target; 2]);
1331 *goal = target;
1332 self.animating |= value != goal;
1333 *value
1334 }
1335
1336 /// How long `held` has stayed true through the frames that ask of `id`; zero while
1337 /// false, as a status waits before showing a state that may pass at once.
1338 pub fn lasted(&mut self, id: Id, held: bool) -> Duration {
1339 let now = self.now;
1340 let state = self.states.entry(id).or_default();
1341 state.touched = self.frame;
1342 state.since = held.then(|| state.since.unwrap_or(now));
1343 state
1344 .since
1345 .map_or(Duration::ZERO, |since| now.saturating_duration_since(since))
1346 }
1347
1348 /// Makes `family` the interface's font, where fontique doesn't know the system's, as
1349 /// with Mac OS X 10.6's Lucida Grande.
1350 pub fn set_system_font(&mut self, family: &str) {
1351 self.texts.set_system_font(family);
1352 }
1353
1354 /// Previews `family` in the font `data` holds, as a font menu shows the substitute a
1355 /// missing family lays out in.
1356 pub fn preview_font(&mut self, data: parley::fontique::Blob<u8>, family: &str) {
1357 self.texts.preview_font(data, family);
1358 }
1359
1360 /// Draws what the interface's fonts and their scripts' fallbacks lack with the first of
1361 /// `families` that has it; see `draw::fall_back_to`.
1362 pub fn fall_back_to(&mut self, families: &[String]) {
1363 self.texts.fall_back_to(families);
1364 }
1365
1366 /// Whether the interface's fonts draw every glyph of `text`.
1367 pub fn shows(&mut self, text: &str) -> bool {
1368 self.texts.shows(text, self.frame)
1369 }
1370
1371 /// The size of `text` as a label, in logical pixels.
1372 pub fn measure(&mut self, text: &str) -> [f32; 2] {
1373 self.texts
1374 .label(text, self.theme.font_size, self.frame)
1375 .size
1376 }
1377
1378 /// Paints `color` over the current box at `rect`, relative to its corner, with corners
1379 /// of `radius`.
1380 pub fn mark(&mut self, rect: [f32; 4], color: [f32; 4], radius: f32) {
1381 let index = *self.stack.last().unwrap();
1382 self.nodes[index].marks.push((rect, color, radius));
1383 }
1384
1385 /// The caret opacity of field `id` this frame, restarting its blink when `moved` or
1386 /// when it was not shown the frame before.
1387 pub(crate) fn blink(&mut self, id: Id, moved: bool) -> f32 {
1388 let start = match self.caret {
1389 Some((shown, start, frame)) if shown == id && frame + 1 == self.frame && !moved => {
1390 start
1391 }
1392 _ => self.now,
1393 };
1394 self.caret = Some((id, start, self.frame));
1395 let (opacity, hold) = draw::edit::caret_blink(self.now.saturating_duration_since(start));
1396 if let Some(hold) = hold {
1397 self.wake_after(hold);
1398 }
1399 opacity
1400 }
1401
1402 /// How the user acted on the box this frame; a box's routed events are taken once.
1403 pub fn signal(&mut self, id: Id) -> Signal {
1404 let mut signal = self.signals.remove(&id).unwrap_or_default();
1405 signal.hovered = self.hover == Some(id);
1406 signal.dragging = self.active == Some(id);
1407 signal.focused = self.focus == Some(id);
1408 signal
1409 }
1410
1411 /// Solves this frame's layout and prepares its painting.
1412 pub fn end(&mut self) {
1413 debug_assert_eq!(self.stack.len(), 1, "open without close");
1414 layout::solve(
1415 &mut self.nodes,
1416 &self.states,
1417 self.scale,
1418 &mut self.texts,
1419 self.frame,
1420 );
1421 for node in &self.nodes {
1422 let state = self.states.entry(node.id).or_default();
1423 state.rect = Some(node.rect);
1424 state.hovers = node.hover_fill.is_some() || node.hover_border.is_some();
1425 if node.flags.contains(Flags::SCROLL) {
1426 state.content = node.content;
1427 let most = (node.content - (node.rect[3] - node.rect[1])).max(0.0);
1428 state.scroll_target = state.scroll_target.clamp(0.0, most);
1429 state.scroll = state.scroll.clamp(0.0, most);
1430 }
1431 }
1432 for (scroller, target) in std::mem::take(&mut self.scrolls) {
1433 let (Some(top), Some(start)) = (self.laid_out(target), self.laid_out(scroller)) else {
1434 continue;
1435 };
1436 let state = self.states.entry(scroller).or_default();
1437 let most = (state.content - (start[3] - start[1])).max(0.0);
1438 state.scroll_target = (state.scroll + top[1] - start[1]).clamp(0.0, most);
1439 self.animating |= state.scroll != state.scroll_target;
1440 }
1441 self.states
1442 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);
1443 self.texts.prune(self.frame);
1444 self.lists.retain(|id, _| self.states.contains_key(id));
1445 if let Some(gone) = self
1446 .popups
1447 .iter()
1448 .position(|popup| !self.states.contains_key(&popup.id))
1449 {
1450 self.close_from(gone);
1451 }
1452 if let Some(tip) = self.tip.filter(|tip| tip.frame != self.frame) {
1453 if tip.due.is_some_and(|due| due <= self.now) {
1454 self.warm = Some(self.now);
1455 }
1456 self.tip = None;
1457 }
1458 let tip = self.tip.map(|tip| tip.id.child("tooltip"));
1459 let last = std::mem::take(&mut self.display);
1460 let last_popups = self.popups_painted;
1461 self.hits.clear();
1462 self.paint(0, None, None);
1463 self.popups_painted = self.display.len();
1464 let beneath = self.hits.len();
1465 let mut painted = Vec::new();
1466 for index in self.nodes[0].children.clone() {
1467 let node = &self.nodes[index];
1468 let Some(anchor) = node.anchor else {
1469 continue;
1470 };
1471 let placed = Placed {
1472 id: node.id,
1473 anchor,
1474 around: node.around,
1475 rect: node.rect,
1476 };
1477 if Some(placed.id) == tip {
1478 continue;
1479 }
1480 if let Some(open) = self.opening(placed.id, anchor) {
1481 self.scrim(anchor, open);
1482 let grows = self.popup_motion(anchor) == PopupMotion::Grow;
1483 self.nodes[index].motion = Some(placed.motion(open, grows));
1484 }
1485 let from = self.display.len() - self.popups_painted;
1486 self.paint(index, None, None);
1487 if !self.still.is_empty() {
1488 let still = std::mem::take(&mut self.still);
1489 self.display.extend(still);
1490 self.display.extend([
1491 Display::Motion(None),
1492 Display::Clip(None),
1493 Display::Round(None),
1494 ]);
1495 }
1496 painted.push((placed, from..self.display.len() - self.popups_painted));
1497 }
1498 for (placed, range) in std::mem::replace(&mut self.painted, painted) {
1499 if self.painted.iter().any(|(shown, _)| shown.id == placed.id) {
1500 continue;
1501 }
1502 // What held still while it opened goes at once, uncovering what it stood in for.
1503 let mut still = false;
1504 let display = last[last_popups + range.start..last_popups + range.end]
1505 .iter()
1506 .filter(|item| match item {
1507 Display::Motion(motion) => {
1508 still = motion.is_none();
1509 false
1510 }
1511 _ => !still,
1512 })
1513 .cloned()
1514 .collect();
1515 self.closing.push(Closing {
1516 placed,
1517 display,
1518 closed: self.now,
1519 });
1520 }
1521 for closing in std::mem::take(&mut self.closing) {
1522 let Placed { id, anchor, .. } = closing.placed;
1523 let motion = self.popup_motion(anchor);
1524 let open = match motion {
1525 PopupMotion::Grow => {
1526 (1.0 - self.progress(closing.closed, anchor.durations()[1])).powi(3)
1527 }
1528 PopupMotion::Cut => 0.0,
1529 PopupMotion::Fade([_, close]) => {
1530 (1.0 - self.progress(closing.closed, close)).powi(4)
1531 }
1532 };
1533 if open == 0.0 {
1534 continue;
1535 }
1536 self.scrim(anchor, open);
1537 let grows = motion == PopupMotion::Grow;
1538 let motion = closing.placed.motion(open, grows);
1539 self.display.push(Display::Motion(Some(motion)));
1540 self.display.extend(closing.display.iter().cloned());
1541 self.display.push(Display::Motion(None));
1542 if !self.painted.iter().any(|(shown, _)| shown.id == id) {
1543 self.closing.push(closing);
1544 }
1545 }
1546 // Over everything, closing popups included; it goes at once, without closing.
1547 if let Some(index) = self.nodes[0]
1548 .children
1549 .iter()
1550 .copied()
1551 .find(|index| Some(self.nodes[*index].id) == tip)
1552 && let Some(due) = self.tip.and_then(|tip| tip.due)
1553 {
1554 let opacity = self.progress(due, TIP_FADE);
1555 self.nodes[index].motion = Some(Motion {
1556 opacity,
1557 ..Motion::NONE
1558 });
1559 self.paint(index, None, None);
1560 }
1561 self.modal = if self.popups.is_empty() { 0 } else { beneath };
1562 for id in [
1563 &mut self.hover,
1564 &mut self.active,
1565 &mut self.focus,
1566 &mut self.resume,
1567 ] {
1568 if id.is_some_and(|id| !self.states.contains_key(&id)) {
1569 *id = None;
1570 }
1571 }
1572 self.stops = self.stops();
1573 }
1574
1575 /// How far open popup `id` beside `anchor` shows this frame, from 0 to 1, while open.
1576 pub fn opening(&mut self, id: Id, anchor: Anchor) -> Option<f32> {
1577 let opened = self.popups.iter().find(|popup| popup.id == id)?.opened;
1578 Some(match self.popup_motion(anchor) {
1579 PopupMotion::Grow => 1.0 - (1.0 - self.progress(opened, anchor.durations()[0])).powi(3),
1580 PopupMotion::Cut => 1.0,
1581 PopupMotion::Fade([open, _]) => self.progress(opened, open),
1582 })
1583 }
1584
1585 /// How popups beside `anchor` open and close; dialogs and the palette always swing as
1586 /// Snowbound's own.
1587 fn popup_motion(&self, anchor: Anchor) -> PopupMotion {
1588 if matches!(anchor, Anchor::Dialog | Anchor::Top | Anchor::Dock(_)) {
1589 PopupMotion::Grow
1590 } else {
1591 self.theme.menu().motion
1592 }
1593 }
1594
1595 /// How far through an animation `duration` seconds long that began at `start` this
1596 /// frame is, asking for frames until it ends.
1597 fn progress(&mut self, start: Instant, duration: f32) -> f32 {
1598 let progress =
1599 (self.now.saturating_duration_since(start).as_secs_f32() / duration).min(1.0);
1600 self.animating |= progress < 1.0;
1601 progress
1602 }
1603
1604 /// Dims the window beneath a dialog `open` of the way open.
1605 fn scrim(&mut self, anchor: Anchor, open: f32) {
1606 if anchor == Anchor::Dialog {
1607 self.display.push(Display::Rect {
1608 rect: self.nodes[0].rect,
1609 fill: [0.0, 0.0, 0.0, self.theme.shadow[3] * open],
1610 shade: None,
1611 border: None,
1612 radius: 0.0,
1613 });
1614 }
1615 }
1616
1617 fn paint(&mut self, index: usize, clip: Option<[f32; 4]>, motion: Option<Motion>) {
1618 // Painted over the popup it holds still in, which appears as one picture beneath.
1619 if self.nodes[index].flags.contains(Flags::STILL) && motion.is_some() {
1620 let from = self.display.len();
1621 self.paint_box(index, clip, motion);
1622 let still = self.display.split_off(from);
1623 self.still.extend(still);
1624 } else {
1625 self.paint_box(index, clip, motion);
1626 }
1627 }
1628
1629 fn paint_box(&mut self, index: usize, clip: Option<[f32; 4]>, motion: Option<Motion>) {
1630 let node = &self.nodes[index];
1631 // The motion the box and its children paint with, where it changes it.
1632 let own = if node.flags.contains(Flags::STILL) {
1633 Some(None)
1634 } else {
1635 node.motion.map(Some)
1636 };
1637 if let Some(own) = own {
1638 self.display.push(Display::Motion(own));
1639 }
1640 let state = &self.states[&node.id];
1641 let rect = node.rect;
1642 let visible = clip.map_or(Some(rect), |clip| intersect(rect, clip));
1643 let blend = |base: Option<[f32; 4]>, hover: Option<[f32; 4]>| match (base, hover) {
1644 (base, Some(hover)) => Some(mix(base.unwrap_or_default(), hover, state.hot)),
1645 (base, None) => base,
1646 };
1647 let fill = blend(node.fill, node.hover_fill);
1648 // The gradient keeps its difference from the fill as hover moves it.
1649 let shade = node.gradient.map(|shade| {
1650 let base = node.fill.unwrap_or(shade);
1651 std::array::from_fn(|channel| {
1652 shade[channel] + fill.unwrap_or(base)[channel] - base[channel]
1653 })
1654 });
1655 let border = blend(node.border, node.hover_border);
1656 let [left, top, right, bottom] = node.inset;
1657 // A box squeezed smaller than its inset, or than nothing as it eases shut, paints
1658 // empty rather than inverted.
1659 let [x, y] = [rect[0] + left, rect[1] + top];
1660 let painted = [x, y, (rect[2] - right).max(x), (rect[3] - bottom).max(y)];
1661 let size = [painted[2] - painted[0], painted[3] - painted[1]];
1662 if let Some(color) = node.shadow {
1663 let (own, menu);
1664 let shadows: &[Shadow] = match node.anchor {
1665 Some(Anchor::Dialog | Anchor::Dock(_)) | None => {
1666 let [blur, drop] = if node.anchor.is_some() {
1667 POPUP_SHADOW
1668 } else {
1669 SHADOW
1670 };
1671 own = [Shadow {
1672 color,
1673 blur,
1674 drop,
1675 spread: 0.0,
1676 }];
1677 &own
1678 }
1679 Some(_) => {
1680 menu = self.theme.menu();
1681 &menu.shadows
1682 }
1683 };
1684 for &Shadow {
1685 color,
1686 blur,
1687 drop,
1688 spread,
1689 } in shadows
1690 {
1691 let origin = [painted[0] - spread, painted[1] + drop - spread];
1692 let grown = size.map(|side| side + 2.0 * spread);
1693 self.display.push(match node.shape {
1694 Shape::Rounded => Display::Shadow {
1695 rect: [
1696 origin[0],
1697 origin[1],
1698 origin[0] + grown[0],
1699 origin[1] + grown[1],
1700 ],
1701 radius: node.radius + spread,
1702 blur,
1703 color,
1704 },
1705 Shape::Tab { .. } => Display::Path {
1706 data: outline(node.shape, grown, node.radius + spread),
1707 origin,
1708 style: PathStyle::Shadow(blur),
1709 colors: [color; 2],
1710 },
1711 });
1712 }
1713 }
1714 if node.shape == Shape::Rounded {
1715 if fill.is_some() || border.is_some() {
1716 self.display.push(Display::Rect {
1717 rect: painted,
1718 fill: fill.unwrap_or([0.0; 4]),
1719 shade,
1720 border,
1721 radius: node.radius,
1722 });
1723 }
1724 } else {
1725 let data = outline(node.shape, size, node.radius);
1726 let paints = [
1727 fill.map(|fill| (PathStyle::Fill, [fill, shade.unwrap_or(fill)])),
1728 border.map(|border| (PathStyle::Stroke(1.0), [border; 2])),
1729 ];
1730 for (style, colors) in paints.into_iter().flatten() {
1731 self.display.push(Display::Path {
1732 data: data.clone(),
1733 origin: [painted[0], painted[1]],
1734 style,
1735 colors,
1736 });
1737 }
1738 }
1739 for (mark, color, radius) in &node.marks {
1740 self.display.push(Display::Rect {
1741 rect: [
1742 rect[0] + mark[0],
1743 rect[1] + mark[1],
1744 rect[0] + mark[2],
1745 rect[1] + mark[3],
1746 ],
1747 fill: *color,
1748 shade: None,
1749 border: None,
1750 radius: *radius,
1751 });
1752 }
1753 // A box squeezed narrower than its padding keeps an empty inside, not an inverted one.
1754 let left = (painted[0] + node.pad[0]).min(painted[2]);
1755 let inner = [
1756 left,
1757 painted[1],
1758 (painted[2] - node.pad[0]).max(left),
1759 painted[3],
1760 ];
1761 let mut x = if node.center {
1762 (inner[0] + inner[2] - node.content_width()) / 2.0
1763 } else {
1764 inner[0]
1765 };
1766 let top = (painted[1] + painted[3] - ICON) / 2.0;
1767 if let Some(sources) = node.icon {
1768 self.display.push(Display::Icon {
1769 sources,
1770 origin: [x, top],
1771 tint: node.tint,
1772 });
1773 x += ICON + ICON_GAP;
1774 } else if let Some(image) = &node.image {
1775 let rect = if node.label.is_some() {
1776 [x, top, x + ICON, top + ICON]
1777 } else {
1778 let top = (painted[1] + node.pad[1]).min(painted[3]);
1779 [inner[0], top, inner[2], (painted[3] - node.pad[1]).max(top)]
1780 };
1781 self.display.push(Display::Image {
1782 image: image.clone(),
1783 rect,
1784 });
1785 x += ICON + ICON_GAP;
1786 }
1787 if let Some(label) = &node.label {
1788 let y = (painted[1] + painted[3] - label.size[1]) / 2.0;
1789 self.display.push(Display::Text {
1790 label: label.clone(),
1791 origin: [x, y],
1792 clip: inner,
1793 color: node.color,
1794 });
1795 }
1796 if let Some(visible) = visible
1797 && node
1798 .flags
1799 .intersects(Flags::CLICKABLE | Flags::CUSTOM | Flags::SCROLL)
1800 {
1801 self.hits.push(Hit {
1802 id: node.id,
1803 rect: visible,
1804 flags: node.flags,
1805 cursor: node.cursor,
1806 });
1807 if node.flags.contains(Flags::CUSTOM) {
1808 self.display.push(Display::Custom {
1809 id: node.id,
1810 rect: visible,
1811 });
1812 }
1813 }
1814 // A popup's contents show only inside its rounded outline.
1815 let round = node.anchor.map(|_| (rect, node.radius));
1816 let inner_clip = if node.flags.contains(Flags::CLIP) || round.is_some() {
1817 Some(visible.unwrap_or([rect[0], rect[1], rect[0], rect[1]]))
1818 } else {
1819 clip
1820 };
1821 if inner_clip != clip {
1822 self.display.push(Display::Clip(inner_clip));
1823 }
1824 if round.is_some() {
1825 self.display.push(Display::Round(round));
1826 }
1827 for child in node.children.clone() {
1828 if self.nodes[child].anchor.is_none() && !self.nodes[child].hidden {
1829 self.paint(child, inner_clip, own.unwrap_or(motion));
1830 }
1831 }
1832 if round.is_some() {
1833 self.display.push(Display::Round(None));
1834 }
1835 self.fade(index);
1836 if inner_clip != clip {
1837 self.display.push(Display::Clip(clip));
1838 }
1839 let node = &self.nodes[index];
1840 if self.focus_ring
1841 && self.focus == Some(node.id)
1842 && node.access.is_some()
1843 && node.anchor.is_none()
1844 && !node.flags.contains(Flags::CUSTOM)
1845 {
1846 let size = size.map(|side| side + 2.0 * FOCUS_RING);
1847 self.display.push(Display::Path {
1848 data: outline(Shape::Rounded, size, node.radius + FOCUS_RING),
1849 origin: [painted[0] - FOCUS_RING, painted[1] - FOCUS_RING],
1850 style: PathStyle::Stroke(FOCUS_RING),
1851 colors: [self.theme.accent; 2],
1852 });
1853 }
1854 if own.is_some() {
1855 self.display.push(Display::Motion(motion));
1856 }
1857 }
1858
1859 /// Fades box `index`'s children out towards its ends along its axis by its `fade`, in
1860 /// steps of a point, into its fill however opaque that is: each step clears what lies
1861 /// beneath towards transparency and lays the fill over it.
1862 fn fade(&mut self, index: usize) {
1863 let node = &self.nodes[index];
1864 let along = usize::from(node.axis == Axis::Y);
1865 let rect = node.rect;
1866 let fill = node.fill.unwrap_or_default();
1867 let mut steps = Vec::new();
1868 for (side, width) in node.fade.into_iter().enumerate() {
1869 let count = width.round() as usize;
1870 for step in 0..count {
1871 // Through the step's middle, eased so the fade leaves the children softly.
1872 let alpha = 1.0 - (step as f32 + 0.5) / count as f32;
1873 let alpha = alpha * alpha * (3.0 - 2.0 * alpha);
1874 let at = if side == 0 {
1875 rect[along] + step as f32
1876 } else {
1877 rect[along + 2] - 1.0 - step as f32
1878 };
1879 let mut band = rect;
1880 [band[along], band[along + 2]] = [at, at + 1.0];
1881 steps.push((band, alpha));
1882 }
1883 }
1884 let [width, height] = [rect[2] - rect[0], rect[3] - rect[1]];
1885 let data = if along == 0 {
1886 format!("M0 0H1V{height}H0Z")
1887 } else {
1888 format!("M0 0H{width}V1H0Z")
1889 };
1890 for (rect, alpha) in steps {
1891 // Laid over what the erasing leaves, the fill's share makes up the rest of the
1892 // children's lost opacity: all of it where the fill is opaque, none where clear.
1893 let laid = alpha * fill[3];
1894 if laid < 1.0 {
1895 self.display.push(Display::Path {
1896 data: data.clone(),
1897 origin: [rect[0], rect[1]],
1898 style: PathStyle::Erase,
1899 colors: [[0.0, 0.0, 0.0, 1.0 - (1.0 - alpha) / (1.0 - laid)]; 2],
1900 });
1901 }
1902 self.display.push(Display::Rect {
1903 rect,
1904 fill: [fill[0], fill[1], fill[2], laid],
1905 shade: None,
1906 border: None,
1907 radius: 0.0,
1908 });
1909 }
1910 }
1911
1912 /// This frame's painting in order, in logical pixels.
1913 pub fn layers(&self) -> Vec<Layer<'_>> {
1914 let mut layers = Vec::new();
1915 let mut clip = None;
1916 let mut round = None;
1917 let mut motion = None;
1918 let mut primitives = Vec::new();
1919 fn flush<'a>(
1920 layers: &mut Vec<Layer<'a>>,
1921 clip: Option<[f32; 4]>,
1922 round: Option<([f32; 4], f32)>,
1923 motion: Option<Motion>,
1924 primitives: &mut Vec<Primitive<'a>>,
1925 ) {
1926 if !primitives.is_empty() {
1927 layers.push(Layer::Primitives(Primitives {
1928 clip,
1929 round,
1930 motion,
1931 primitives: std::mem::take(primitives),
1932 }));
1933 }
1934 }
1935 for item in &self.display {
1936 match item {
1937 Display::Clip(next) => {
1938 flush(&mut layers, clip, round, motion, &mut primitives);
1939 clip = *next;
1940 }
1941 Display::Round(next) => {
1942 flush(&mut layers, clip, round, motion, &mut primitives);
1943 round = *next;
1944 }
1945 Display::Motion(next) => {
1946 flush(&mut layers, clip, round, motion, &mut primitives);
1947 motion = *next;
1948 }
1949 Display::Rect {
1950 rect,
1951 fill,
1952 shade,
1953 border,
1954 radius,
1955 } => {
1956 if let Some(shade) = shade {
1957 primitives.push(Primitive::Gradient {
1958 rect: *rect,
1959 radius: [*radius; 2],
1960 colors: [*fill, *shade],
1961 });
1962 } else if fill[3] > 0.0 {
1963 primitives.push(if *radius > 0.0 {
1964 Primitive::RoundedRect {
1965 rect: *rect,
1966 radius: [*radius; 2],
1967 stroke: None,
1968 color: *fill,
1969 }
1970 } else {
1971 Primitive::Rect {
1972 rect: *rect,
1973 color: *fill,
1974 }
1975 });
1976 }
1977 if let Some(border) = border.filter(|border| border[3] > 0.0) {
1978 primitives.push(Primitive::RoundedRect {
1979 rect: *rect,
1980 radius: [*radius; 2],
1981 stroke: Some(Stroke::Solid(1.0 / self.scale)),
1982 color: border,
1983 });
1984 }
1985 }
1986 Display::Path {
1987 data,
1988 origin,
1989 style,
1990 colors,
1991 } => primitives.push(Primitive::Path {
1992 data,
1993 origin: *origin,
1994 style: *style,
1995 colors: *colors,
1996 }),
1997 &Display::Shadow {
1998 rect,
1999 radius,
2000 blur,
2001 color,
2002 } => primitives.push(Primitive::Shadow {
2003 rect,
2004 radius,
2005 blur,
2006 color,
2007 }),
2008 Display::Segment { from, to, color } => primitives.push(Primitive::Segment {
2009 from: *from,
2010 to: *to,
2011 width: 1.0,
2012 round: false,
2013 color: *color,
2014 }),
2015 Display::Image { image, rect } => {
2016 primitives.push(Primitive::Image { image, rect: *rect })
2017 }
2018 Display::Icon {
2019 sources,
2020 origin,
2021 tint,
2022 } => primitives.push(Primitive::Icon {
2023 sources,
2024 origin: *origin,
2025 size: ICON,
2026 tint: *tint,
2027 palette: self.icon_palette,
2028 }),
2029 Display::Text {
2030 label,
2031 origin,
2032 clip,
2033 color,
2034 } => primitives.push(Primitive::Text {
2035 text: &**label,
2036 origin: *origin,
2037 clip: Some(*clip),
2038 ink: *color,
2039 }),
2040 Display::Custom { id, rect } => {
2041 flush(&mut layers, clip, round, motion, &mut primitives);
2042 layers.push(Layer::Custom {
2043 id: *id,
2044 rect: *rect,
2045 });
2046 }
2047 }
2048 }
2049 flush(&mut layers, clip, round, motion, &mut primitives);
2050 layers
2051 }
2052
2053 pub(crate) fn state(&mut self, id: Id) -> &mut State {
2054 self.states.entry(id).or_default()
2055 }
2056}
2057
2058impl Built {
2059 /// The width of the icon and label together.
2060 fn content_width(&self) -> f32 {
2061 let label = self.label.as_ref().map_or(0.0, |label| label.size[0]);
2062 match (self.icon.is_some() || self.image.is_some(), label > 0.0) {
2063 (true, true) => ICON + ICON_GAP + label,
2064 (true, false) => ICON,
2065 (false, _) => label,
2066 }
2067 }
2068
2069 fn new(
2070 id: Id,
2071 parent: usize,
2072 spec: Spec<'_>,
2073 label: Option<Rc<Label>>,
2074 text: [f32; 4],
2075 ) -> Self {
2076 Self {
2077 id,
2078 parent,
2079 children: Vec::new(),
2080 flags: spec.flags,
2081 size: spec.size,
2082 axis: spec.axis,
2083 label,
2084 overflow: spec.overflow,
2085 color: spec.color.unwrap_or(text),
2086 fill: spec.fill,
2087 gradient: spec.gradient,
2088 shadow: spec.shadow,
2089 inset: spec.inset,
2090 hover_fill: spec.hover_fill,
2091 border: spec.border,
2092 hover_border: spec.hover_border,
2093 radius: spec.radius,
2094 shape: spec.shape,
2095 icon: spec.icon,
2096 tint: spec.tint.or(spec.color).unwrap_or(text),
2097 image: spec.image.cloned(),
2098 pad: spec.pad,
2099 gap: spec.gap,
2100 center: spec.center,
2101 position: spec.position,
2102 offset: spec.offset,
2103 cursor: spec.cursor,
2104 anchor: spec.anchor,
2105 around: [0.0; 4],
2106 open: 1.0,
2107 motion: None,
2108 marks: Vec::new(),
2109 fold: spec.fold,
2110 fade: spec.fade,
2111 access: spec.role.map(accesskit::Node::new),
2112 hidden: false,
2113 computed: [0.0; 2],
2114 relative: [0.0; 2],
2115 rect: [0.0; 4],
2116 content: 0.0,
2117 }
2118 }
2119}
2120
2121/// SVG path data outlining a box of `size` from its corner, rounding corners by `radius`.
2122/// A tab's outline stays open along its bottom, so a border leaves the edge it stands on.
2123fn outline(shape: Shape, [width, height]: [f32; 2], radius: f32) -> String {
2124 let corners = match shape {
2125 Shape::Tab { lean } => [
2126 ([0.0, height], 0.0),
2127 ([0.0, 0.0], radius),
2128 ([width - lean, 0.0], radius / 2.0),
2129 ([width - lean + height, height], 0.0),
2130 ],
2131 Shape::Rounded => [
2132 ([0.0, height], radius),
2133 ([0.0, 0.0], radius),
2134 ([width, 0.0], radius),
2135 ([width, height], radius),
2136 ],
2137 };
2138 let mut data = String::new();
2139 for index in 0..corners.len() {
2140 let corner = Corner::new(&corners, index);
2141 let verb = if index == 0 { 'M' } else { 'L' };
2142 data += &format!(
2143 "{verb}{} {}{}",
2144 corner.start[0],
2145 corner.start[1],
2146 corner.curve([0.0; 2])
2147 );
2148 }
2149 if !matches!(shape, Shape::Tab { .. }) {
2150 data.push('Z');
2151 }
2152 data
2153}
2154
2155/// A polygon's corner rounded by a quarter-circle's cubic, from where it leaves the edge
2156/// before to where it joins the edge after.
2157struct Corner {
2158 point: [f32; 2],
2159 start: [f32; 2],
2160 end: [f32; 2],
2161 controls: [[f32; 2]; 2],
2162 /// It turns clockwise, so it bulges out of a shape listed clockwise.
2163 outward: bool,
2164}
2165
2166impl Corner {
2167 /// The corner at `points[index]`, each point with its rounding radius.
2168 fn new(points: &[([f32; 2], f32)], index: usize) -> Self {
2169 let count = points.len();
2170 let (point, radius) = points[index];
2171 let [before, after] =
2172 [(index + count - 1) % count, (index + 1) % count].map(|index| points[index].0);
2173 let direction = |from: [f32; 2], to: [f32; 2]| {
2174 let length = (to[0] - from[0]).hypot(to[1] - from[1]).max(f32::EPSILON);
2175 [(to[0] - from[0]) / length, (to[1] - from[1]) / length]
2176 };
2177 let [incoming, outgoing] = [direction(before, point), direction(point, after)];
2178 let along = |from: [f32; 2], direction: [f32; 2], distance: f32| {
2179 [
2180 from[0] + direction[0] * distance,
2181 from[1] + direction[1] * distance,
2182 ]
2183 };
2184 let start = along(point, incoming, -radius);
2185 let end = along(point, outgoing, radius);
2186 // A quarter circle's cubic control distance.
2187 let handle = radius * 0.552_284_8;
2188 Self {
2189 point,
2190 start,
2191 end,
2192 controls: [
2193 along(start, incoming, handle),
2194 along(end, outgoing, -handle),
2195 ],
2196 outward: incoming[0] * outgoing[1] - incoming[1] * outgoing[0] > 0.0,
2197 }
2198 }
2199
2200 /// The cubic from `start` to `end`, as path data relative to `origin`.
2201 fn curve(&self, origin: [f32; 2]) -> String {
2202 let [a, b, c] = [self.controls[0], self.controls[1], self.end]
2203 .map(|point| [point[0] - origin[0], point[1] - origin[1]]);
2204 format!("C{} {} {} {} {} {}", a[0], a[1], b[0], b[1], c[0], c[1])
2205 }
2206}
2207
2208/// Where a box dragged from index `from` of a list laid out at `spans`, each a start and a
2209/// length along it, lands with its middle at `middle`: after every other box whose middle it
2210/// has passed, as browser tabs reorder. The index is in the list's new order.
2211pub fn drop_slot(spans: &[[f32; 2]], from: usize, middle: f32) -> usize {
2212 spans
2213 .iter()
2214 .enumerate()
2215 .filter(|(index, [start, length])| *index != from && start + length / 2.0 < middle)
2216 .count()
2217}
2218
2219/// How far box `index` of a list slides aside while the box at `from`, `length` long, is
2220/// dragged to land at `to`.
2221pub fn slide(index: usize, from: usize, to: usize, length: f32) -> f32 {
2222 if from < index && index <= to {
2223 -length
2224 } else if to <= index && index < from {
2225 length
2226 } else {
2227 0.0
2228 }
2229}
2230
2231/// `a` moved `t` of the way to `b`, mixed as premultiplied colours, so from transparent
2232/// `b` fades in with its own hue.
2233pub fn mix(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
2234 let alpha = a[3] + (b[3] - a[3]) * t;
2235 if alpha == 0.0 {
2236 return [0.0; 4];
2237 }
2238 let channel = |i: usize| (a[i] * a[3] + (b[i] * b[3] - a[i] * a[3]) * t) / alpha;
2239 [channel(0), channel(1), channel(2), alpha]
2240}
2241
2242fn contains(rect: [f32; 4], point: [f32; 2]) -> bool {
2243 point[0] >= rect[0] && point[0] < rect[2] && point[1] >= rect[1] && point[1] < rect[3]
2244}
2245
2246fn intersect(a: [f32; 4], b: [f32; 4]) -> Option<[f32; 4]> {
2247 let rect = [
2248 a[0].max(b[0]),
2249 a[1].max(b[1]),
2250 a[2].min(b[2]),
2251 a[3].min(b[3]),
2252 ];
2253 (rect[0] < rect[2] && rect[1] < rect[3]).then_some(rect)
2254}
2255
2256#[cfg(test)]
2257mod tests;