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};
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#[repr(C)]
225#[derive(Clone, Copy, Debug, PartialEq)]
226pub struct Clip {
227 pub rect: Rect,
228 /// Clockwise from the top-left: `[tl, tr, br, bl]`. All zero = a plain
229 /// rect clip.
230 pub radius: [f32; 4],
231}
232
233impl Clip {
234 /// A clip that clips nothing.
235 pub const NONE: Clip = Clip {
236 rect: NO_CLIP,
237 radius: SQUARE,
238 };
239
240 /// A plain rect clip.
241 pub fn rect(rect: Rect) -> Self {
242 Self {
243 rect,
244 radius: SQUARE,
245 }
246 }
247
248 /// Logical to physical pixels.
249 pub fn scaled(&self, s: f32) -> Clip {
250 Clip {
251 rect: self.rect.scaled(s),
252 radius: self.radius.map(|r| r * s),
253 }
254 }
255
256 /// This clip narrowed by a clipping node's box and that node's radii.
257 ///
258 /// The rect is the plain intersection, as it has always been. The radii
259 /// are decided per corner: a corner takes whichever of the two shapes
260 /// rounds it *more* (the intersection of two rounded corners is the
261 /// tighter one), and only while that corner of the result is still the
262 /// same point as that corner of the shape it came from — a corner an
263 /// ancestor's straight edge has already cut away is square, which is
264 /// what that ancestor made it.
265 ///
266 /// The one case it approximates: an ancestor edge that cuts *partway*
267 /// into a rounded corner moves that corner, so its radius drops to zero
268 /// and a sliver at the very corner goes unclipped. A second clipper
269 /// offset from the first, both rounded, is the shape that does it.
270 pub fn intersect(&self, box_rect: Rect, box_radius: [f32; 4]) -> Clip {
271 let rect = self.rect.intersect(&box_rect);
272 if self.radius == SQUARE && box_radius == SQUARE {
273 return Clip::rect(rect);
274 }
275 let mut radius = SQUARE;
276 for (i, r) in radius.iter_mut().enumerate() {
277 let mine = surviving(rect, self.rect, self.radius[i], i);
278 let theirs = surviving(rect, box_rect, box_radius[i], i);
279 *r = mine.max(theirs);
280 }
281 Clip { rect, radius }
282 }
283}
284
285/// `radius`, if corner `i` of `rect` is still corner `i` of `src`; else 0.
286/// Corners run clockwise from the top-left, like the radii.
287fn surviving(rect: Rect, src: Rect, radius: f32, i: usize) -> f32 {
288 if radius <= 0.0 {
289 return 0.0;
290 }
291 let (dx, dy) = match i {
292 0 => (rect.x - src.x, rect.y - src.y),
293 1 => (rect.x + rect.w - src.x - src.w, rect.y - src.y),
294 2 => (
295 rect.x + rect.w - src.x - src.w,
296 rect.y + rect.h - src.y - src.h,
297 ),
298 _ => (rect.x - src.x, rect.y + rect.h - src.y - src.h),
299 };
300 // The common case is exact — the intersection kept the whole box; the
301 // epsilon is for the sub-pixel drift a laid-out rect can carry.
302 if dx.abs() < 0.01 && dy.abs() < 0.01 {
303 radius
304 } else {
305 0.0
306 }
307}
308
309/// What a [`QuadKind::Fragment`] quad points at: which registered WGSL
310/// paints it, and the sixteen numbers that frame passes it.
311///
312/// It rides beside the quads rather than on them because `Quad` is copied
313/// twice per node on a 10,000-node frame and 68 more bytes on it would be
314/// paid by every quad of every frame, for a kind almost none of them are
315/// (the same reasoning puts a segment's endpoints in
316/// `uv`). A frame that draws no fragment leaves the vector empty.
317#[derive(Clone, Copy, Debug, PartialEq)]
318pub struct FragmentDraw {
319 pub id: crate::resources::FragmentId,
320 /// Positional, app-defined; the view's `params` zero-padded to
321 /// sixteen. The shader reads them as four `vec4<f32>`.
322 pub params: [f32; 16],
323 /// The image the function samples through `kui_sample`, resolved to
324 /// where its texels are this frame.
325 pub image: FragmentImage,
326}
327
328/// Where a fragment's `image` row lands for one frame: nowhere, in the
329/// glyph atlas the fragment pipeline already has bound, or in a texture
330/// of the image's own that the backend binds in the atlas's place for
331/// that one quad — the same swap a [`QuadKind::Texture`] quad asks for.
332/// The core decides between the last two on the image's backing, so a
333/// backend meets the same two cases it already draws.
334#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
335pub enum FragmentImage {
336 /// No `image` row: `kui_sample` returns transparent black.
337 #[default]
338 None,
339 /// The image sits in the atlas at this texel rect, `[x, y, w, h]`.
340 Atlas([u32; 4]),
341 /// The image has a texture of its own: `index` names the entry of
342 /// [`DisplayList::textures`] (and `texture_pixels`) that carries it,
343 /// `uv` is the texel rect in that texture — the whole image.
344 Texture { index: u32, uv: [u32; 4] },
345}
346
347impl FragmentImage {
348 /// The texel rect the shader reads as `FragmentIn::image`; zero with
349 /// no image.
350 pub fn uv(self) -> [u32; 4] {
351 match self {
352 FragmentImage::None => [0; 4],
353 FragmentImage::Atlas(uv) | FragmentImage::Texture { uv, .. } => uv,
354 }
355 }
356}
357
358/// What a [`QuadKind::Texture`] quad points at: which registered image,
359/// and the texel rect of it to show (the whole image, or the crop a
360/// `fit="cover"` made). Beside the quads for the reason [`FragmentDraw`]
361/// is: a handle and a rect on every quad would be paid by the 20,000 that
362/// are not one. A frame that draws no texture-backed image leaves the
363/// vector empty.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub struct TextureDraw {
366 pub id: crate::resources::ImageId,
367 /// `[x, y, w, h]` in the texture's own texels.
368 pub uv: [u32; 4],
369}
370
371/// A texture-backed image's pixels as a frame hands them to a backend;
372/// see [`DisplayList::texture_pixels`].
373#[derive(Clone, Debug)]
374pub struct TexturePixels {
375 pub width: u32,
376 pub height: u32,
377 /// Moves with every `update_image`; a backend that uploaded this
378 /// revision has nothing to do.
379 pub rev: u32,
380 pub rgba: std::sync::Arc<Vec<u8>>,
381}
382
383#[derive(Default)]
384pub struct DisplayList {
385 pub quads: Vec<Quad>,
386 /// The clips the quads name, in physical pixels. One entry per
387 /// *distinct* clip a frame reaches — a handful, even on a frame of
388 /// ten thousand quads, because a clip is inherited and only a clipping
389 /// node makes a new one. Empty only on a frame that drew nothing.
390 pub clips: Vec<Clip>,
391 /// One entry per [`QuadKind::Fragment`] quad, indexed by its `uv[0]`.
392 /// Empty on a frame that draws none.
393 pub fragments: Vec<FragmentDraw>,
394 /// The WGSL behind each entry of [`Self::fragments`], at the same
395 /// index: what a backend compiles the first time it meets a handle.
396 /// It rides here rather than on `FragmentDraw` so that struct stays
397 /// `Copy` and digestible; an `Arc` clone per fragment quad is a
398 /// refcount bump, and a frame with no fragment has neither vector.
399 pub fragment_sources: Vec<std::sync::Arc<str>>,
400 /// One entry per [`QuadKind::Texture`] quad, indexed by its `uv[0]`.
401 /// Empty on a frame that draws none.
402 pub textures: Vec<TextureDraw>,
403 /// The pixels behind each entry of [`Self::textures`], at the same
404 /// index: what a backend uploads the first time it meets a handle, and
405 /// again whenever `rev` has moved. Shared with the resource entry, so
406 /// this is a refcount per texture quad and no copy — the reason
407 /// `fragment_sources` rides here the same way.
408 pub texture_pixels: Vec<TexturePixels>,
409 /// Image handles removed since the last frame whose backing was a
410 /// texture: what a backend drops from its cache. Cleared with the
411 /// quads, so a host that renders one list a frame sees each once —
412 /// and a removal is carried by one window's list, whichever drew
413 /// next after it, since the device the cache lives on is shared by
414 /// every window of the session.
415 pub dropped_textures: Vec<crate::resources::ImageId>,
416 /// Fragment handles removed since the last frame: what a backend
417 /// drops the pipelines it built for. Carried the same way.
418 pub dropped_fragments: Vec<crate::resources::FragmentId>,
419 /// Physical pixels.
420 pub viewport: Size,
421 pub scale: f32,
422 /// The frame clock in seconds — the same one transitions read, as the
423 /// driver last set it. A backend hands it to a fragment as
424 /// `FragmentIn::time`; nothing else reads it. Zero when the driver
425 /// never set a clock, which is what a headless frame looks like.
426 pub time: f32,
427}
428
429impl DisplayList {
430 pub fn clear(&mut self) {
431 self.quads.clear();
432 self.clips.clear();
433 self.fragments.clear();
434 self.fragment_sources.clear();
435 self.textures.clear();
436 self.texture_pixels.clear();
437 self.dropped_textures.clear();
438 self.dropped_fragments.clear();
439 }
440
441 /// The clip a quad names. Out of range — which a well-formed frame
442 /// never is — reads as clipping nothing, so a malformed list draws
443 /// rather than panics.
444 pub fn clip_of(&self, q: &Quad) -> Clip {
445 self.clips
446 .get(q.clip as usize)
447 .copied()
448 .unwrap_or(Clip::NONE)
449 }
450
451 /// Interns a clip and returns its index. See [`intern_clip`].
452 pub fn intern_clip(&mut self, clip: Clip) -> ClipId {
453 intern_clip(&mut self.clips, clip)
454 }
455}
456
457/// Interns a clip into a frame's table and returns the index a quad names.
458///
459/// Only the last entry is compared, so this is a constant-time append with
460/// a run-length check and not a real intern: a clip that comes back after
461/// another one gets a second entry. That is deliberate. Emission runs in
462/// paint order, so equal clips arrive in runs, and the alternative — a scan
463/// of the whole table — is quadratic on the one frame shape that makes many
464/// clips (a screen of width-clamped labels, which narrows the clip once per
465/// label). A duplicate costs thirty-two bytes on a list that is orders of
466/// magnitude shorter than the quads; a quadratic scan costs the frame.
467///
468/// Callers avoid most of the calls entirely: a node whose clip is its
469/// parent's reuses the index the parent interned without comparing anything
470/// (`Core::emit_frame`).
471pub fn intern_clip(clips: &mut Vec<Clip>, clip: Clip) -> ClipId {
472 if let Some(last) = clips.last()
473 && *last == clip
474 {
475 return clips.len() as ClipId - 1;
476 }
477 clips.push(clip);
478 clips.len() as ClipId - 1
479}