kui_core/display.rs
1//! The renderer boundary: a flat list of quads in physical pixels. A backend
2//! needs exactly two abilities — draw these quads, and mirror the glyph atlas
3//! to a texture. Everything else (layout, shaping, styling) happened already.
4
5use crate::color::Color;
6use crate::geom::{Rect, Size};
7
8#[repr(u32)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum QuadKind {
11 /// Rounded rect with optional border; ignores uv.
12 Solid,
13 /// Alpha-mask glyph: atlas alpha times `color`.
14 GlyphMask,
15 /// Color bitmap glyph (emoji): atlas rgb, alpha times `color.a`.
16 GlyphColor,
17 /// Registered image blitted into the atlas: atlas rgba tinted by
18 /// `color` (white = as-is), rounded by `radius` like a solid.
19 /// `border_w` is the `sampling` flag — 0 linear, 1 nearest (ADR 0025,
20 /// decision 4) — a slot this kind had no other use for; `blur` is 0.
21 Image,
22 /// LCD subpixel glyph: atlas rgb is per-channel coverage (times
23 /// `color.a`), `color.rgb` the text color. Needs per-channel (dual
24 /// source) blending; a backend without it treats the atlas alpha as a
25 /// plain mask.
26 GlyphSubpixel,
27 /// Drop shadow: `color` filling a rounded rect inset from `rect` by
28 /// `blur` on every side, its edge ramped over `blur` px. The core
29 /// emits it just before the node's own quads, already offset and
30 /// spread, so a backend only has to soften the SDF it already
31 /// computes. Ignores `uv`, `border_color` and `border_w`.
32 Shadow,
33 /// A round-capped stroke between two endpoints
34 /// (`docs/adr/0010-a-segment-primitive.md`). `uv` holds the endpoints
35 /// as `[x0, y0, x1, y1]` in physical px, each an `f32` stored through
36 /// `to_bits` — [`Quad::segment_ends`] reads them back — `border_w` is
37 /// the stroke width and `color` the stroke. `rect` is the bounding
38 /// box, the endpoints inflated by half the width plus two logical px
39 /// so the edge ramp is never cut by the quad's own edge. A backend
40 /// evaluates an SDF capsule against the fragment's position. Ignores
41 /// `radius`, `border_color` and `blur`.
42 Segment,
43 /// A box a host-registered WGSL function paints
44 /// (`docs/adr/0015-a-fragment-element-and-the-painter-it-is-not.md`).
45 /// `uv[0]` indexes [`DisplayList::fragments`], which carries the
46 /// handle and the sixteen parameters; the other three words of `uv`
47 /// are zero. `rect`, `radius`, `clip` and `clip_radius` are the
48 /// node's and mean what they mean everywhere else — the backend
49 /// rounds and clips a fragment exactly as it rounds and clips a
50 /// solid. `color` carries the group opacity in its alpha and nothing
51 /// else (`rgb` is zero), because a fragment returns its own colour
52 /// and a faded subtree still has to fade it. `border_color`,
53 /// `border_w` and `blur` are zero and ignored. A backend that cannot
54 /// draw one — anything predating this kind — draws nothing, which is
55 /// what a missing handle does too.
56 Fragment,
57 /// A registered image drawn from a texture of its own rather than the
58 /// atlas (`docs/adr/0025-the-image-is-the-canvas.md`): one that did
59 /// not fit a page, or whose pixels the app has replaced. `uv[0]`
60 /// indexes [`DisplayList::textures`], which carries the handle and the
61 /// texel rect *in that texture*; the other three words of `uv` are
62 /// zero. Everything else is what an `Image` quad's is — `rect`,
63 /// `radius`, `clip`, `color` as a tint (white = as-is) with the group
64 /// opacity in its alpha, and `border_w` the sampling flag. A backend
65 /// binds the texture in place of the atlas for the run and draws it
66 /// as an `Image`; one that predates the kind draws nothing.
67 Texture,
68}
69
70impl QuadKind {
71 /// Every kind, in discriminant order — what the conformance report's
72 /// `kinds` line counts and the C header's `KUI_QUAD_*` mirror.
73 pub const ALL: [QuadKind; 9] = [
74 QuadKind::Solid,
75 QuadKind::GlyphMask,
76 QuadKind::GlyphColor,
77 QuadKind::Image,
78 QuadKind::GlyphSubpixel,
79 QuadKind::Shadow,
80 QuadKind::Segment,
81 QuadKind::Fragment,
82 QuadKind::Texture,
83 ];
84}
85
86#[repr(C)]
87#[derive(Clone, Copy, Debug)]
88pub struct Quad {
89 /// Physical pixels.
90 pub rect: Rect,
91 pub color: Color,
92 pub border_color: Color,
93 /// Corner radii in physical pixels, clockwise from the top-left:
94 /// `[tl, tr, br, bl]`.
95 pub radius: [f32; 4],
96 pub border_w: f32,
97 /// `QuadKind::Shadow` only: the blur radius in physical pixels, which
98 /// is also how far `rect` is inflated past the shape being blurred.
99 /// Zero elsewhere.
100 pub blur: f32,
101 pub kind: QuadKind,
102 /// Which entry of [`DisplayList::clips`] clips this quad: pixels
103 /// outside that rect — and outside its rounded corners, when it has
104 /// any — are discarded.
105 ///
106 /// An index rather than the clip itself because a clip is thirty-two
107 /// bytes and a frame has a handful of them: every quad under one card
108 /// names the same entry, and a frame that clips nothing names one
109 /// entry from every quad it has. Carrying the rect and its four radii
110 /// on the quad cost 32 of the 124 bytes each, on a struct written once
111 /// per quad and then walked again by the fade pass, the backend's
112 /// upload and the previous frame `depart` keeps. The same reasoning
113 /// put a fragment's parameters in [`DisplayList::fragments`].
114 pub clip: ClipId,
115 /// Atlas texels: x, y, w, h. For [`QuadKind::Segment`] the two
116 /// endpoints instead, as `f32` bits (see [`Quad::segment_ends`]).
117 pub uv: [u32; 4],
118}
119
120/// An index into [`DisplayList::clips`]. Every quad has one; there is no
121/// "no clip" value, because a frame that clips nothing still names an
122/// entry — [`Clip::NONE`] scaled — and a backend that reads it needs no
123/// special case.
124pub type ClipId = u32;
125
126/// The entry every frame's clip table starts with: [`Clip::NONE`] in
127/// physical pixels. Emission seeds it before any quad is made, so a quad
128/// that is clipped by nothing — most quads of most frames — names this
129/// without interning anything.
130pub const NO_CLIP_ID: ClipId = 0;
131
132impl Quad {
133 /// A [`QuadKind::Segment`]'s endpoints, `[x0, y0, x1, y1]` in physical
134 /// px, decoded from the bits `uv` carries. Meaningless for any other
135 /// kind.
136 pub fn segment_ends(&self) -> [f32; 4] {
137 self.uv.map(f32::from_bits)
138 }
139
140 /// The `uv` a [`QuadKind::Segment`] carries for these endpoints.
141 pub fn segment_uv(ends: [f32; 4]) -> [u32; 4] {
142 ends.map(f32::to_bits)
143 }
144}
145
146/// A clip that clips nothing.
147pub const NO_CLIP: Rect = Rect {
148 x: -1e9,
149 y: -1e9,
150 w: 2e9,
151 h: 2e9,
152};
153
154/// Radii that round nothing.
155pub const SQUARE: [f32; 4] = [0.0; 4];
156
157/// The clip a node inherits: a rect, and the radii to round its corners by.
158///
159/// A node that clips (`clip`, `scroll_x`, `scroll_y`) and has a `radius`
160/// rounds what it clips — the way CSS rounds `overflow: hidden` under a
161/// `border-radius` — so the children of a rounded card stay inside its
162/// corners instead of poking out of them. Nothing declares this: the radii
163/// are the clipping node's own.
164///
165/// One rounded rect cannot name the intersection of two, so nesting is
166/// approximated by [`Clip::intersect`], which says what it gives up.
167#[repr(C)]
168#[derive(Clone, Copy, Debug, PartialEq)]
169pub struct Clip {
170 pub rect: Rect,
171 /// Clockwise from the top-left: `[tl, tr, br, bl]`. All zero = a plain
172 /// rect clip.
173 pub radius: [f32; 4],
174}
175
176impl Clip {
177 /// A clip that clips nothing.
178 pub const NONE: Clip = Clip {
179 rect: NO_CLIP,
180 radius: SQUARE,
181 };
182
183 /// A plain rect clip.
184 pub fn rect(rect: Rect) -> Self {
185 Self {
186 rect,
187 radius: SQUARE,
188 }
189 }
190
191 /// Logical to physical pixels.
192 pub fn scaled(&self, s: f32) -> Clip {
193 Clip {
194 rect: self.rect.scaled(s),
195 radius: self.radius.map(|r| r * s),
196 }
197 }
198
199 /// This clip narrowed by a clipping node's box and that node's radii.
200 ///
201 /// The rect is the plain intersection, as it has always been. The radii
202 /// are decided per corner: a corner takes whichever of the two shapes
203 /// rounds it *more* (the intersection of two rounded corners is the
204 /// tighter one), and only while that corner of the result is still the
205 /// same point as that corner of the shape it came from — a corner an
206 /// ancestor's straight edge has already cut away is square, which is
207 /// what that ancestor made it.
208 ///
209 /// The one case it approximates: an ancestor edge that cuts *partway*
210 /// into a rounded corner moves that corner, so its radius drops to zero
211 /// and a sliver at the very corner goes unclipped. A second clipper
212 /// offset from the first, both rounded, is the shape that does it.
213 pub fn intersect(&self, box_rect: Rect, box_radius: [f32; 4]) -> Clip {
214 let rect = self.rect.intersect(&box_rect);
215 if self.radius == SQUARE && box_radius == SQUARE {
216 return Clip::rect(rect);
217 }
218 let mut radius = SQUARE;
219 for (i, r) in radius.iter_mut().enumerate() {
220 let mine = surviving(rect, self.rect, self.radius[i], i);
221 let theirs = surviving(rect, box_rect, box_radius[i], i);
222 *r = mine.max(theirs);
223 }
224 Clip { rect, radius }
225 }
226}
227
228/// `radius`, if corner `i` of `rect` is still corner `i` of `src`; else 0.
229/// Corners run clockwise from the top-left, like the radii.
230fn surviving(rect: Rect, src: Rect, radius: f32, i: usize) -> f32 {
231 if radius <= 0.0 {
232 return 0.0;
233 }
234 let (dx, dy) = match i {
235 0 => (rect.x - src.x, rect.y - src.y),
236 1 => (rect.x + rect.w - src.x - src.w, rect.y - src.y),
237 2 => (
238 rect.x + rect.w - src.x - src.w,
239 rect.y + rect.h - src.y - src.h,
240 ),
241 _ => (rect.x - src.x, rect.y + rect.h - src.y - src.h),
242 };
243 // The common case is exact — the intersection kept the whole box; the
244 // epsilon is for the sub-pixel drift a laid-out rect can carry.
245 if dx.abs() < 0.01 && dy.abs() < 0.01 {
246 radius
247 } else {
248 0.0
249 }
250}
251
252/// What a [`QuadKind::Fragment`] quad points at: which registered WGSL
253/// paints it, and the sixteen numbers that frame passes it.
254///
255/// It rides beside the quads rather than on them because `Quad` is copied
256/// twice per node on a 10,000-node frame and 68 more bytes on it would be
257/// paid by every quad of every frame, for a kind almost none of them are
258/// (C15, and ADR 0010's reasoning for putting a segment's endpoints in
259/// `uv`). A frame that draws no fragment leaves the vector empty.
260#[derive(Clone, Copy, Debug, PartialEq)]
261pub struct FragmentDraw {
262 pub id: crate::resources::FragmentId,
263 /// Positional, app-defined; the view's `params` zero-padded to
264 /// sixteen. The shader reads them as four `vec4<f32>`.
265 pub params: [f32; 16],
266 /// The image the function samples through `kui_sample`, resolved to
267 /// where its texels are this frame (backlog V1, ADR 0025 decision 7).
268 pub image: FragmentImage,
269}
270
271/// Where a fragment's `image` row lands for one frame: nowhere, in the
272/// glyph atlas the fragment pipeline already has bound, or in a texture
273/// of the image's own that the backend binds in the atlas's place for
274/// that one quad — the same swap a [`QuadKind::Texture`] quad asks for.
275/// The core decides between the last two on the image's backing, so a
276/// backend meets the same two cases it already draws.
277#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
278pub enum FragmentImage {
279 /// No `image` row: `kui_sample` returns transparent black.
280 #[default]
281 None,
282 /// The image sits in the atlas at this texel rect, `[x, y, w, h]`.
283 Atlas([u32; 4]),
284 /// The image has a texture of its own: `index` names the entry of
285 /// [`DisplayList::textures`] (and `texture_pixels`) that carries it,
286 /// `uv` is the texel rect in that texture — the whole image.
287 Texture { index: u32, uv: [u32; 4] },
288}
289
290impl FragmentImage {
291 /// The texel rect the shader reads as `FragmentIn::image`; zero with
292 /// no image.
293 pub fn uv(self) -> [u32; 4] {
294 match self {
295 FragmentImage::None => [0; 4],
296 FragmentImage::Atlas(uv) | FragmentImage::Texture { uv, .. } => uv,
297 }
298 }
299}
300
301/// What a [`QuadKind::Texture`] quad points at: which registered image,
302/// and the texel rect of it to show (the whole image, or the crop a
303/// `fit="cover"` made). Beside the quads for the reason [`FragmentDraw`]
304/// is: a handle and a rect on every quad would be paid by the 20,000 that
305/// are not one. A frame that draws no texture-backed image leaves the
306/// vector empty.
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub struct TextureDraw {
309 pub id: crate::resources::ImageId,
310 /// `[x, y, w, h]` in the texture's own texels.
311 pub uv: [u32; 4],
312}
313
314/// A texture-backed image's pixels as a frame hands them to a backend;
315/// see [`DisplayList::texture_pixels`].
316#[derive(Clone, Debug)]
317pub struct TexturePixels {
318 pub width: u32,
319 pub height: u32,
320 /// Moves with every `update_image`; a backend that uploaded this
321 /// revision has nothing to do.
322 pub rev: u32,
323 pub rgba: std::sync::Arc<Vec<u8>>,
324}
325
326#[derive(Default)]
327pub struct DisplayList {
328 pub quads: Vec<Quad>,
329 /// The clips the quads name, in physical pixels. One entry per
330 /// *distinct* clip a frame reaches — a handful, even on a frame of
331 /// ten thousand quads, because a clip is inherited and only a clipping
332 /// node makes a new one. Empty only on a frame that drew nothing.
333 pub clips: Vec<Clip>,
334 /// One entry per [`QuadKind::Fragment`] quad, indexed by its `uv[0]`.
335 /// Empty on a frame that draws none.
336 pub fragments: Vec<FragmentDraw>,
337 /// The WGSL behind each entry of [`Self::fragments`], at the same
338 /// index: what a backend compiles the first time it meets a handle.
339 /// It rides here rather than on `FragmentDraw` so that struct stays
340 /// `Copy` and digestible; an `Arc` clone per fragment quad is a
341 /// refcount bump, and a frame with no fragment has neither vector.
342 pub fragment_sources: Vec<std::sync::Arc<str>>,
343 /// One entry per [`QuadKind::Texture`] quad, indexed by its `uv[0]`.
344 /// Empty on a frame that draws none.
345 pub textures: Vec<TextureDraw>,
346 /// The pixels behind each entry of [`Self::textures`], at the same
347 /// index: what a backend uploads the first time it meets a handle, and
348 /// again whenever `rev` has moved. Shared with the resource entry, so
349 /// this is a refcount per texture quad and no copy — the reason
350 /// `fragment_sources` rides here the same way.
351 pub texture_pixels: Vec<TexturePixels>,
352 /// Image handles removed since the last frame whose backing was a
353 /// texture: what a backend drops from its cache. Cleared with the
354 /// quads, so a host that renders one list a frame sees each once —
355 /// and a removal is carried by one window's list, whichever drew
356 /// next after it, since the device the cache lives on is shared by
357 /// every window of the session (AR8).
358 pub dropped_textures: Vec<crate::resources::ImageId>,
359 /// Fragment handles removed since the last frame: what a backend
360 /// drops the pipelines it built for. Carried the same way.
361 pub dropped_fragments: Vec<crate::resources::FragmentId>,
362 /// Physical pixels.
363 pub viewport: Size,
364 pub scale: f32,
365 /// The frame clock in seconds — the same one transitions read, as the
366 /// driver last set it. A backend hands it to a fragment as
367 /// `FragmentIn::time`; nothing else reads it. Zero when the driver
368 /// never set a clock, which is what a headless frame looks like.
369 pub time: f32,
370}
371
372impl DisplayList {
373 pub fn clear(&mut self) {
374 self.quads.clear();
375 self.clips.clear();
376 self.fragments.clear();
377 self.fragment_sources.clear();
378 self.textures.clear();
379 self.texture_pixels.clear();
380 self.dropped_textures.clear();
381 self.dropped_fragments.clear();
382 }
383
384 /// The clip a quad names. Out of range — which a well-formed frame
385 /// never is — reads as clipping nothing, so a malformed list draws
386 /// rather than panics.
387 pub fn clip_of(&self, q: &Quad) -> Clip {
388 self.clips
389 .get(q.clip as usize)
390 .copied()
391 .unwrap_or(Clip::NONE)
392 }
393
394 /// Interns a clip and returns its index. See [`intern_clip`].
395 pub fn intern_clip(&mut self, clip: Clip) -> ClipId {
396 intern_clip(&mut self.clips, clip)
397 }
398}
399
400/// Interns a clip into a frame's table and returns the index a quad names.
401///
402/// Only the last entry is compared, so this is a constant-time append with
403/// a run-length check and not a real intern: a clip that comes back after
404/// another one gets a second entry. That is deliberate. Emission runs in
405/// paint order, so equal clips arrive in runs, and the alternative — a scan
406/// of the whole table — is quadratic on the one frame shape that makes many
407/// clips (a screen of width-clamped labels, which narrows the clip once per
408/// label). A duplicate costs thirty-two bytes on a list that is orders of
409/// magnitude shorter than the quads; a quadratic scan costs the frame.
410///
411/// Callers avoid most of the calls entirely: a node whose clip is its
412/// parent's reuses the index the parent interned without comparing anything
413/// (`Core::emit_frame`).
414pub fn intern_clip(clips: &mut Vec<Clip>, clip: Clip) -> ClipId {
415 if let Some(last) = clips.last()
416 && *last == clip
417 {
418 return clips.len() as ClipId - 1;
419 }
420 clips.push(clip);
421 clips.len() as ClipId - 1
422}