smudge_core/paint.rs
1//! Paint operations and the display list that renderers replay.
2//!
3//! Widgets emit [`PaintOp`]s in paint coordinates: logical window
4//! coordinates with the scroll offsets of the enclosing scroll views left
5//! out, so that scrolling moves existing operations instead of rebuilding
6//! them (see [`crate::widget::Widget::paint`]). Outside a scrolled subtree
7//! the two coincide. Y grows downwards and the origin is the top left of the
8//! window. The frame collects the operations into one [`DisplayList`] in
9//! paint order, and both renderers replay that list.
10//!
11//! A display list may carry [`DisplayItem::PushOffset`] scopes. Items inside a
12//! scope are drawn translated by the offset, so a scrolled subtree can keep
13//! its operations in its own coordinates while the window scrolls. A renderer
14//! that replays a list must honor them.
15//!
16//! A renderer skips items whose bounds miss the damaged area, so
17//! [`PaintOp::bounds`] must cover every pixel an operation can change.
18
19use std::rc::Rc;
20use std::sync::Arc;
21
22use crate::geometry::{Color, Corners, Point, Rect};
23
24/// The bytes of one font file, shared by every glyph run that uses it.
25///
26/// Glyph runs carry the font as plain bytes so that paint operations never
27/// depend on a particular shaping library. `id` identifies the data uniquely
28/// for the lifetime of the process, which renderers use as a glyph cache key
29/// without hashing the bytes. `index` selects one face inside a font file that
30/// holds several.
31#[derive(Clone)]
32pub struct FontBlob {
33 /// The unique identifier of the font data within this process.
34 pub id: u64,
35 /// The bytes of the font file.
36 pub data: Arc<dyn AsRef<[u8]> + Send + Sync>,
37 /// The face to use when the file holds a collection of fonts.
38 pub index: u32,
39}
40
41impl PartialEq for FontBlob {
42 fn eq(&self, other: &Self) -> bool {
43 self.id == other.id && self.index == other.index
44 }
45}
46
47impl std::fmt::Debug for FontBlob {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("FontBlob")
50 .field("id", &self.id)
51 .field("index", &self.index)
52 .field("len", &self.data.as_ref().as_ref().len())
53 .finish()
54 }
55}
56
57/// One glyph of a [`GlyphRun`], positioned in the run's paint coordinates.
58#[derive(Copy, Clone, Debug, PartialEq)]
59pub struct PositionedGlyph {
60 /// The glyph identifier inside the run's font, as shaping produced it.
61 pub id: u32,
62 /// Horizontal position of the glyph's baseline origin.
63 pub x: f32,
64 /// Vertical position of the glyph's baseline origin.
65 pub y: f32,
66}
67
68/// A shaped run of glyphs that all use one font, size and color.
69///
70/// Glyph positions are in the run's paint coordinates, which leave out the
71/// scroll offsets of enclosing views (see [`crate::widget::Widget::paint`]),
72/// with each glyph's `x` and `y` at its baseline origin. A renderer
73/// rasterizes each glyph separately and blits it in its run's color, so
74/// nothing else about the text, such as clusters or line breaking, travels
75/// with the run.
76#[derive(Clone, Debug, PartialEq)]
77pub struct GlyphRun {
78 /// The font the glyph identifiers refer to.
79 pub font: FontBlob,
80 /// The font size in logical pixels.
81 pub size: f32,
82 /// The straight-alpha sRGB color the glyphs paint with.
83 pub color: Color,
84 /// The glyphs of the run, in paint order.
85 pub glyphs: Vec<PositionedGlyph>,
86 /// The rectangle in the run's paint coordinates that covers the ink of
87 /// every glyph, plus one logical pixel on every side for anti-aliasing.
88 ///
89 /// A renderer may rely on it as the area the run can change: ink outside
90 /// it is not guaranteed to be drawn.
91 pub ink_bounds: Rect,
92}
93
94/// A single drawing operation in paint coordinates, which leave out the
95/// scroll offsets of enclosing views (see [`crate::widget::Widget::paint`]).
96#[derive(Clone, Debug, PartialEq)]
97#[non_exhaustive]
98pub enum PaintOp {
99 /// A filled rectangle with optional rounded corners.
100 FillRect {
101 /// Area the fill covers.
102 rect: Rect,
103 /// Corner radii of the filled shape.
104 radius: Corners,
105 /// Straight-alpha sRGB fill color.
106 color: Color,
107 },
108 /// A border drawn inside `rect`, so it never extends past the rectangle.
109 StrokeRect {
110 /// Outer edge of the border.
111 rect: Rect,
112 /// Corner radii of the border.
113 radius: Corners,
114 /// Border thickness in logical pixels, measured inwards from the edge
115 /// of `rect`.
116 width: f32,
117 /// Straight-alpha sRGB border color.
118 color: Color,
119 },
120 /// A shaped run of glyphs, painted in one font, size and color.
121 ///
122 /// The run is shared in an `Rc` so that rebuilding the display list for a
123 /// frame in which text did not change copies no glyph vectors.
124 Glyphs(Rc<GlyphRun>),
125 /// A premultiplied RGBA image stretched over a rounded rectangle.
126 Image {
127 /// Area the image stretches over.
128 rect: Rect,
129 /// Corner radii of the rounded rectangle the image clips to.
130 radius: Corners,
131 /// The pixels to draw, shared with the element that shows them.
132 image: crate::image::Image,
133 },
134}
135
136impl PaintOp {
137 /// Returns the rectangle in the operation's paint coordinates that this
138 /// operation can change.
139 ///
140 /// The bounds cover the whole operation, including a stroke, which is drawn
141 /// inside its rectangle. Anti-aliasing can shade pixels the edge passes
142 /// through, which is why damage converts these bounds to physical pixels by
143 /// rounding outwards rather than to the nearest pixel.
144 pub fn bounds(&self) -> Rect {
145 match self {
146 PaintOp::FillRect { rect, .. } | PaintOp::StrokeRect { rect, .. } => *rect,
147 PaintOp::Glyphs(run) => run.ink_bounds,
148 PaintOp::Image { rect, .. } => *rect,
149 }
150 }
151}
152
153/// One entry in a [`DisplayList`].
154#[derive(Clone, Debug, PartialEq)]
155pub enum DisplayItem {
156 /// A drawing operation.
157 Op(PaintOp),
158 /// Clips every following item to this rectangle until the matching
159 /// [`DisplayItem::PopClip`].
160 PushClip(Rect),
161 /// Restores the clip that was active before the matching
162 /// [`DisplayItem::PushClip`].
163 PopClip,
164 /// Draws every following item translated by this offset until the
165 /// matching [`DisplayItem::PopOffset`].
166 ///
167 /// Offsets nest with clips and with each other: an item's window position
168 /// is its own position plus the sum of the open offsets. Clips pushed
169 /// inside an offset scope hold their rectangle in the scope's coordinates,
170 /// so translating the scope moves the clip with the items it cuts.
171 PushOffset(Point),
172 /// Restores the offset that was active before the matching
173 /// [`DisplayItem::PushOffset`].
174 PopOffset,
175}
176
177/// A flat list of display items in paint order, ready for a renderer to replay.
178///
179/// The list is rebuilt from the widget tree whenever a node paints. Clips are
180/// nested, so every [`DisplayItem::PushClip`] needs a later
181/// [`DisplayItem::PopClip`], which [`DisplayList::finish`] checks in debug
182/// builds. Offsets nest the same way with [`DisplayItem::PushOffset`] and
183/// [`DisplayItem::PopOffset`].
184#[derive(Clone, Debug, Default, PartialEq)]
185pub struct DisplayList {
186 items: Vec<DisplayItem>,
187}
188
189impl DisplayList {
190 /// Creates an empty list.
191 ///
192 /// ```
193 /// use smudge_core::geometry::{Color, Corners, Rect};
194 /// use smudge_core::paint::{DisplayList, PaintOp};
195 ///
196 /// let mut list = DisplayList::new();
197 /// list.push_op(PaintOp::FillRect {
198 /// rect: Rect::new(0.0, 0.0, 8.0, 8.0),
199 /// radius: Corners::ZERO,
200 /// color: Color::BLACK,
201 /// });
202 /// list.finish();
203 /// assert_eq!(list.len(), 1);
204 /// ```
205 pub fn new() -> DisplayList {
206 DisplayList::default()
207 }
208
209 /// Removes every item, leaving the list empty and ready for a new build.
210 pub fn clear(&mut self) {
211 self.items.clear();
212 }
213
214 /// Appends one drawing operation.
215 pub fn push_op(&mut self, op: PaintOp) {
216 self.items.push(DisplayItem::Op(op));
217 }
218
219 /// Appends several drawing operations in order.
220 pub fn extend_ops(&mut self, ops: &[PaintOp]) {
221 self.items.extend(ops.iter().cloned().map(DisplayItem::Op));
222 }
223
224 /// Starts a clip to `rect`, which applies to every following item until the
225 /// matching [`DisplayList::pop_clip`].
226 pub fn push_clip(&mut self, rect: Rect) {
227 self.items.push(DisplayItem::PushClip(rect));
228 }
229
230 /// Ends the most recent clip.
231 ///
232 /// Callers must pair every call with an earlier
233 /// [`DisplayList::push_clip`]. [`DisplayList::finish`] reports an imbalance
234 /// in debug builds.
235 pub fn pop_clip(&mut self) {
236 self.items.push(DisplayItem::PopClip);
237 }
238
239 /// Starts an offset scope, which translates every following item by
240 /// `offset` until the matching [`DisplayList::pop_offset`].
241 ///
242 /// Scopes nest with clips and with each other. A clip pushed inside a
243 /// scope holds its rectangle in the scope's coordinates, so it moves with
244 /// the scope.
245 pub fn push_offset(&mut self, offset: Point) {
246 self.items.push(DisplayItem::PushOffset(offset));
247 }
248
249 /// Ends the most recent offset scope.
250 ///
251 /// Callers must pair every call with an earlier
252 /// [`DisplayList::push_offset`]. [`DisplayList::finish`] reports an
253 /// imbalance in debug builds.
254 pub fn pop_offset(&mut self) {
255 self.items.push(DisplayItem::PopOffset);
256 }
257
258 /// Returns the items in paint order.
259 pub fn items(&self) -> &[DisplayItem] {
260 &self.items
261 }
262
263 /// Returns the number of items, counting clips.
264 pub fn len(&self) -> usize {
265 self.items.len()
266 }
267
268 /// Returns `true` when the list holds no item.
269 pub fn is_empty(&self) -> bool {
270 self.items.is_empty()
271 }
272
273 /// Completes a build of the list.
274 ///
275 /// The frame calls this once after rebuilding the list and before handing
276 /// it to a renderer. In release builds it does nothing, because an
277 /// unbalanced list is an internal invariant violation rather than input
278 /// from outside the library.
279 ///
280 /// # Panics
281 ///
282 /// Panics in debug builds when a [`DisplayItem::PushClip`] has no matching
283 /// [`DisplayItem::PopClip`], when a `PopClip` appears with no clip open,
284 /// or when an offset scope is unbalanced the same way.
285 pub fn finish(&mut self) {
286 #[cfg(debug_assertions)]
287 self.debug_assert_balanced();
288 }
289
290 /// Panics unless every clip and offset scope was pushed before it was
291 /// popped.
292 #[cfg(debug_assertions)]
293 fn debug_assert_balanced(&self) {
294 let mut open = 0usize;
295 let mut open_offsets = 0usize;
296 for item in &self.items {
297 match item {
298 DisplayItem::PushClip(_) => open += 1,
299 DisplayItem::PopClip => {
300 debug_assert!(open > 0, "PopClip with no open clip");
301 open = open.saturating_sub(1);
302 }
303 DisplayItem::PushOffset(_) => open_offsets += 1,
304 DisplayItem::PopOffset => {
305 debug_assert!(open_offsets > 0, "PopOffset with no open offset");
306 open_offsets = open_offsets.saturating_sub(1);
307 }
308 DisplayItem::Op(_) => {}
309 }
310 }
311 debug_assert_eq!(open, 0, "PushClip with no matching PopClip");
312 debug_assert_eq!(open_offsets, 0, "PushOffset with no matching PopOffset");
313 }
314}
315
316#[cfg(test)]
317#[path = "paint_tests.rs"]
318mod tests;