| 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 | |
| 6 | mod access; |
| 7 | mod layout; |
| 8 | mod list; |
| 9 | pub mod popup; |
| 10 | pub mod shell; |
| 11 | mod text; |
| 12 | mod theme; |
| 13 | mod widgets; |
| 14 | |
| 15 | pub use list::{List, Row, Rows, list}; |
| 16 | pub use theme::{Menu, PopupMotion, Section, Shades, Shadow, Theme}; |
| 17 | pub 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 | |
| 23 | use draw::{ |
| 24 | PathStyle, Primitive, RasterImage, Stroke, |
| 25 | edit::{Clicks, SelectionUnit}, |
| 26 | }; |
| 27 | use parley::editing::Selection; |
| 28 | use std::{ |
| 29 | collections::HashMap, |
| 30 | hash::{DefaultHasher, Hash, Hasher}, |
| 31 | ops::BitOr, |
| 32 | rc::Rc, |
| 33 | time::Duration, |
| 34 | }; |
| 35 | use text::{Label, Texts}; |
| 36 | use web_time::Instant; |
| 37 | use 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. |
| 44 | const 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. |
| 47 | pub const SHADOW: [f32; 2] = [3.0, 0.0]; |
| 48 | const POPUP_SHADOW: [f32; 2] = [12.0, 4.0]; |
| 49 | /// Logical size of a box's icon, and its distance from the label. |
| 50 | const ICON: f32 = 16.0; |
| 51 | const ICON_GAP: f32 = 6.0; |
| 52 | /// Seconds a dialog and other popups take to open and to close. |
| 53 | const DIALOG: [f32; 2] = [0.24, 0.16]; |
| 54 | const 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`. |
| 58 | const TIP_DELAY: f32 = 0.5; |
| 59 | const TIP_WARM: f32 = 0.5; |
| 60 | const TIP_FADE: f32 = 0.1; |
| 61 | /// How far outside a control the ring the keyboard's focus draws round it lies, and its width. |
| 62 | const 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)] |
| 66 | pub struct Id(u64); |
| 67 | |
| 68 | impl 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)] |
| 85 | pub 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)] |
| 99 | pub struct Extent { |
| 100 | pub size: Size, |
| 101 | pub strictness: f32, |
| 102 | } |
| 103 | |
| 104 | impl Default for Extent { |
| 105 | fn default() -> Self { |
| 106 | children() |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | /// Logical pixels, as `px`. |
| 111 | impl From<f32> for Extent { |
| 112 | fn from(pixels: f32) -> Self { |
| 113 | px(pixels) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | pub fn px(pixels: f32) -> Extent { |
| 118 | Extent { |
| 119 | size: Size::Pixels(pixels), |
| 120 | strictness: 1.0, |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | pub 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. |
| 132 | pub fn fill() -> Extent { |
| 133 | Extent { |
| 134 | size: Size::Fraction(1.0), |
| 135 | strictness: 0.0, |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | pub fn children() -> Extent { |
| 140 | Extent { |
| 141 | size: Size::Children, |
| 142 | strictness: 1.0, |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| 147 | pub enum Axis { |
| 148 | #[default] |
| 149 | X, |
| 150 | Y, |
| 151 | } |
| 152 | |
| 153 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| 154 | pub struct Flags(u32); |
| 155 | |
| 156 | impl 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 | |
| 182 | impl 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)] |
| 192 | pub 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)] |
| 203 | pub 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. |
| 214 | const POPUP_MARGIN: f32 = 4.0; |
| 215 | |
| 216 | /// The longest a popup may be in a window `room` long; one longer scrolls or squeezes. |
| 217 | fn 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)] |
| 224 | pub 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 | |
| 248 | impl 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)] |
| 301 | struct Motion { |
| 302 | zoom: f32, |
| 303 | pivot: [f32; 2], |
| 304 | tilt: f32, |
| 305 | opacity: f32, |
| 306 | } |
| 307 | |
| 308 | impl 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)] |
| 319 | pub 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)] |
| 389 | pub 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)] |
| 412 | pub 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. |
| 431 | pub 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. |
| 442 | pub 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 | |
| 450 | impl 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 | |
| 487 | struct 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)] |
| 536 | struct 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. |
| 563 | struct 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)] |
| 582 | struct 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)] |
| 590 | struct Placed { |
| 591 | id: Id, |
| 592 | anchor: Anchor, |
| 593 | around: [f32; 4], |
| 594 | rect: [f32; 4], |
| 595 | } |
| 596 | |
| 597 | impl 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. |
| 643 | struct Closing { |
| 644 | placed: Placed, |
| 645 | display: Vec<Display>, |
| 646 | closed: Instant, |
| 647 | } |
| 648 | |
| 649 | struct Hit { |
| 650 | id: Id, |
| 651 | rect: [f32; 4], |
| 652 | flags: Flags, |
| 653 | cursor: Option<CursorIcon>, |
| 654 | } |
| 655 | |
| 656 | #[derive(Clone)] |
| 657 | enum 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 | |
| 711 | pub 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 | |
| 778 | impl 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 | |
| 2055 | impl 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. |
| 2120 | fn 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. |
| 2154 | struct 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 | |
| 2163 | impl 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. |
| 2208 | pub 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`. |
| 2218 | pub 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. |
| 2230 | pub 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 | |
| 2239 | fn 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 | |
| 2243 | fn 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)] |
| 2254 | mod tests; |