kui_core/display.rs
1//! The renderer boundary: a flat list of quads in physical pixels.
2//!
3//! A backend needs exactly two abilities: draw these quads, and mirror the
4//! glyph atlas to a texture. Everything else (layout, shaping, styling)
5//! happened already. [`Core::output`](crate::Core::output) hands out the
6//! finished frame's [`DisplayList`] together with the window's
7//! [`GlyphAtlas`](crate::atlas::GlyphAtlas).
8//!
9//! Consuming one frame:
10//!
11//! ```rust
12//! use kui_core::{Color, Core, NodeSpec, QuadKind, Size};
13//!
14//! let mut core = Core::new();
15//! let mut ui = core.frame(Size::new(200.0, 100.0), 2.0);
16//! ui.leaf(NodeSpec::row().size(50.0, 20.0).bg(Color::WHITE));
17//! ui.finish();
18//!
19//! let (list, atlas) = core.output();
20//! if atlas.dirty {
21//! // Upload `atlas.pixels` (RGBA, `atlas.size` square). When
22//! // `atlas.epoch` moved, the page was replaced: re-create the texture.
23//! atlas.dirty = false;
24//! }
25//! for quad in &list.quads {
26//! let clip = list.clip_of(quad); // physical px, like `quad.rect`
27//! match quad.kind {
28//! QuadKind::Solid => { /* rounded rect, optional border */ }
29//! QuadKind::GlyphMask | QuadKind::GlyphColor | QuadKind::GlyphSubpixel => {
30//! /* sample the atlas at the texel rect in `quad.uv` */
31//! }
32//! _ => { /* see each variant's doc */ }
33//! }
34//! let _ = clip;
35//! }
36//! assert_eq!(list.scale, 2.0);
37//! // 50 logical px at scale 2.
38//! assert!(list.quads.iter().any(|q| q.kind == QuadKind::Solid && q.rect.w == 100.0));
39//! ```
40//!
41//! Quads are in paint order. Every quad names an entry of
42//! [`DisplayList::clips`]; a [`QuadKind::Fragment`] or
43//! [`QuadKind::Texture`] quad also names an entry of
44//! [`DisplayList::fragments`] or [`DisplayList::textures`] through
45//! `uv[0]`, and the list carries what a backend needs to compile or upload
46//! those the first time it meets them.
47
48use crate::color::Color;
49use crate::geom::{Rect, Size, Transform};
50
51#[repr(u32)]
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum QuadKind {
54 /// Rounded rect with optional border; ignores uv.
55 Solid,
56 /// Alpha-mask glyph: atlas alpha times `color`.
57 GlyphMask,
58 /// Color bitmap glyph (emoji): atlas rgb, alpha times `color.a`.
59 GlyphColor,
60 /// Registered image blitted into the atlas: atlas rgba tinted by
61 /// `color` (white = as-is), rounded by `radius` like a solid.
62 /// `border_w` is the `sampling` flag — 0 linear, 1 nearest — a slot this
63 /// kind had no other use for; `blur` is 0.
64 Image,
65 /// LCD subpixel glyph: atlas rgb is per-channel coverage (times
66 /// `color.a`), `color.rgb` the text color. Needs per-channel (dual
67 /// source) blending; a backend without it treats the atlas alpha as a
68 /// plain mask.
69 GlyphSubpixel,
70 /// Drop shadow: `color` filling a rounded rect inset from `rect` by
71 /// `blur` on every side, its edge ramped over `blur` px. The core
72 /// emits it just before the node's own quads, already offset and
73 /// spread, so a backend only has to soften the SDF it already
74 /// computes. Ignores `uv`, `border_color` and `border_w`.
75 Shadow,
76 /// A round-capped stroke between two endpoints.
77 /// `uv` holds the endpoints
78 /// as `[x0, y0, x1, y1]` in physical px, each an `f32` stored through
79 /// `to_bits` — [`Quad::segment_ends`] reads them back — `border_w` is
80 /// the stroke width and `color` the stroke. `rect` is the bounding
81 /// box, the endpoints inflated by half the width plus two logical px
82 /// so the edge ramp is never cut by the quad's own edge. A backend
83 /// evaluates an SDF capsule against the fragment's position. Ignores
84 /// `radius`, `border_color` and `blur`.
85 Segment,
86 /// A box a host-registered WGSL function paints.
87 /// `uv[0]` indexes [`DisplayList::fragments`], which carries the
88 /// handle and the sixteen parameters; the other three words of `uv`
89 /// are zero. `rect`, `radius`, `clip` and `clip_radius` are the
90 /// node's and mean what they mean everywhere else — the backend
91 /// rounds and clips a fragment exactly as it rounds and clips a
92 /// solid. `color` carries the group opacity in its alpha and nothing
93 /// else (`rgb` is zero), because a fragment returns its own colour
94 /// and a faded subtree still has to fade it. `border_color`,
95 /// `border_w` and `blur` are zero and ignored. A backend that cannot
96 /// draw one — anything predating this kind — draws nothing, which is
97 /// what a missing handle does too.
98 Fragment,
99 /// A registered image drawn from a texture of its own rather than the
100 /// atlas: one that did
101 /// not fit a page, or whose pixels the app has replaced. `uv[0]`
102 /// indexes [`DisplayList::textures`], which carries the handle and the
103 /// texel rect *in that texture*; the other three words of `uv` are
104 /// zero. Everything else is what an `Image` quad's is — `rect`,
105 /// `radius`, `clip`, `color` as a tint (white = as-is) with the group
106 /// opacity in its alpha, and `border_w` the sampling flag. A backend
107 /// binds the texture in place of the atlas for the run and draws it
108 /// as an `Image`; one that predates the kind draws nothing.
109 Texture,
110 /// Blur what is already drawn beneath `rect` — CSS's
111 /// `backdrop-filter: blur()`, a node's `backdrop_blur` (backlog
112 /// F129). `blur` is the radius in physical px; `radius`, `clip` and
113 /// the clip's radii shape the region as they shape a solid; `color`
114 /// carries the group opacity in its alpha (`rgb` zero), how much of
115 /// the blurred picture replaces the sharp one. The core emits it just
116 /// before the node's own background, so what the node paints lies on
117 /// top of the blur. It paints nothing of its own: a backend reads back
118 /// the pixels drawn so far under the rect (plus a margin of `blur` so
119 /// the edge pulls in what lies outside it), blurs them, and writes
120 /// them back inside the shape. A backend that cannot read its target —
121 /// the CPU raster, anything predating the kind — draws nothing, which
122 /// leaves the node over an unblurred backdrop.
123 Backdrop,
124}
125
126impl QuadKind {
127 /// Every kind, in discriminant order — what the conformance report's
128 /// `kinds` line counts and the C header's `KUI_QUAD_*` mirror.
129 pub const ALL: [QuadKind; 10] = [
130 QuadKind::Solid,
131 QuadKind::GlyphMask,
132 QuadKind::GlyphColor,
133 QuadKind::Image,
134 QuadKind::GlyphSubpixel,
135 QuadKind::Shadow,
136 QuadKind::Segment,
137 QuadKind::Fragment,
138 QuadKind::Texture,
139 QuadKind::Backdrop,
140 ];
141}
142
143#[repr(C)]
144#[derive(Clone, Copy, Debug)]
145pub struct Quad {
146 /// Physical pixels.
147 pub rect: Rect,
148 pub color: Color,
149 pub border_color: Color,
150 /// Corner radii in physical pixels, clockwise from the top-left:
151 /// `[tl, tr, br, bl]`.
152 pub radius: [f32; 4],
153 pub border_w: f32,
154 /// `QuadKind::Shadow` only: the blur radius in physical pixels, which
155 /// is also how far `rect` is inflated past the shape being blurred.
156 /// Zero elsewhere.
157 pub blur: f32,
158 pub kind: QuadKind,
159 /// Which entry of [`DisplayList::clips`] clips this quad: pixels
160 /// outside that rect — and outside its rounded corners, when it has
161 /// any — are discarded.
162 ///
163 /// An index rather than the clip itself because a clip is thirty-two
164 /// bytes and a frame has a handful of them: every quad under one card
165 /// names the same entry, and a frame that clips nothing names one
166 /// entry from every quad it has. Carrying the rect and its four radii
167 /// on the quad cost 32 of the 124 bytes each, on a struct written once
168 /// per quad and then walked again by the fade pass, the backend's
169 /// upload and the previous frame `depart` keeps. The same reasoning
170 /// put a fragment's parameters in [`DisplayList::fragments`].
171 pub clip: ClipId,
172 /// Atlas texels: x, y, w, h. For [`QuadKind::Segment`] the two
173 /// endpoints instead, as `f32` bits (see [`Quad::segment_ends`]).
174 pub uv: [u32; 4],
175}
176
177/// An index into [`DisplayList::clips`]. Every quad has one; there is no
178/// "no clip" value, because a frame that clips nothing still names an
179/// entry — [`Clip::NONE`] scaled — and a backend that reads it needs no
180/// special case.
181pub type ClipId = u32;
182
183/// The entry every frame's clip table starts with: [`Clip::NONE`] in
184/// physical pixels. Emission seeds it before any quad is made, so a quad
185/// that is clipped by nothing — most quads of most frames — names this
186/// without interning anything.
187pub const NO_CLIP_ID: ClipId = 0;
188
189impl Quad {
190 /// A [`QuadKind::Segment`]'s endpoints, `[x0, y0, x1, y1]` in physical
191 /// px, decoded from the bits `uv` carries. Meaningless for any other
192 /// kind.
193 pub fn segment_ends(&self) -> [f32; 4] {
194 self.uv.map(f32::from_bits)
195 }
196
197 /// The `uv` a [`QuadKind::Segment`] carries for these endpoints.
198 pub fn segment_uv(ends: [f32; 4]) -> [u32; 4] {
199 ends.map(f32::to_bits)
200 }
201}
202
203/// A clip that clips nothing.
204pub const NO_CLIP: Rect = Rect {
205 x: -1e9,
206 y: -1e9,
207 w: 2e9,
208 h: 2e9,
209};
210
211/// Radii that round nothing.
212pub const SQUARE: [f32; 4] = [0.0; 4];
213
214/// The clip a node inherits: a rect, and the radii to round its corners by.
215///
216/// A node that clips (`clip`, `scroll_x`, `scroll_y`) and has a `radius`
217/// rounds what it clips — the way CSS rounds `overflow: hidden` under a
218/// `border-radius` — so the children of a rounded card stay inside its
219/// corners instead of poking out of them. Nothing declares this: the radii
220/// are the clipping node's own.
221///
222/// One rounded rect cannot name the intersection of two, so nesting is
223/// approximated by [`Clip::intersect`], which says what it gives up.
224///
225/// An entry is the *space* a quad is painted in, not only what cuts it
226/// (ADR 0043): `transform` is the similarity every quad naming it is drawn
227/// through — the identity on every entry of a frame with no `rotate` or
228/// `scale` — and `inner` is a second clip in the quad's own space, before
229/// the transform, from clipping nodes inside a turned subtree. `rect` and
230/// `radius` stay the clip in framebuffer space, from clipping nodes
231/// outside any turn. A backend that reads only `rect` and `radius` draws
232/// a turned subtree upright and cut by its outer clip, which is what every
233/// backend did before.
234#[repr(C)]
235#[derive(Clone, Copy, Debug, PartialEq)]
236pub struct Clip {
237 pub rect: Rect,
238 /// Clockwise from the top-left: `[tl, tr, br, bl]`. All zero = a plain
239 /// rect clip.
240 pub radius: [f32; 4],
241 /// The turn, scale and move every quad naming this entry is drawn
242 /// through: `pixel = R(angle) · scale · p + (tx, ty)` for `p` a point
243 /// of the quad's own rect. The identity when nothing turns.
244 pub transform: Transform,
245 /// The clip in the quad's own space, before `transform`: what a
246 /// clipping node inside a turned subtree cuts. [`NO_CLIP`] when
247 /// nothing inside the turn clips, and on every entry of a frame with
248 /// no turn.
249 pub inner: Rect,
250 /// `inner`'s corner radii, as `radius` is `rect`'s.
251 pub inner_radius: [f32; 4],
252}
253
254impl Clip {
255 /// A clip that clips nothing.
256 pub const NONE: Clip = Clip {
257 rect: NO_CLIP,
258 radius: SQUARE,
259 transform: Transform::IDENTITY,
260 inner: NO_CLIP,
261 inner_radius: SQUARE,
262 };
263
264 /// A plain rect clip.
265 pub fn rect(rect: Rect) -> Self {
266 Self { rect, ..Self::NONE }
267 }
268
269 /// Logical to physical pixels.
270 pub fn scaled(&self, s: f32) -> Clip {
271 Clip {
272 rect: self.rect.scaled(s),
273 radius: self.radius.map(|r| r * s),
274 transform: self.transform.scaled(s),
275 inner: self.inner.scaled(s),
276 inner_radius: self.inner_radius.map(|r| r * s),
277 }
278 }
279
280 /// Whether every quad naming this entry is drawn through a turn or a
281 /// scale.
282 #[inline]
283 pub fn turned(&self) -> bool {
284 !self.transform.is_identity()
285 }
286
287 /// This space, entered by a node that turns by `own` (about its
288 /// pivot, in this space's own coordinates): the transform composes,
289 /// the outer clip is untouched, and the inner clip is pulled back
290 /// into the new space as its bounding box there, its corners square
291 /// (ADR 0043, decision 4: a clip between two nested turns is
292 /// approximated).
293 pub fn turned_by(&self, own: Transform) -> Clip {
294 let inner = if self.inner == NO_CLIP {
295 NO_CLIP
296 } else {
297 own.unbounds(self.inner)
298 };
299 Clip {
300 rect: self.rect,
301 radius: self.radius,
302 transform: own.then(&self.transform),
303 inner,
304 inner_radius: SQUARE,
305 }
306 }
307
308 /// The rect, in the quad's own space, outside which nothing it draws
309 /// can show: `rect` itself when nothing turns, else the inner clip
310 /// narrowed by the outer one pulled back through the transform. What
311 /// a glyph cull and a visibility test read, since both compare
312 /// positions in the quad's space.
313 #[inline]
314 pub fn visible(&self) -> Rect {
315 if !self.turned() {
316 return self.rect;
317 }
318 let outer = if self.rect == NO_CLIP {
319 NO_CLIP
320 } else {
321 self.transform.unbounds(self.rect)
322 };
323 self.inner.intersect(&outer)
324 }
325
326 /// Where `rect`, a box in the quad's own space, shows on screen:
327 /// itself cut to the clip when nothing turns, else the bounding box
328 /// of its turned shape, cut to the outer clip. What the access tree
329 /// reports (ADR 0043, decision 7).
330 pub fn shown(&self, rect: Rect) -> Rect {
331 if !self.turned() {
332 return rect.intersect(&self.rect);
333 }
334 self.transform
335 .bounds(rect.intersect(&self.inner))
336 .intersect(&self.rect)
337 }
338
339 /// This clip narrowed by a clipping node's box and that node's radii.
340 ///
341 /// The rect is the plain intersection, as it has always been. The radii
342 /// are decided per corner: a corner takes whichever of the two shapes
343 /// rounds it *more* (the intersection of two rounded corners is the
344 /// tighter one), and only while that corner of the result is still the
345 /// same point as that corner of the shape it came from — a corner an
346 /// ancestor's straight edge has already cut away is square, which is
347 /// what that ancestor made it.
348 ///
349 /// The one case it approximates: an ancestor edge that cuts *partway*
350 /// into a rounded corner moves that corner, so its radius drops to zero
351 /// and a sliver at the very corner goes unclipped. A second clipper
352 /// offset from the first, both rounded, is the shape that does it.
353 ///
354 /// Under a turn the box is in the quad's own space, so it narrows the
355 /// inner clip and leaves the framebuffer one alone (ADR 0043).
356 pub fn intersect(&self, box_rect: Rect, box_radius: [f32; 4]) -> Clip {
357 if self.turned() {
358 let (inner, inner_radius) = narrow(self.inner, self.inner_radius, box_rect, box_radius);
359 return Clip {
360 inner,
361 inner_radius,
362 ..*self
363 };
364 }
365 let (rect, radius) = narrow(self.rect, self.radius, box_rect, box_radius);
366 Clip {
367 rect,
368 radius,
369 ..*self
370 }
371 }
372}
373
374/// One rounded rect narrowed by another: the plain intersection, and per
375/// corner whichever of the two rounds it more while that corner is still
376/// its own (see [`Clip::intersect`]).
377fn narrow(rect: Rect, radius: [f32; 4], box_rect: Rect, box_radius: [f32; 4]) -> (Rect, [f32; 4]) {
378 let out = rect.intersect(&box_rect);
379 if radius == SQUARE && box_radius == SQUARE {
380 return (out, SQUARE);
381 }
382 let mut r = SQUARE;
383 for (i, c) in r.iter_mut().enumerate() {
384 let mine = surviving(out, rect, radius[i], i);
385 let theirs = surviving(out, box_rect, box_radius[i], i);
386 *c = mine.max(theirs);
387 }
388 (out, r)
389}
390
391/// `radius`, if corner `i` of `rect` is still corner `i` of `src`; else 0.
392/// Corners run clockwise from the top-left, like the radii.
393fn surviving(rect: Rect, src: Rect, radius: f32, i: usize) -> f32 {
394 if radius <= 0.0 {
395 return 0.0;
396 }
397 let (dx, dy) = match i {
398 0 => (rect.x - src.x, rect.y - src.y),
399 1 => (rect.x + rect.w - src.x - src.w, rect.y - src.y),
400 2 => (
401 rect.x + rect.w - src.x - src.w,
402 rect.y + rect.h - src.y - src.h,
403 ),
404 _ => (rect.x - src.x, rect.y + rect.h - src.y - src.h),
405 };
406 // The common case is exact — the intersection kept the whole box; the
407 // epsilon is for the sub-pixel drift a laid-out rect can carry.
408 if dx.abs() < 0.01 && dy.abs() < 0.01 {
409 radius
410 } else {
411 0.0
412 }
413}
414
415/// What a [`QuadKind::Fragment`] quad points at: which registered WGSL
416/// paints it, and the sixteen numbers that frame passes it.
417///
418/// It rides beside the quads rather than on them because `Quad` is copied
419/// twice per node on a 10,000-node frame and 68 more bytes on it would be
420/// paid by every quad of every frame, for a kind almost none of them are
421/// (the same reasoning puts a segment's endpoints in
422/// `uv`). A frame that draws no fragment leaves the vector empty.
423#[derive(Clone, Copy, Debug, PartialEq)]
424pub struct FragmentDraw {
425 pub id: crate::resources::FragmentId,
426 /// Positional, app-defined; the view's `params` zero-padded to
427 /// sixteen. The shader reads them as four `vec4<f32>`.
428 pub params: [f32; 16],
429 /// The image the function samples through `kui_sample`, resolved to
430 /// where its texels are this frame.
431 pub image: FragmentImage,
432}
433
434/// Where a fragment's `image` row lands for one frame: nowhere, in the
435/// glyph atlas the fragment pipeline already has bound, or in a texture
436/// of the image's own that the backend binds in the atlas's place for
437/// that one quad — the same swap a [`QuadKind::Texture`] quad asks for.
438/// The core decides between the last two on the image's backing, so a
439/// backend meets the same two cases it already draws.
440#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
441pub enum FragmentImage {
442 /// No `image` row: `kui_sample` returns transparent black.
443 #[default]
444 None,
445 /// The image sits in the atlas at this texel rect, `[x, y, w, h]`.
446 Atlas([u32; 4]),
447 /// The image has a texture of its own: `index` names the entry of
448 /// [`DisplayList::textures`] (and `texture_pixels`) that carries it,
449 /// `uv` is the texel rect in that texture — the whole image.
450 Texture { index: u32, uv: [u32; 4] },
451}
452
453impl FragmentImage {
454 /// The texel rect the shader reads as `FragmentIn::image`; zero with
455 /// no image.
456 pub fn uv(self) -> [u32; 4] {
457 match self {
458 FragmentImage::None => [0; 4],
459 FragmentImage::Atlas(uv) | FragmentImage::Texture { uv, .. } => uv,
460 }
461 }
462}
463
464/// What a [`QuadKind::Texture`] quad points at: which registered image,
465/// and the texel rect of it to show (the whole image, or the crop a
466/// `fit="cover"` made). Beside the quads for the reason [`FragmentDraw`]
467/// is: a handle and a rect on every quad would be paid by the 20,000 that
468/// are not one. A frame that draws no texture-backed image leaves the
469/// vector empty.
470#[derive(Clone, Copy, Debug, PartialEq, Eq)]
471pub struct TextureDraw {
472 pub id: crate::resources::ImageId,
473 /// `[x, y, w, h]` in the texture's own texels.
474 pub uv: [u32; 4],
475}
476
477/// A texture-backed image's pixels as a frame hands them to a backend;
478/// see [`DisplayList::texture_pixels`].
479#[derive(Clone, Debug)]
480pub struct TexturePixels {
481 pub width: u32,
482 pub height: u32,
483 /// Moves with every `update_image`; a backend that uploaded this
484 /// revision has nothing to do.
485 pub rev: u32,
486 pub rgba: std::sync::Arc<Vec<u8>>,
487}
488
489#[derive(Default)]
490pub struct DisplayList {
491 pub quads: Vec<Quad>,
492 /// The clips the quads name, in physical pixels. One entry per
493 /// *distinct* clip a frame reaches — a handful, even on a frame of
494 /// ten thousand quads, because a clip is inherited and only a clipping
495 /// node makes a new one. Empty only on a frame that drew nothing.
496 pub clips: Vec<Clip>,
497 /// One entry per [`QuadKind::Fragment`] quad, indexed by its `uv[0]`.
498 /// Empty on a frame that draws none.
499 pub fragments: Vec<FragmentDraw>,
500 /// The WGSL behind each entry of [`Self::fragments`], at the same
501 /// index: what a backend compiles the first time it meets a handle.
502 /// It rides here rather than on `FragmentDraw` so that struct stays
503 /// `Copy` and digestible; an `Arc` clone per fragment quad is a
504 /// refcount bump, and a frame with no fragment has neither vector.
505 pub fragment_sources: Vec<std::sync::Arc<str>>,
506 /// One entry per [`QuadKind::Texture`] quad, indexed by its `uv[0]`.
507 /// Empty on a frame that draws none.
508 pub textures: Vec<TextureDraw>,
509 /// The pixels behind each entry of [`Self::textures`], at the same
510 /// index: what a backend uploads the first time it meets a handle, and
511 /// again whenever `rev` has moved. Shared with the resource entry, so
512 /// this is a refcount per texture quad and no copy — the reason
513 /// `fragment_sources` rides here the same way.
514 pub texture_pixels: Vec<TexturePixels>,
515 /// Image handles removed since the last frame whose backing was a
516 /// texture: what a backend drops from its cache. Cleared with the
517 /// quads, so a host that renders one list a frame sees each once —
518 /// and a removal is carried by one window's list, whichever drew
519 /// next after it, since the device the cache lives on is shared by
520 /// every window of the session.
521 pub dropped_textures: Vec<crate::resources::ImageId>,
522 /// Fragment handles removed since the last frame: what a backend
523 /// drops the pipelines it built for. Carried the same way.
524 pub dropped_fragments: Vec<crate::resources::FragmentId>,
525 /// Physical pixels.
526 pub viewport: Size,
527 pub scale: f32,
528 /// The frame clock in seconds — the same one transitions read, as the
529 /// driver last set it. A backend hands it to a fragment as
530 /// `FragmentIn::time`; nothing else reads it. Zero when the driver
531 /// never set a clock, which is what a headless frame looks like.
532 pub time: f32,
533}
534
535impl DisplayList {
536 pub fn clear(&mut self) {
537 self.quads.clear();
538 self.clips.clear();
539 self.fragments.clear();
540 self.fragment_sources.clear();
541 self.textures.clear();
542 self.texture_pixels.clear();
543 self.dropped_textures.clear();
544 self.dropped_fragments.clear();
545 }
546
547 /// The clip a quad names. Out of range — which a well-formed frame
548 /// never is — reads as clipping nothing, so a malformed list draws
549 /// rather than panics.
550 pub fn clip_of(&self, q: &Quad) -> Clip {
551 self.clips
552 .get(q.clip as usize)
553 .copied()
554 .unwrap_or(Clip::NONE)
555 }
556
557 /// Interns a clip and returns its index. See [`intern_clip`].
558 pub fn intern_clip(&mut self, clip: Clip) -> ClipId {
559 intern_clip(&mut self.clips, clip)
560 }
561}
562
563/// Interns a clip into a frame's table and returns the index a quad names.
564///
565/// Only the last entry is compared, so this is a constant-time append with
566/// a run-length check and not a real intern: a clip that comes back after
567/// another one gets a second entry. That is deliberate. Emission runs in
568/// paint order, so equal clips arrive in runs, and the alternative — a scan
569/// of the whole table — is quadratic on the one frame shape that makes many
570/// clips (a screen of width-clamped labels, which narrows the clip once per
571/// label). A duplicate costs thirty-two bytes on a list that is orders of
572/// magnitude shorter than the quads; a quadratic scan costs the frame.
573///
574/// Callers avoid most of the calls entirely: a node whose clip is its
575/// parent's reuses the index the parent interned without comparing anything
576/// (`Core::emit_frame`).
577pub fn intern_clip(clips: &mut Vec<Clip>, clip: Clip) -> ClipId {
578 if let Some(last) = clips.last()
579 && *last == clip
580 {
581 return clips.len() as ClipId - 1;
582 }
583 clips.push(clip);
584 clips.len() as ClipId - 1
585}