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 focus where it is a text field, which the keyboard's edits act on.
873 pub fn focused_field(&self) -> Option<Id> {
874 self.focus.filter(|focus| {
875 let flags = self.hit_flags(*focus);
876 flags.contains(Flags::FOCUSABLE) && !flags.contains(Flags::CUSTOM)
877 })
878 }
879
880 pub fn set_focus(&mut self, id: Option<Id>) {
881 self.focus = id;
882 }
883
884 /// Focuses text field `id` with all its text selected once it is built, as a field
885 /// opening to rename something does.
886 pub fn focus_all(&mut self, id: Id) {
887 let state = self.states.entry(id).or_default();
888 state.touched = self.frame;
889 state.select = Some([0, usize::MAX]);
890 self.focus = Some(id);
891 }
892
893 /// The clickable or custom box at `point` in the latest layout.
894 pub fn box_at(&self, point: [f32; 2]) -> Option<Id> {
895 self.hit(point, Flags::CLICKABLE | Flags::CUSTOM)
896 }
897
898 /// The least width the box's children fit in, with its padding, as built this frame or
899 /// last: each row's groups folded, and each box yielding all its strictness lets it.
900 pub fn narrowest(&self, id: Id) -> Option<f32> {
901 let index = self.nodes.iter().position(|node| node.id == id)?;
902 Some(layout::narrowest(&self.nodes, index))
903 }
904
905 /// Where box `id` lies in the latest layout: this frame's once `end` has run, otherwise
906 /// the previous frame's, which input is routed against; none for one not laid out yet.
907 /// For hosts painting, hit-testing or mapping input after layout: a box placed beside
908 /// another takes an `Anchor`, which layout resolves in the same frame.
909 pub fn laid_out(&self, id: Id) -> Option<[f32; 4]> {
910 self.states.get(&id).and_then(|state| state.rect)
911 }
912
913 /// The window's size in logical pixels this frame.
914 pub fn size(&self) -> [f32; 2] {
915 let root = self.nodes.first().map(|root| root.size);
916 root.unwrap_or_default().map(|extent| match extent.size {
917 Size::Pixels(pixels) => pixels,
918 _ => 0.0,
919 })
920 }
921
922 /// Scrolls `scroller`, a scrolling box, until `target` within it starts at its top, as
923 /// this frame lays them out, easing there.
924 pub fn scroll_to(&mut self, scroller: Id, target: Id) {
925 self.scrolls.push((scroller, target));
926 }
927
928 /// The pointer's cursor, or None over a custom box, whose host chooses.
929 pub fn cursor(&self) -> Option<CursorIcon> {
930 let hit = self
931 .hover
932 .and_then(|id| self.hits.iter().find(|hit| hit.id == id));
933 match hit {
934 Some(hit) if hit.flags.contains(Flags::CUSTOM) => None,
935 Some(hit) => Some(hit.cursor.unwrap_or_default()),
936 None => Some(CursorIcon::Default),
937 }
938 }
939
940 /// Starts a frame of `size` logical pixels drawn at `scale` device pixels each, and
941 /// routes the queued input through the previous frame's layout.
942 pub fn begin(&mut self, size: [f32; 2], scale: f32, now: Instant) {
943 self.frame += 1;
944 let dt = now
945 .saturating_duration_since(self.now)
946 .as_secs_f32()
947 .min(0.1);
948 self.now = now;
949 self.scale = scale;
950 self.nodes.clear();
951 self.nodes.push(Built::new(
952 Id::ROOT,
953 0,
954 Spec {
955 size: [px(size[0]), px(size[1])],
956 axis: Axis::Y,
957 ..Spec::default()
958 },
959 None,
960 self.theme.text,
961 ));
962 self.stack.clear();
963 self.stack.push(0);
964 self.signals.clear();
965 self.moved = false;
966 self.wake = None;
967 self.routed = !self.queue.is_empty();
968 for event in std::mem::take(&mut self.queue) {
969 self.route(event);
970 }
971 // Boxes that move under a still pointer, as reordered tabs or a scrolled list, take
972 // the hover from it.
973 if !self.moved
974 && self.active.is_none()
975 && let Some(point) = self.pointer
976 {
977 let hover = self.hit(point, Flags::CLICKABLE | Flags::CUSTOM);
978 if hover != self.hover {
979 self.custom_event(self.hover, Event::PointerLeft);
980 self.hover = hover;
981 }
982 }
983 self.ease(dt);
984 }
985
986 fn route(&mut self, event: Event) {
987 if matches!(event, Event::Button { pressed: true, .. } | Event::Wheel(_)) {
988 if let Some(tip) = &mut self.tip {
989 tip.due = None;
990 }
991 self.warm = None;
992 }
993 match event {
994 Event::PointerMoved(point) => {
995 self.pointer = Some(point);
996 self.moved = true;
997 let hover = self.hit(point, Flags::CLICKABLE | Flags::CUSTOM);
998 if hover != self.hover
999 && self.active.is_none_or(|active| Some(active) != self.hover)
1000 {
1001 self.custom_event(self.hover, Event::PointerLeft);
1002 }
1003 self.hover = hover;
1004 self.custom_event(self.active.or(hover), event);
1005 }
1006 Event::Pressure(_) => self.custom_event(self.active.or(self.hover), event),
1007 Event::PointerLeft => {
1008 self.pointer = None;
1009 if self.active.is_none() {
1010 self.custom_event(self.hover, Event::PointerLeft);
1011 self.hover = None;
1012 }
1013 }
1014 Event::Button {
1015 button,
1016 pressed: true,
1017 at,
1018 } => {
1019 let Some(point) = self.pointer else {
1020 return;
1021 };
1022 self.focus_ring = false;
1023 if !self.popups.is_empty() {
1024 let under = self.popups.iter().rposition(|popup| {
1025 self.laid_out(popup.id)
1026 .is_some_and(|rect| contains(rect, point))
1027 });
1028 // A press outside every popup only dismisses them.
1029 let Some(under) = under else {
1030 self.close_from(0);
1031 return;
1032 };
1033 self.close_from(under + 1);
1034 }
1035 let target = self.hit(point, Flags::CLICKABLE | Flags::CUSTOM);
1036 if let Some(id) = target
1037 && button == MouseButton::Right
1038 {
1039 self.signals.entry(id).or_default().context = Some(point);
1040 }
1041 if let Some(id) = target
1042 && button == MouseButton::Left
1043 {
1044 self.active = Some(id);
1045 let signal = self.signals.entry(id).or_default();
1046 signal.pressed = true;
1047 signal.unit = self.clicks.press(at, point, 4.0);
1048 if self.hit_flags(id).contains(Flags::FOCUSABLE) {
1049 self.focus = Some(id);
1050 }
1051 }
1052 self.custom_event(target, event);
1053 }
1054 Event::Button { pressed: false, .. } => {
1055 let Some(active) = self.active else {
1056 self.custom_event(self.hover, event);
1057 return;
1058 };
1059 if self
1060 .pointer
1061 .and_then(|point| self.hit(point, Flags::CLICKABLE | Flags::CUSTOM))
1062 == Some(active)
1063 {
1064 self.signals.entry(active).or_default().clicked = true;
1065 }
1066 self.custom_event(Some(active), event);
1067 self.active = None;
1068 self.hover = self
1069 .pointer
1070 .and_then(|point| self.hit(point, Flags::CLICKABLE | Flags::CUSTOM));
1071 if self.hover != Some(active) {
1072 self.custom_event(Some(active), Event::PointerLeft);
1073 }
1074 }
1075 Event::Wheel(delta) => {
1076 let Some(point) = self.pointer else {
1077 return;
1078 };
1079 let target = self.hit(point, Flags::SCROLL | Flags::CUSTOM);
1080 match target {
1081 Some(id) if self.hit_flags(id).contains(Flags::CUSTOM) => {
1082 self.custom_event(Some(id), event)
1083 }
1084 Some(id) => {
1085 let state = self.states.entry(id).or_default();
1086 let shown = state.rect.map_or(0.0, |rect| rect[3] - rect[1]);
1087 let most = (state.content - shown).max(0.0);
1088 // The wheel moves the content at once, as on the page; only jumps ease.
1089 state.scroll = (state.scroll - delta[1]).clamp(0.0, most);
1090 state.scroll_target = state.scroll;
1091 self.signals.entry(id).or_default().events.push(event);
1092 }
1093 None => {}
1094 }
1095 }
1096 Event::Key {
1097 key: Key::Named(NamedKey::Escape),
1098 ..
1099 } if !self.popups.is_empty() => self.close_from(self.popups.len() - 1),
1100 Event::Key {
1101 key: Key::Named(key),
1102 ..
1103 } if self.traverse(key) => {}
1104 Event::Access(request) => self.act(request),
1105 Event::Key { .. } | Event::Ime(_) => {
1106 if let Some(focus) = self.focus {
1107 self.signals.entry(focus).or_default().events.push(event);
1108 }
1109 }
1110 Event::Modifiers(modifiers) => {
1111 self.modifiers = modifiers;
1112 let custom: Vec<_> = self
1113 .hits
1114 .iter()
1115 .filter(|hit| hit.flags.contains(Flags::CUSTOM))
1116 .map(|hit| hit.id)
1117 .collect();
1118 for id in custom {
1119 self.signals
1120 .entry(id)
1121 .or_default()
1122 .events
1123 .push(event.clone());
1124 }
1125 }
1126 }
1127 }
1128
1129 fn custom_event(&mut self, target: Option<Id>, event: Event) {
1130 if let Some(id) = target
1131 && self.hit_flags(id).contains(Flags::CUSTOM)
1132 {
1133 self.signals.entry(id).or_default().events.push(event);
1134 }
1135 }
1136
1137 fn hit_flags(&self, id: Id) -> Flags {
1138 self.hits
1139 .iter()
1140 .find(|hit| hit.id == id)
1141 .map_or(Flags::default(), |hit| hit.flags)
1142 }
1143
1144 /// The topmost box under `point` with any of `flags`.
1145 fn hit(&self, point: [f32; 2], flags: Flags) -> Option<Id> {
1146 self.hits[self.modal..]
1147 .iter()
1148 .rev()
1149 .find(|hit| hit.flags.intersects(flags) && contains(hit.rect, point))
1150 .map(|hit| hit.id)
1151 }
1152
1153 /// Opens popup `id`, which shows while built with its `Spec::anchor` each frame, and
1154 /// takes the keyboard. Popups open from within another stay above it; any other
1155 /// closes those open.
1156 pub fn open_popup(&mut self, id: Id) {
1157 if self.popup_open(id) {
1158 return;
1159 }
1160 let within = self
1161 .stack
1162 .iter()
1163 .filter_map(|index| {
1164 let id = self.nodes[*index].id;
1165 self.popups.iter().position(|popup| popup.id == id)
1166 })
1167 .max();
1168 self.push_popup(id, within.map_or(0, |within| within + 1), None);
1169 }
1170
1171 /// Opens popup `id` as the submenu of open popup `from`, beside its row `row`, in place of
1172 /// any other opened from it.
1173 pub fn open_submenu(&mut self, id: Id, from: Id, row: Id) {
1174 let Some(from) = self.popups.iter().position(|popup| popup.id == from) else {
1175 return;
1176 };
1177 if self.popups.get(from + 1).is_none_or(|open| open.id != id) {
1178 self.push_popup(id, from + 1, Some(row));
1179 }
1180 }
1181
1182 /// Closes the popups from `at` on and opens `id` above the rest.
1183 fn push_popup(&mut self, id: Id, at: usize, beside: Option<Id>) {
1184 self.close_from(at);
1185 self.popups.push(Popup {
1186 id,
1187 focus: self.focus,
1188 query: String::new(),
1189 highlight: None,
1190 picked: None,
1191 opened: self.now,
1192 beside,
1193 submenu: None,
1194 });
1195 self.focus = Some(id);
1196 self.states.entry(id).or_default().touched = self.frame;
1197 }
1198
1199 pub fn popup_open(&self, id: Id) -> bool {
1200 self.popups.iter().any(|popup| popup.id == id)
1201 }
1202
1203 /// Whether any menu, dialog or other popup is open.
1204 pub fn popups_open(&self) -> bool {
1205 !self.popups.is_empty()
1206 }
1207
1208 /// Closes popup `id` and those opened from it, returning the focus it took; a submenu
1209 /// closes the menus it opened from with it, as choosing from it ends them.
1210 pub fn close_popup(&mut self, id: Id) {
1211 if let Some(mut index) = self.popups.iter().position(|popup| popup.id == id) {
1212 while index > 0 && self.popups[index].beside.is_some() {
1213 index -= 1;
1214 }
1215 self.close_from(index);
1216 }
1217 }
1218
1219 fn close_from(&mut self, index: usize) {
1220 if let Some(popup) = self.popups.get(index) {
1221 self.focus = popup.focus;
1222 self.popups.truncate(index);
1223 }
1224 }
1225
1226 fn ease(&mut self, dt: f32) {
1227 self.dt = dt;
1228 let rate = 1.0 - 0.5_f32.powf(dt / HALF_LIFE);
1229 let mut animating = false;
1230 for (id, state) in &mut self.states {
1231 let hot = self.hover == Some(*id) && self.active.is_none_or(|active| active == *id);
1232 if !state.hovers {
1233 state.hot = f32::from(u8::from(hot));
1234 }
1235 let targets = [
1236 (&mut state.hot, f32::from(u8::from(hot)), 0.002),
1237 (&mut state.scroll, state.scroll_target, 0.25),
1238 ];
1239 let tween = state
1240 .tween
1241 .as_mut()
1242 .map(|[value, target]| (value, *target, 0.005));
1243 for (value, target, close) in targets.into_iter().chain(tween) {
1244 let delta = target - *value;
1245 if delta.abs() < close {
1246 *value = target;
1247 } else {
1248 *value += delta * rate;
1249 animating = true;
1250 }
1251 }
1252 }
1253 self.animating = animating;
1254 }
1255
1256 /// The box being built, which new boxes become children of.
1257 pub fn current(&self) -> Id {
1258 self.nodes[*self.stack.last().unwrap()].id
1259 }
1260
1261 /// The id a box built next under the current parent would take.
1262 pub fn id(&self, part: impl Hash) -> Id {
1263 self.current().child(part)
1264 }
1265
1266 /// Adds a box and makes it the parent of the boxes built until `close`.
1267 pub fn open(&mut self, part: impl Hash, spec: Spec<'_>) -> Id {
1268 self.open_as(self.id(part), spec)
1269 }
1270
1271 /// Opens a box under an id chosen outside the hierarchy, so the host can name it
1272 /// wherever it is built.
1273 pub fn open_as(&mut self, id: Id, spec: Spec<'_>) -> Id {
1274 let label = spec.text.map(|text| {
1275 let size = spec.font_size.unwrap_or(self.theme.font_size);
1276 self.texts
1277 .styled(text, size, spec.bold, spec.italic, spec.font, self.frame)
1278 });
1279 // Popups hang from the root, outside the clips and flow of where they are built.
1280 let parent = if spec.anchor.is_some() {
1281 0
1282 } else {
1283 *self.stack.last().unwrap()
1284 };
1285 let open = match spec.anchor {
1286 Some(anchor @ Anchor::Over(_)) if self.popup_motion(anchor) == PopupMotion::Grow => {
1287 self.opening(id, anchor).unwrap_or(1.0)
1288 }
1289 _ => 1.0,
1290 };
1291 let index = self.nodes.len();
1292 self.states.entry(id).or_default().touched = self.frame;
1293 self.nodes.push(Built {
1294 open,
1295 ..Built::new(id, parent, spec, label, self.theme.text)
1296 });
1297 self.nodes[parent].children.push(index);
1298 self.stack.push(index);
1299 id
1300 }
1301
1302 pub fn close(&mut self) {
1303 debug_assert!(self.stack.len() > 1, "close without open");
1304 self.stack.pop();
1305 }
1306
1307 /// Adds a box without children and reports how it was used.
1308 pub fn leaf(&mut self, part: impl Hash, spec: Spec<'_>) -> Signal {
1309 let id = self.open(part, spec);
1310 self.close();
1311 self.signal(id)
1312 }
1313
1314 /// Holds animated value `id` at `value` without easing, as a dragged box follows the
1315 /// pointer; it eases from there once animated again.
1316 pub fn hold(&mut self, id: Id, value: f32) -> f32 {
1317 let state = self.states.entry(id).or_default();
1318 state.touched = self.frame;
1319 state.tween = Some([value; 2]);
1320 value
1321 }
1322
1323 /// Eases towards `target` over the next frames, starting there the first time `id` asks.
1324 pub fn animate(&mut self, id: Id, target: f32) -> f32 {
1325 let state = self.states.entry(id).or_default();
1326 state.touched = self.frame;
1327 let [value, goal] = state.tween.get_or_insert([target; 2]);
1328 *goal = target;
1329 self.animating |= value != goal;
1330 *value
1331 }
1332
1333 /// How long `held` has stayed true through the frames that ask of `id`; zero while
1334 /// false, as a status waits before showing a state that may pass at once.
1335 pub fn lasted(&mut self, id: Id, held: bool) -> Duration {
1336 let now = self.now;
1337 let state = self.states.entry(id).or_default();
1338 state.touched = self.frame;
1339 state.since = held.then(|| state.since.unwrap_or(now));
1340 state
1341 .since
1342 .map_or(Duration::ZERO, |since| now.saturating_duration_since(since))
1343 }
1344
1345 /// Makes `family` the interface's font, where fontique doesn't know the system's, as
1346 /// with Mac OS X 10.6's Lucida Grande.
1347 pub fn set_system_font(&mut self, family: &str) {
1348 self.texts.set_system_font(family);
1349 }
1350
1351 /// Previews `family` in the font `data` holds, as a font menu shows the substitute a
1352 /// missing family lays out in.
1353 pub fn preview_font(&mut self, data: parley::fontique::Blob<u8>, family: &str) {
1354 self.texts.preview_font(data, family);
1355 }
1356
1357 /// Draws what the interface's fonts and their scripts' fallbacks lack with the first of
1358 /// `families` that has it; see `draw::fall_back_to`.
1359 pub fn fall_back_to(&mut self, families: &[String]) {
1360 self.texts.fall_back_to(families);
1361 }
1362
1363 /// Whether the interface's fonts draw every glyph of `text`.
1364 pub fn shows(&mut self, text: &str) -> bool {
1365 self.texts.shows(text, self.frame)
1366 }
1367
1368 /// The size of `text` as a label, in logical pixels.
1369 pub fn measure(&mut self, text: &str) -> [f32; 2] {
1370 self.texts
1371 .label(text, self.theme.font_size, self.frame)
1372 .size
1373 }
1374
1375 /// Paints `color` over the current box at `rect`, relative to its corner, with corners
1376 /// of `radius`.
1377 pub fn mark(&mut self, rect: [f32; 4], color: [f32; 4], radius: f32) {
1378 let index = *self.stack.last().unwrap();
1379 self.nodes[index].marks.push((rect, color, radius));
1380 }
1381
1382 /// The caret opacity of field `id` this frame, restarting its blink when `moved` or
1383 /// when it was not shown the frame before.
1384 pub(crate) fn blink(&mut self, id: Id, moved: bool) -> f32 {
1385 let start = match self.caret {
1386 Some((shown, start, frame)) if shown == id && frame + 1 == self.frame && !moved => {
1387 start
1388 }
1389 _ => self.now,
1390 };
1391 self.caret = Some((id, start, self.frame));
1392 let (opacity, hold) = draw::edit::caret_blink(self.now.saturating_duration_since(start));
1393 if let Some(hold) = hold {
1394 self.wake_after(hold);
1395 }
1396 opacity
1397 }
1398
1399 /// How the user acted on the box this frame; a box's routed events are taken once.
1400 pub fn signal(&mut self, id: Id) -> Signal {
1401 let mut signal = self.signals.remove(&id).unwrap_or_default();
1402 signal.hovered = self.hover == Some(id);
1403 signal.dragging = self.active == Some(id);
1404 signal.focused = self.focus == Some(id);
1405 signal
1406 }
1407
1408 /// Solves this frame's layout and prepares its painting.
1409 pub fn end(&mut self) {
1410 debug_assert_eq!(self.stack.len(), 1, "open without close");
1411 layout::solve(
1412 &mut self.nodes,
1413 &self.states,
1414 self.scale,
1415 &mut self.texts,
1416 self.frame,
1417 );
1418 for node in &self.nodes {
1419 let state = self.states.entry(node.id).or_default();
1420 state.rect = Some(node.rect);
1421 state.hovers = node.hover_fill.is_some() || node.hover_border.is_some();
1422 if node.flags.contains(Flags::SCROLL) {
1423 state.content = node.content;
1424 let most = (node.content - (node.rect[3] - node.rect[1])).max(0.0);
1425 state.scroll_target = state.scroll_target.clamp(0.0, most);
1426 state.scroll = state.scroll.clamp(0.0, most);
1427 }
1428 }
1429 for (scroller, target) in std::mem::take(&mut self.scrolls) {
1430 let (Some(top), Some(start)) = (self.laid_out(target), self.laid_out(scroller)) else {
1431 continue;
1432 };
1433 let state = self.states.entry(scroller).or_default();
1434 let most = (state.content - (start[3] - start[1])).max(0.0);
1435 state.scroll_target = (state.scroll + top[1] - start[1]).clamp(0.0, most);
1436 self.animating |= state.scroll != state.scroll_target;
1437 }
1438 self.states
1439 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);
1440 self.texts.prune(self.frame);
1441 self.lists.retain(|id, _| self.states.contains_key(id));
1442 if let Some(gone) = self
1443 .popups
1444 .iter()
1445 .position(|popup| !self.states.contains_key(&popup.id))
1446 {
1447 self.close_from(gone);
1448 }
1449 if let Some(tip) = self.tip.filter(|tip| tip.frame != self.frame) {
1450 if tip.due.is_some_and(|due| due <= self.now) {
1451 self.warm = Some(self.now);
1452 }
1453 self.tip = None;
1454 }
1455 let tip = self.tip.map(|tip| tip.id.child("tooltip"));
1456 let last = std::mem::take(&mut self.display);
1457 let last_popups = self.popups_painted;
1458 self.hits.clear();
1459 self.paint(0, None, None);
1460 self.popups_painted = self.display.len();
1461 let beneath = self.hits.len();
1462 let mut painted = Vec::new();
1463 for index in self.nodes[0].children.clone() {
1464 let node = &self.nodes[index];
1465 let Some(anchor) = node.anchor else {
1466 continue;
1467 };
1468 let placed = Placed {
1469 id: node.id,
1470 anchor,
1471 around: node.around,
1472 rect: node.rect,
1473 };
1474 if Some(placed.id) == tip {
1475 continue;
1476 }
1477 if let Some(open) = self.opening(placed.id, anchor) {
1478 self.scrim(anchor, open);
1479 let grows = self.popup_motion(anchor) == PopupMotion::Grow;
1480 self.nodes[index].motion = Some(placed.motion(open, grows));
1481 }
1482 let from = self.display.len() - self.popups_painted;
1483 self.paint(index, None, None);
1484 if !self.still.is_empty() {
1485 let still = std::mem::take(&mut self.still);
1486 self.display.extend(still);
1487 self.display.extend([
1488 Display::Motion(None),
1489 Display::Clip(None),
1490 Display::Round(None),
1491 ]);
1492 }
1493 painted.push((placed, from..self.display.len() - self.popups_painted));
1494 }
1495 for (placed, range) in std::mem::replace(&mut self.painted, painted) {
1496 if self.painted.iter().any(|(shown, _)| shown.id == placed.id) {
1497 continue;
1498 }
1499 // What held still while it opened goes at once, uncovering what it stood in for.
1500 let mut still = false;
1501 let display = last[last_popups + range.start..last_popups + range.end]
1502 .iter()
1503 .filter(|item| match item {
1504 Display::Motion(motion) => {
1505 still = motion.is_none();
1506 false
1507 }
1508 _ => !still,
1509 })
1510 .cloned()
1511 .collect();
1512 self.closing.push(Closing {
1513 placed,
1514 display,
1515 closed: self.now,
1516 });
1517 }
1518 for closing in std::mem::take(&mut self.closing) {
1519 let Placed { id, anchor, .. } = closing.placed;
1520 let motion = self.popup_motion(anchor);
1521 let open = match motion {
1522 PopupMotion::Grow => {
1523 (1.0 - self.progress(closing.closed, anchor.durations()[1])).powi(3)
1524 }
1525 PopupMotion::Cut => 0.0,
1526 PopupMotion::Fade([_, close]) => {
1527 (1.0 - self.progress(closing.closed, close)).powi(4)
1528 }
1529 };
1530 if open == 0.0 {
1531 continue;
1532 }
1533 self.scrim(anchor, open);
1534 let grows = motion == PopupMotion::Grow;
1535 let motion = closing.placed.motion(open, grows);
1536 self.display.push(Display::Motion(Some(motion)));
1537 self.display.extend(closing.display.iter().cloned());
1538 self.display.push(Display::Motion(None));
1539 if !self.painted.iter().any(|(shown, _)| shown.id == id) {
1540 self.closing.push(closing);
1541 }
1542 }
1543 // Over everything, closing popups included; it goes at once, without closing.
1544 if let Some(index) = self.nodes[0]
1545 .children
1546 .iter()
1547 .copied()
1548 .find(|index| Some(self.nodes[*index].id) == tip)
1549 && let Some(due) = self.tip.and_then(|tip| tip.due)
1550 {
1551 let opacity = self.progress(due, TIP_FADE);
1552 self.nodes[index].motion = Some(Motion {
1553 opacity,
1554 ..Motion::NONE
1555 });
1556 self.paint(index, None, None);
1557 }
1558 self.modal = if self.popups.is_empty() { 0 } else { beneath };
1559 for id in [
1560 &mut self.hover,
1561 &mut self.active,
1562 &mut self.focus,
1563 &mut self.resume,
1564 ] {
1565 if id.is_some_and(|id| !self.states.contains_key(&id)) {
1566 *id = None;
1567 }
1568 }
1569 self.stops = self.stops();
1570 }
1571
1572 /// How far open popup `id` beside `anchor` shows this frame, from 0 to 1, while open.
1573 pub fn opening(&mut self, id: Id, anchor: Anchor) -> Option<f32> {
1574 let opened = self.popups.iter().find(|popup| popup.id == id)?.opened;
1575 Some(match self.popup_motion(anchor) {
1576 PopupMotion::Grow => 1.0 - (1.0 - self.progress(opened, anchor.durations()[0])).powi(3),
1577 PopupMotion::Cut => 1.0,
1578 PopupMotion::Fade([open, _]) => self.progress(opened, open),
1579 })
1580 }
1581
1582 /// How popups beside `anchor` open and close; dialogs and the palette always swing as
1583 /// Snowbound's own.
1584 fn popup_motion(&self, anchor: Anchor) -> PopupMotion {
1585 if matches!(anchor, Anchor::Dialog | Anchor::Top | Anchor::Dock(_)) {
1586 PopupMotion::Grow
1587 } else {
1588 self.theme.menu().motion
1589 }
1590 }
1591
1592 /// How far through an animation `duration` seconds long that began at `start` this
1593 /// frame is, asking for frames until it ends.
1594 fn progress(&mut self, start: Instant, duration: f32) -> f32 {
1595 let progress =
1596 (self.now.saturating_duration_since(start).as_secs_f32() / duration).min(1.0);
1597 self.animating |= progress < 1.0;
1598 progress
1599 }
1600
1601 /// Dims the window beneath a dialog `open` of the way open.
1602 fn scrim(&mut self, anchor: Anchor, open: f32) {
1603 if anchor == Anchor::Dialog {
1604 self.display.push(Display::Rect {
1605 rect: self.nodes[0].rect,
1606 fill: [0.0, 0.0, 0.0, self.theme.shadow[3] * open],
1607 shade: None,
1608 border: None,
1609 radius: 0.0,
1610 });
1611 }
1612 }
1613
1614 fn paint(&mut self, index: usize, clip: Option<[f32; 4]>, motion: Option<Motion>) {
1615 // Painted over the popup it holds still in, which appears as one picture beneath.
1616 if self.nodes[index].flags.contains(Flags::STILL) && motion.is_some() {
1617 let from = self.display.len();
1618 self.paint_box(index, clip, motion);
1619 let still = self.display.split_off(from);
1620 self.still.extend(still);
1621 } else {
1622 self.paint_box(index, clip, motion);
1623 }
1624 }
1625
1626 fn paint_box(&mut self, index: usize, clip: Option<[f32; 4]>, motion: Option<Motion>) {
1627 let node = &self.nodes[index];
1628 // The motion the box and its children paint with, where it changes it.
1629 let own = if node.flags.contains(Flags::STILL) {
1630 Some(None)
1631 } else {
1632 node.motion.map(Some)
1633 };
1634 if let Some(own) = own {
1635 self.display.push(Display::Motion(own));
1636 }
1637 let state = &self.states[&node.id];
1638 let rect = node.rect;
1639 let visible = clip.map_or(Some(rect), |clip| intersect(rect, clip));
1640 let blend = |base: Option<[f32; 4]>, hover: Option<[f32; 4]>| match (base, hover) {
1641 (base, Some(hover)) => Some(mix(base.unwrap_or_default(), hover, state.hot)),
1642 (base, None) => base,
1643 };
1644 let fill = blend(node.fill, node.hover_fill);
1645 // The gradient keeps its difference from the fill as hover moves it.
1646 let shade = node.gradient.map(|shade| {
1647 let base = node.fill.unwrap_or(shade);
1648 std::array::from_fn(|channel| {
1649 shade[channel] + fill.unwrap_or(base)[channel] - base[channel]
1650 })
1651 });
1652 let border = blend(node.border, node.hover_border);
1653 let [left, top, right, bottom] = node.inset;
1654 // A box squeezed smaller than its inset, or than nothing as it eases shut, paints
1655 // empty rather than inverted.
1656 let [x, y] = [rect[0] + left, rect[1] + top];
1657 let painted = [x, y, (rect[2] - right).max(x), (rect[3] - bottom).max(y)];
1658 let size = [painted[2] - painted[0], painted[3] - painted[1]];
1659 if let Some(color) = node.shadow {
1660 let (own, menu);
1661 let shadows: &[Shadow] = match node.anchor {
1662 Some(Anchor::Dialog | Anchor::Dock(_)) | None => {
1663 let [blur, drop] = if node.anchor.is_some() {
1664 POPUP_SHADOW
1665 } else {
1666 SHADOW
1667 };
1668 own = [Shadow {
1669 color,
1670 blur,
1671 drop,
1672 spread: 0.0,
1673 }];
1674 &own
1675 }
1676 Some(_) => {
1677 menu = self.theme.menu();
1678 &menu.shadows
1679 }
1680 };
1681 for &Shadow {
1682 color,
1683 blur,
1684 drop,
1685 spread,
1686 } in shadows
1687 {
1688 let origin = [painted[0] - spread, painted[1] + drop - spread];
1689 let grown = size.map(|side| side + 2.0 * spread);
1690 self.display.push(match node.shape {
1691 Shape::Rounded => Display::Shadow {
1692 rect: [
1693 origin[0],
1694 origin[1],
1695 origin[0] + grown[0],
1696 origin[1] + grown[1],
1697 ],
1698 radius: node.radius + spread,
1699 blur,
1700 color,
1701 },
1702 Shape::Tab { .. } => Display::Path {
1703 data: outline(node.shape, grown, node.radius + spread),
1704 origin,
1705 style: PathStyle::Shadow(blur),
1706 colors: [color; 2],
1707 },
1708 });
1709 }
1710 }
1711 if node.shape == Shape::Rounded {
1712 if fill.is_some() || border.is_some() {
1713 self.display.push(Display::Rect {
1714 rect: painted,
1715 fill: fill.unwrap_or([0.0; 4]),
1716 shade,
1717 border,
1718 radius: node.radius,
1719 });
1720 }
1721 } else {
1722 let data = outline(node.shape, size, node.radius);
1723 let paints = [
1724 fill.map(|fill| (PathStyle::Fill, [fill, shade.unwrap_or(fill)])),
1725 border.map(|border| (PathStyle::Stroke(1.0), [border; 2])),
1726 ];
1727 for (style, colors) in paints.into_iter().flatten() {
1728 self.display.push(Display::Path {
1729 data: data.clone(),
1730 origin: [painted[0], painted[1]],
1731 style,
1732 colors,
1733 });
1734 }
1735 }
1736 for (mark, color, radius) in &node.marks {
1737 self.display.push(Display::Rect {
1738 rect: [
1739 rect[0] + mark[0],
1740 rect[1] + mark[1],
1741 rect[0] + mark[2],
1742 rect[1] + mark[3],
1743 ],
1744 fill: *color,
1745 shade: None,
1746 border: None,
1747 radius: *radius,
1748 });
1749 }
1750 // A box squeezed narrower than its padding keeps an empty inside, not an inverted one.
1751 let left = (painted[0] + node.pad[0]).min(painted[2]);
1752 let inner = [
1753 left,
1754 painted[1],
1755 (painted[2] - node.pad[0]).max(left),
1756 painted[3],
1757 ];
1758 let mut x = if node.center {
1759 (inner[0] + inner[2] - node.content_width()) / 2.0
1760 } else {
1761 inner[0]
1762 };
1763 let top = (painted[1] + painted[3] - ICON) / 2.0;
1764 if let Some(sources) = node.icon {
1765 self.display.push(Display::Icon {
1766 sources,
1767 origin: [x, top],
1768 tint: node.tint,
1769 });
1770 x += ICON + ICON_GAP;
1771 } else if let Some(image) = &node.image {
1772 let rect = if node.label.is_some() {
1773 [x, top, x + ICON, top + ICON]
1774 } else {
1775 let top = (painted[1] + node.pad[1]).min(painted[3]);
1776 [inner[0], top, inner[2], (painted[3] - node.pad[1]).max(top)]
1777 };
1778 self.display.push(Display::Image {
1779 image: image.clone(),
1780 rect,
1781 });
1782 x += ICON + ICON_GAP;
1783 }
1784 if let Some(label) = &node.label {
1785 let y = (painted[1] + painted[3] - label.size[1]) / 2.0;
1786 self.display.push(Display::Text {
1787 label: label.clone(),
1788 origin: [x, y],
1789 clip: inner,
1790 color: node.color,
1791 });
1792 }
1793 if let Some(visible) = visible
1794 && node
1795 .flags
1796 .intersects(Flags::CLICKABLE | Flags::CUSTOM | Flags::SCROLL)
1797 {
1798 self.hits.push(Hit {
1799 id: node.id,
1800 rect: visible,
1801 flags: node.flags,
1802 cursor: node.cursor,
1803 });
1804 if node.flags.contains(Flags::CUSTOM) {
1805 self.display.push(Display::Custom {
1806 id: node.id,
1807 rect: visible,
1808 });
1809 }
1810 }
1811 // A popup's contents show only inside its rounded outline.
1812 let round = node.anchor.map(|_| (rect, node.radius));
1813 let inner_clip = if node.flags.contains(Flags::CLIP) || round.is_some() {
1814 Some(visible.unwrap_or([rect[0], rect[1], rect[0], rect[1]]))
1815 } else {
1816 clip
1817 };
1818 if inner_clip != clip {
1819 self.display.push(Display::Clip(inner_clip));
1820 }
1821 if round.is_some() {
1822 self.display.push(Display::Round(round));
1823 }
1824 for child in node.children.clone() {
1825 if self.nodes[child].anchor.is_none() && !self.nodes[child].hidden {
1826 self.paint(child, inner_clip, own.unwrap_or(motion));
1827 }
1828 }
1829 if round.is_some() {
1830 self.display.push(Display::Round(None));
1831 }
1832 self.fade(index);
1833 if inner_clip != clip {
1834 self.display.push(Display::Clip(clip));
1835 }
1836 let node = &self.nodes[index];
1837 if self.focus_ring
1838 && self.focus == Some(node.id)
1839 && node.access.is_some()
1840 && node.anchor.is_none()
1841 && !node.flags.contains(Flags::CUSTOM)
1842 {
1843 let size = size.map(|side| side + 2.0 * FOCUS_RING);
1844 self.display.push(Display::Path {
1845 data: outline(Shape::Rounded, size, node.radius + FOCUS_RING),
1846 origin: [painted[0] - FOCUS_RING, painted[1] - FOCUS_RING],
1847 style: PathStyle::Stroke(FOCUS_RING),
1848 colors: [self.theme.accent; 2],
1849 });
1850 }
1851 if own.is_some() {
1852 self.display.push(Display::Motion(motion));
1853 }
1854 }
1855
1856 /// Fades box `index`'s children out towards its ends along its axis by its `fade`, in
1857 /// steps of a point, into its fill however opaque that is: each step clears what lies
1858 /// beneath towards transparency and lays the fill over it.
1859 fn fade(&mut self, index: usize) {
1860 let node = &self.nodes[index];
1861 let along = usize::from(node.axis == Axis::Y);
1862 let rect = node.rect;
1863 let fill = node.fill.unwrap_or_default();
1864 let mut steps = Vec::new();
1865 for (side, width) in node.fade.into_iter().enumerate() {
1866 let count = width.round() as usize;
1867 for step in 0..count {
1868 // Through the step's middle, eased so the fade leaves the children softly.
1869 let alpha = 1.0 - (step as f32 + 0.5) / count as f32;
1870 let alpha = alpha * alpha * (3.0 - 2.0 * alpha);
1871 let at = if side == 0 {
1872 rect[along] + step as f32
1873 } else {
1874 rect[along + 2] - 1.0 - step as f32
1875 };
1876 let mut band = rect;
1877 [band[along], band[along + 2]] = [at, at + 1.0];
1878 steps.push((band, alpha));
1879 }
1880 }
1881 let [width, height] = [rect[2] - rect[0], rect[3] - rect[1]];
1882 let data = if along == 0 {
1883 format!("M0 0H1V{height}H0Z")
1884 } else {
1885 format!("M0 0H{width}V1H0Z")
1886 };
1887 for (rect, alpha) in steps {
1888 // Laid over what the erasing leaves, the fill's share makes up the rest of the
1889 // children's lost opacity: all of it where the fill is opaque, none where clear.
1890 let laid = alpha * fill[3];
1891 if laid < 1.0 {
1892 self.display.push(Display::Path {
1893 data: data.clone(),
1894 origin: [rect[0], rect[1]],
1895 style: PathStyle::Erase,
1896 colors: [[0.0, 0.0, 0.0, 1.0 - (1.0 - alpha) / (1.0 - laid)]; 2],
1897 });
1898 }
1899 self.display.push(Display::Rect {
1900 rect,
1901 fill: [fill[0], fill[1], fill[2], laid],
1902 shade: None,
1903 border: None,
1904 radius: 0.0,
1905 });
1906 }
1907 }
1908
1909 /// This frame's painting in order, in logical pixels.
1910 pub fn layers(&self) -> Vec<Layer<'_>> {
1911 let mut layers = Vec::new();
1912 let mut clip = None;
1913 let mut round = None;
1914 let mut motion = None;
1915 let mut primitives = Vec::new();
1916 fn flush<'a>(
1917 layers: &mut Vec<Layer<'a>>,
1918 clip: Option<[f32; 4]>,
1919 round: Option<([f32; 4], f32)>,
1920 motion: Option<Motion>,
1921 primitives: &mut Vec<Primitive<'a>>,
1922 ) {
1923 if !primitives.is_empty() {
1924 layers.push(Layer::Primitives(Primitives {
1925 clip,
1926 round,
1927 motion,
1928 primitives: std::mem::take(primitives),
1929 }));
1930 }
1931 }
1932 for item in &self.display {
1933 match item {
1934 Display::Clip(next) => {
1935 flush(&mut layers, clip, round, motion, &mut primitives);
1936 clip = *next;
1937 }
1938 Display::Round(next) => {
1939 flush(&mut layers, clip, round, motion, &mut primitives);
1940 round = *next;
1941 }
1942 Display::Motion(next) => {
1943 flush(&mut layers, clip, round, motion, &mut primitives);
1944 motion = *next;
1945 }
1946 Display::Rect {
1947 rect,
1948 fill,
1949 shade,
1950 border,
1951 radius,
1952 } => {
1953 if let Some(shade) = shade {
1954 primitives.push(Primitive::Gradient {
1955 rect: *rect,
1956 radius: [*radius; 2],
1957 colors: [*fill, *shade],
1958 });
1959 } else if fill[3] > 0.0 {
1960 primitives.push(if *radius > 0.0 {
1961 Primitive::RoundedRect {
1962 rect: *rect,
1963 radius: [*radius; 2],
1964 stroke: None,
1965 color: *fill,
1966 }
1967 } else {
1968 Primitive::Rect {
1969 rect: *rect,
1970 color: *fill,
1971 }
1972 });
1973 }
1974 if let Some(border) = border.filter(|border| border[3] > 0.0) {
1975 primitives.push(Primitive::RoundedRect {
1976 rect: *rect,
1977 radius: [*radius; 2],
1978 stroke: Some(Stroke::Solid(1.0 / self.scale)),
1979 color: border,
1980 });
1981 }
1982 }
1983 Display::Path {
1984 data,
1985 origin,
1986 style,
1987 colors,
1988 } => primitives.push(Primitive::Path {
1989 data,
1990 origin: *origin,
1991 style: *style,
1992 colors: *colors,
1993 }),
1994 &Display::Shadow {
1995 rect,
1996 radius,
1997 blur,
1998 color,
1999 } => primitives.push(Primitive::Shadow {
2000 rect,
2001 radius,
2002 blur,
2003 color,
2004 }),
2005 Display::Segment { from, to, color } => primitives.push(Primitive::Segment {
2006 from: *from,
2007 to: *to,
2008 width: 1.0,
2009 round: false,
2010 color: *color,
2011 }),
2012 Display::Image { image, rect } => {
2013 primitives.push(Primitive::Image { image, rect: *rect })
2014 }
2015 Display::Icon {
2016 sources,
2017 origin,
2018 tint,
2019 } => primitives.push(Primitive::Icon {
2020 sources,
2021 origin: *origin,
2022 size: ICON,
2023 tint: *tint,
2024 palette: self.icon_palette,
2025 }),
2026 Display::Text {
2027 label,
2028 origin,
2029 clip,
2030 color,
2031 } => primitives.push(Primitive::Text {
2032 text: &**label,
2033 origin: *origin,
2034 clip: Some(*clip),
2035 ink: *color,
2036 }),
2037 Display::Custom { id, rect } => {
2038 flush(&mut layers, clip, round, motion, &mut primitives);
2039 layers.push(Layer::Custom {
2040 id: *id,
2041 rect: *rect,
2042 });
2043 }
2044 }
2045 }
2046 flush(&mut layers, clip, round, motion, &mut primitives);
2047 layers
2048 }
2049
2050 pub(crate) fn state(&mut self, id: Id) -> &mut State {
2051 self.states.entry(id).or_default()
2052 }
2053}
2054
2055impl Built {
2056 /// The width of the icon and label together.
2057 fn content_width(&self) -> f32 {
2058 let label = self.label.as_ref().map_or(0.0, |label| label.size[0]);
2059 match (self.icon.is_some() || self.image.is_some(), label > 0.0) {
2060 (true, true) => ICON + ICON_GAP + label,
2061 (true, false) => ICON,
2062 (false, _) => label,
2063 }
2064 }
2065
2066 fn new(
2067 id: Id,
2068 parent: usize,
2069 spec: Spec<'_>,
2070 label: Option<Rc<Label>>,
2071 text: [f32; 4],
2072 ) -> Self {
2073 Self {
2074 id,
2075 parent,
2076 children: Vec::new(),
2077 flags: spec.flags,
2078 size: spec.size,
2079 axis: spec.axis,
2080 label,
2081 overflow: spec.overflow,
2082 color: spec.color.unwrap_or(text),
2083 fill: spec.fill,
2084 gradient: spec.gradient,
2085 shadow: spec.shadow,
2086 inset: spec.inset,
2087 hover_fill: spec.hover_fill,
2088 border: spec.border,
2089 hover_border: spec.hover_border,
2090 radius: spec.radius,
2091 shape: spec.shape,
2092 icon: spec.icon,
2093 tint: spec.tint.or(spec.color).unwrap_or(text),
2094 image: spec.image.cloned(),
2095 pad: spec.pad,
2096 gap: spec.gap,
2097 center: spec.center,
2098 position: spec.position,
2099 offset: spec.offset,
2100 cursor: spec.cursor,
2101 anchor: spec.anchor,
2102 around: [0.0; 4],
2103 open: 1.0,
2104 motion: None,
2105 marks: Vec::new(),
2106 fold: spec.fold,
2107 fade: spec.fade,
2108 access: spec.role.map(accesskit::Node::new),
2109 hidden: false,
2110 computed: [0.0; 2],
2111 relative: [0.0; 2],
2112 rect: [0.0; 4],
2113 content: 0.0,
2114 }
2115 }
2116}
2117
2118/// SVG path data outlining a box of `size` from its corner, rounding corners by `radius`.
2119/// A tab's outline stays open along its bottom, so a border leaves the edge it stands on.
2120fn outline(shape: Shape, [width, height]: [f32; 2], radius: f32) -> String {
2121 let corners = match shape {
2122 Shape::Tab { lean } => [
2123 ([0.0, height], 0.0),
2124 ([0.0, 0.0], radius),
2125 ([width - lean, 0.0], radius / 2.0),
2126 ([width - lean + height, height], 0.0),
2127 ],
2128 Shape::Rounded => [
2129 ([0.0, height], radius),
2130 ([0.0, 0.0], radius),
2131 ([width, 0.0], radius),
2132 ([width, height], radius),
2133 ],
2134 };
2135 let mut data = String::new();
2136 for index in 0..corners.len() {
2137 let corner = Corner::new(&corners, index);
2138 let verb = if index == 0 { 'M' } else { 'L' };
2139 data += &format!(
2140 "{verb}{} {}{}",
2141 corner.start[0],
2142 corner.start[1],
2143 corner.curve([0.0; 2])
2144 );
2145 }
2146 if !matches!(shape, Shape::Tab { .. }) {
2147 data.push('Z');
2148 }
2149 data
2150}
2151
2152/// A polygon's corner rounded by a quarter-circle's cubic, from where it leaves the edge
2153/// before to where it joins the edge after.
2154struct Corner {
2155 point: [f32; 2],
2156 start: [f32; 2],
2157 end: [f32; 2],
2158 controls: [[f32; 2]; 2],
2159 /// It turns clockwise, so it bulges out of a shape listed clockwise.
2160 outward: bool,
2161}
2162
2163impl Corner {
2164 /// The corner at `points[index]`, each point with its rounding radius.
2165 fn new(points: &[([f32; 2], f32)], index: usize) -> Self {
2166 let count = points.len();
2167 let (point, radius) = points[index];
2168 let [before, after] =
2169 [(index + count - 1) % count, (index + 1) % count].map(|index| points[index].0);
2170 let direction = |from: [f32; 2], to: [f32; 2]| {
2171 let length = (to[0] - from[0]).hypot(to[1] - from[1]).max(f32::EPSILON);
2172 [(to[0] - from[0]) / length, (to[1] - from[1]) / length]
2173 };
2174 let [incoming, outgoing] = [direction(before, point), direction(point, after)];
2175 let along = |from: [f32; 2], direction: [f32; 2], distance: f32| {
2176 [
2177 from[0] + direction[0] * distance,
2178 from[1] + direction[1] * distance,
2179 ]
2180 };
2181 let start = along(point, incoming, -radius);
2182 let end = along(point, outgoing, radius);
2183 // A quarter circle's cubic control distance.
2184 let handle = radius * 0.552_284_8;
2185 Self {
2186 point,
2187 start,
2188 end,
2189 controls: [
2190 along(start, incoming, handle),
2191 along(end, outgoing, -handle),
2192 ],
2193 outward: incoming[0] * outgoing[1] - incoming[1] * outgoing[0] > 0.0,
2194 }
2195 }
2196
2197 /// The cubic from `start` to `end`, as path data relative to `origin`.
2198 fn curve(&self, origin: [f32; 2]) -> String {
2199 let [a, b, c] = [self.controls[0], self.controls[1], self.end]
2200 .map(|point| [point[0] - origin[0], point[1] - origin[1]]);
2201 format!("C{} {} {} {} {} {}", a[0], a[1], b[0], b[1], c[0], c[1])
2202 }
2203}
2204
2205/// Where a box dragged from index `from` of a list laid out at `spans`, each a start and a
2206/// length along it, lands with its middle at `middle`: after every other box whose middle it
2207/// has passed, as browser tabs reorder. The index is in the list's new order.
2208pub fn drop_slot(spans: &[[f32; 2]], from: usize, middle: f32) -> usize {
2209 spans
2210 .iter()
2211 .enumerate()
2212 .filter(|(index, [start, length])| *index != from && start + length / 2.0 < middle)
2213 .count()
2214}
2215
2216/// How far box `index` of a list slides aside while the box at `from`, `length` long, is
2217/// dragged to land at `to`.
2218pub fn slide(index: usize, from: usize, to: usize, length: f32) -> f32 {
2219 if from < index && index <= to {
2220 -length
2221 } else if to <= index && index < from {
2222 length
2223 } else {
2224 0.0
2225 }
2226}
2227
2228/// `a` moved `t` of the way to `b`, mixed as premultiplied colours, so from transparent
2229/// `b` fades in with its own hue.
2230pub fn mix(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
2231 let alpha = a[3] + (b[3] - a[3]) * t;
2232 if alpha == 0.0 {
2233 return [0.0; 4];
2234 }
2235 let channel = |i: usize| (a[i] * a[3] + (b[i] * b[3] - a[i] * a[3]) * t) / alpha;
2236 [channel(0), channel(1), channel(2), alpha]
2237}
2238
2239fn contains(rect: [f32; 4], point: [f32; 2]) -> bool {
2240 point[0] >= rect[0] && point[0] < rect[2] && point[1] >= rect[1] && point[1] < rect[3]
2241}
2242
2243fn intersect(a: [f32; 4], b: [f32; 4]) -> Option<[f32; 4]> {
2244 let rect = [
2245 a[0].max(b[0]),
2246 a[1].max(b[1]),
2247 a[2].min(b[2]),
2248 a[3].min(b[3]),
2249 ];
2250 (rect[0] < rect[2] && rect[1] < rect[3]).then_some(rect)
2251}
2252
2253#[cfg(test)]
2254mod tests;