Skip to main content

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}