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}
111
112impl QuadKind {
113    /// Every kind, in discriminant order — what the conformance report's
114    /// `kinds` line counts and the C header's `KUI_QUAD_*` mirror.
115    pub const ALL: [QuadKind; 9] = [
116        QuadKind::Solid,
117        QuadKind::GlyphMask,
118        QuadKind::GlyphColor,
119        QuadKind::Image,
120        QuadKind::GlyphSubpixel,
121        QuadKind::Shadow,
122        QuadKind::Segment,
123        QuadKind::Fragment,
124        QuadKind::Texture,
125    ];
126}
127
128#[repr(C)]
129#[derive(Clone, Copy, Debug)]
130pub struct Quad {
131    /// Physical pixels.
132    pub rect: Rect,
133    pub color: Color,
134    pub border_color: Color,
135    /// Corner radii in physical pixels, clockwise from the top-left:
136    /// `[tl, tr, br, bl]`.
137    pub radius: [f32; 4],
138    pub border_w: f32,
139    /// `QuadKind::Shadow` only: the blur radius in physical pixels, which
140    /// is also how far `rect` is inflated past the shape being blurred.
141    /// Zero elsewhere.
142    pub blur: f32,
143    pub kind: QuadKind,
144    /// Which entry of [`DisplayList::clips`] clips this quad: pixels
145    /// outside that rect — and outside its rounded corners, when it has
146    /// any — are discarded.
147    ///
148    /// An index rather than the clip itself because a clip is thirty-two
149    /// bytes and a frame has a handful of them: every quad under one card
150    /// names the same entry, and a frame that clips nothing names one
151    /// entry from every quad it has. Carrying the rect and its four radii
152    /// on the quad cost 32 of the 124 bytes each, on a struct written once
153    /// per quad and then walked again by the fade pass, the backend's
154    /// upload and the previous frame `depart` keeps. The same reasoning
155    /// put a fragment's parameters in [`DisplayList::fragments`].
156    pub clip: ClipId,
157    /// Atlas texels: x, y, w, h. For [`QuadKind::Segment`] the two
158    /// endpoints instead, as `f32` bits (see [`Quad::segment_ends`]).
159    pub uv: [u32; 4],
160}
161
162/// An index into [`DisplayList::clips`]. Every quad has one; there is no
163/// "no clip" value, because a frame that clips nothing still names an
164/// entry — [`Clip::NONE`] scaled — and a backend that reads it needs no
165/// special case.
166pub type ClipId = u32;
167
168/// The entry every frame's clip table starts with: [`Clip::NONE`] in
169/// physical pixels. Emission seeds it before any quad is made, so a quad
170/// that is clipped by nothing — most quads of most frames — names this
171/// without interning anything.
172pub const NO_CLIP_ID: ClipId = 0;
173
174impl Quad {
175    /// A [`QuadKind::Segment`]'s endpoints, `[x0, y0, x1, y1]` in physical
176    /// px, decoded from the bits `uv` carries. Meaningless for any other
177    /// kind.
178    pub fn segment_ends(&self) -> [f32; 4] {
179        self.uv.map(f32::from_bits)
180    }
181
182    /// The `uv` a [`QuadKind::Segment`] carries for these endpoints.
183    pub fn segment_uv(ends: [f32; 4]) -> [u32; 4] {
184        ends.map(f32::to_bits)
185    }
186}
187
188/// A clip that clips nothing.
189pub const NO_CLIP: Rect = Rect {
190    x: -1e9,
191    y: -1e9,
192    w: 2e9,
193    h: 2e9,
194};
195
196/// Radii that round nothing.
197pub const SQUARE: [f32; 4] = [0.0; 4];
198
199/// The clip a node inherits: a rect, and the radii to round its corners by.
200///
201/// A node that clips (`clip`, `scroll_x`, `scroll_y`) and has a `radius`
202/// rounds what it clips — the way CSS rounds `overflow: hidden` under a
203/// `border-radius` — so the children of a rounded card stay inside its
204/// corners instead of poking out of them. Nothing declares this: the radii
205/// are the clipping node's own.
206///
207/// One rounded rect cannot name the intersection of two, so nesting is
208/// approximated by [`Clip::intersect`], which says what it gives up.
209#[repr(C)]
210#[derive(Clone, Copy, Debug, PartialEq)]
211pub struct Clip {
212    pub rect: Rect,
213    /// Clockwise from the top-left: `[tl, tr, br, bl]`. All zero = a plain
214    /// rect clip.
215    pub radius: [f32; 4],
216}
217
218impl Clip {
219    /// A clip that clips nothing.
220    pub const NONE: Clip = Clip {
221        rect: NO_CLIP,
222        radius: SQUARE,
223    };
224
225    /// A plain rect clip.
226    pub fn rect(rect: Rect) -> Self {
227        Self {
228            rect,
229            radius: SQUARE,
230        }
231    }
232
233    /// Logical to physical pixels.
234    pub fn scaled(&self, s: f32) -> Clip {
235        Clip {
236            rect: self.rect.scaled(s),
237            radius: self.radius.map(|r| r * s),
238        }
239    }
240
241    /// This clip narrowed by a clipping node's box and that node's radii.
242    ///
243    /// The rect is the plain intersection, as it has always been. The radii
244    /// are decided per corner: a corner takes whichever of the two shapes
245    /// rounds it *more* (the intersection of two rounded corners is the
246    /// tighter one), and only while that corner of the result is still the
247    /// same point as that corner of the shape it came from — a corner an
248    /// ancestor's straight edge has already cut away is square, which is
249    /// what that ancestor made it.
250    ///
251    /// The one case it approximates: an ancestor edge that cuts *partway*
252    /// into a rounded corner moves that corner, so its radius drops to zero
253    /// and a sliver at the very corner goes unclipped. A second clipper
254    /// offset from the first, both rounded, is the shape that does it.
255    pub fn intersect(&self, box_rect: Rect, box_radius: [f32; 4]) -> Clip {
256        let rect = self.rect.intersect(&box_rect);
257        if self.radius == SQUARE && box_radius == SQUARE {
258            return Clip::rect(rect);
259        }
260        let mut radius = SQUARE;
261        for (i, r) in radius.iter_mut().enumerate() {
262            let mine = surviving(rect, self.rect, self.radius[i], i);
263            let theirs = surviving(rect, box_rect, box_radius[i], i);
264            *r = mine.max(theirs);
265        }
266        Clip { rect, radius }
267    }
268}
269
270/// `radius`, if corner `i` of `rect` is still corner `i` of `src`; else 0.
271/// Corners run clockwise from the top-left, like the radii.
272fn surviving(rect: Rect, src: Rect, radius: f32, i: usize) -> f32 {
273    if radius <= 0.0 {
274        return 0.0;
275    }
276    let (dx, dy) = match i {
277        0 => (rect.x - src.x, rect.y - src.y),
278        1 => (rect.x + rect.w - src.x - src.w, rect.y - src.y),
279        2 => (
280            rect.x + rect.w - src.x - src.w,
281            rect.y + rect.h - src.y - src.h,
282        ),
283        _ => (rect.x - src.x, rect.y + rect.h - src.y - src.h),
284    };
285    // The common case is exact — the intersection kept the whole box; the
286    // epsilon is for the sub-pixel drift a laid-out rect can carry.
287    if dx.abs() < 0.01 && dy.abs() < 0.01 {
288        radius
289    } else {
290        0.0
291    }
292}
293
294/// What a [`QuadKind::Fragment`] quad points at: which registered WGSL
295/// paints it, and the sixteen numbers that frame passes it.
296///
297/// It rides beside the quads rather than on them because `Quad` is copied
298/// twice per node on a 10,000-node frame and 68 more bytes on it would be
299/// paid by every quad of every frame, for a kind almost none of them are
300/// (the same reasoning puts a segment's endpoints in
301/// `uv`). A frame that draws no fragment leaves the vector empty.
302#[derive(Clone, Copy, Debug, PartialEq)]
303pub struct FragmentDraw {
304    pub id: crate::resources::FragmentId,
305    /// Positional, app-defined; the view's `params` zero-padded to
306    /// sixteen. The shader reads them as four `vec4<f32>`.
307    pub params: [f32; 16],
308    /// The image the function samples through `kui_sample`, resolved to
309    /// where its texels are this frame.
310    pub image: FragmentImage,
311}
312
313/// Where a fragment's `image` row lands for one frame: nowhere, in the
314/// glyph atlas the fragment pipeline already has bound, or in a texture
315/// of the image's own that the backend binds in the atlas's place for
316/// that one quad — the same swap a [`QuadKind::Texture`] quad asks for.
317/// The core decides between the last two on the image's backing, so a
318/// backend meets the same two cases it already draws.
319#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
320pub enum FragmentImage {
321    /// No `image` row: `kui_sample` returns transparent black.
322    #[default]
323    None,
324    /// The image sits in the atlas at this texel rect, `[x, y, w, h]`.
325    Atlas([u32; 4]),
326    /// The image has a texture of its own: `index` names the entry of
327    /// [`DisplayList::textures`] (and `texture_pixels`) that carries it,
328    /// `uv` is the texel rect in that texture — the whole image.
329    Texture { index: u32, uv: [u32; 4] },
330}
331
332impl FragmentImage {
333    /// The texel rect the shader reads as `FragmentIn::image`; zero with
334    /// no image.
335    pub fn uv(self) -> [u32; 4] {
336        match self {
337            FragmentImage::None => [0; 4],
338            FragmentImage::Atlas(uv) | FragmentImage::Texture { uv, .. } => uv,
339        }
340    }
341}
342
343/// What a [`QuadKind::Texture`] quad points at: which registered image,
344/// and the texel rect of it to show (the whole image, or the crop a
345/// `fit="cover"` made). Beside the quads for the reason [`FragmentDraw`]
346/// is: a handle and a rect on every quad would be paid by the 20,000 that
347/// are not one. A frame that draws no texture-backed image leaves the
348/// vector empty.
349#[derive(Clone, Copy, Debug, PartialEq, Eq)]
350pub struct TextureDraw {
351    pub id: crate::resources::ImageId,
352    /// `[x, y, w, h]` in the texture's own texels.
353    pub uv: [u32; 4],
354}
355
356/// A texture-backed image's pixels as a frame hands them to a backend;
357/// see [`DisplayList::texture_pixels`].
358#[derive(Clone, Debug)]
359pub struct TexturePixels {
360    pub width: u32,
361    pub height: u32,
362    /// Moves with every `update_image`; a backend that uploaded this
363    /// revision has nothing to do.
364    pub rev: u32,
365    pub rgba: std::sync::Arc<Vec<u8>>,
366}
367
368#[derive(Default)]
369pub struct DisplayList {
370    pub quads: Vec<Quad>,
371    /// The clips the quads name, in physical pixels. One entry per
372    /// *distinct* clip a frame reaches — a handful, even on a frame of
373    /// ten thousand quads, because a clip is inherited and only a clipping
374    /// node makes a new one. Empty only on a frame that drew nothing.
375    pub clips: Vec<Clip>,
376    /// One entry per [`QuadKind::Fragment`] quad, indexed by its `uv[0]`.
377    /// Empty on a frame that draws none.
378    pub fragments: Vec<FragmentDraw>,
379    /// The WGSL behind each entry of [`Self::fragments`], at the same
380    /// index: what a backend compiles the first time it meets a handle.
381    /// It rides here rather than on `FragmentDraw` so that struct stays
382    /// `Copy` and digestible; an `Arc` clone per fragment quad is a
383    /// refcount bump, and a frame with no fragment has neither vector.
384    pub fragment_sources: Vec<std::sync::Arc<str>>,
385    /// One entry per [`QuadKind::Texture`] quad, indexed by its `uv[0]`.
386    /// Empty on a frame that draws none.
387    pub textures: Vec<TextureDraw>,
388    /// The pixels behind each entry of [`Self::textures`], at the same
389    /// index: what a backend uploads the first time it meets a handle, and
390    /// again whenever `rev` has moved. Shared with the resource entry, so
391    /// this is a refcount per texture quad and no copy — the reason
392    /// `fragment_sources` rides here the same way.
393    pub texture_pixels: Vec<TexturePixels>,
394    /// Image handles removed since the last frame whose backing was a
395    /// texture: what a backend drops from its cache. Cleared with the
396    /// quads, so a host that renders one list a frame sees each once —
397    /// and a removal is carried by one window's list, whichever drew
398    /// next after it, since the device the cache lives on is shared by
399    /// every window of the session.
400    pub dropped_textures: Vec<crate::resources::ImageId>,
401    /// Fragment handles removed since the last frame: what a backend
402    /// drops the pipelines it built for. Carried the same way.
403    pub dropped_fragments: Vec<crate::resources::FragmentId>,
404    /// Physical pixels.
405    pub viewport: Size,
406    pub scale: f32,
407    /// The frame clock in seconds — the same one transitions read, as the
408    /// driver last set it. A backend hands it to a fragment as
409    /// `FragmentIn::time`; nothing else reads it. Zero when the driver
410    /// never set a clock, which is what a headless frame looks like.
411    pub time: f32,
412}
413
414impl DisplayList {
415    pub fn clear(&mut self) {
416        self.quads.clear();
417        self.clips.clear();
418        self.fragments.clear();
419        self.fragment_sources.clear();
420        self.textures.clear();
421        self.texture_pixels.clear();
422        self.dropped_textures.clear();
423        self.dropped_fragments.clear();
424    }
425
426    /// The clip a quad names. Out of range — which a well-formed frame
427    /// never is — reads as clipping nothing, so a malformed list draws
428    /// rather than panics.
429    pub fn clip_of(&self, q: &Quad) -> Clip {
430        self.clips
431            .get(q.clip as usize)
432            .copied()
433            .unwrap_or(Clip::NONE)
434    }
435
436    /// Interns a clip and returns its index. See [`intern_clip`].
437    pub fn intern_clip(&mut self, clip: Clip) -> ClipId {
438        intern_clip(&mut self.clips, clip)
439    }
440}
441
442/// Interns a clip into a frame's table and returns the index a quad names.
443///
444/// Only the last entry is compared, so this is a constant-time append with
445/// a run-length check and not a real intern: a clip that comes back after
446/// another one gets a second entry. That is deliberate. Emission runs in
447/// paint order, so equal clips arrive in runs, and the alternative — a scan
448/// of the whole table — is quadratic on the one frame shape that makes many
449/// clips (a screen of width-clamped labels, which narrows the clip once per
450/// label). A duplicate costs thirty-two bytes on a list that is orders of
451/// magnitude shorter than the quads; a quadratic scan costs the frame.
452///
453/// Callers avoid most of the calls entirely: a node whose clip is its
454/// parent's reuses the index the parent interned without comparing anything
455/// (`Core::emit_frame`).
456pub fn intern_clip(clips: &mut Vec<Clip>, clip: Clip) -> ClipId {
457    if let Some(last) = clips.last()
458        && *last == clip
459    {
460        return clips.len() as ClipId - 1;
461    }
462    clips.push(clip);
463    clips.len() as ClipId - 1
464}