Skip to main content

incular_rendering/
display_list.rs

1use crate::compositor::LayerId;
2use crate::effects::{BlendMode, ColorFilter, DropShadowEffect, GaussianBlur};
3use crate::geometry::RRect;
4use crate::glyphs::GlyphRun;
5use crate::gradients::Brush;
6use crate::paint::{Border, FillRule, Paint, PaintStyle, Stroke};
7use crate::paths::Path;
8use incular_core::{Color, Rect, Size, Transform};
9use incular_image::ImageHandle;
10use std::sync::Arc;
11
12/// Renderer-neutral identity for a detachable composition subtree.
13///
14/// The retained widget layer maps its generational transient-surface identity
15/// onto this value. Renderers treat partition markers as no-ops unless a host
16/// explicitly detaches the marked commands for presentation on another
17/// surface.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct SurfacePartitionId {
20    index: u32,
21    generation: u32,
22}
23
24impl SurfacePartitionId {
25    #[must_use]
26    pub const fn from_parts(index: u32, generation: u32) -> Self {
27        Self { index, generation }
28    }
29
30    #[must_use]
31    pub const fn index(self) -> u32 {
32        self.index
33    }
34
35    #[must_use]
36    pub const fn generation(self) -> u32 {
37        self.generation
38    }
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub enum PaintCommand {
43    /// Begins a retained subtree that may be detached to another presentation
44    /// surface. The marker itself has no visual effect.
45    PushSurfacePartition {
46        id: SurfacePartitionId,
47    },
48    /// Ends the most recent surface partition.
49    PopSurfacePartition,
50    Rect {
51        rect: Rect,
52        color: Color,
53    },
54    RRect {
55        rrect: RRect,
56        brush: Brush,
57    },
58    Border {
59        rrect: RRect,
60        border: Border,
61    },
62    FillPath {
63        path: Arc<Path>,
64        brush: Brush,
65        fill_rule: FillRule,
66    },
67    StrokePath {
68        path: Arc<Path>,
69        brush: Brush,
70        stroke: Stroke,
71    },
72    /// Source is image pixels; destination is local logical geometry.
73    Image {
74        image: ImageHandle,
75        source: Rect,
76        destination: Rect,
77        sampling: ImageSampling,
78    },
79    GlyphRun {
80        run: Arc<GlyphRun>,
81        color: Color,
82    },
83    PushClip {
84        rect: Rect,
85    },
86    PushClipRRect {
87        rrect: RRect,
88    },
89    PushClipOval {
90        rect: Rect,
91    },
92    PushClipPath {
93        path: Arc<Path>,
94        fill_rule: FillRule,
95    },
96    PopClip,
97    PushTransform {
98        transform: Transform,
99    },
100    PopTransform,
101    /// Begins an isolated retained opacity group. The compositor renders the
102    /// enclosed commands into a transparent target and applies `alpha` once
103    /// when that target is composited back into its parent.
104    PushOpacity {
105        layer: LayerId,
106        alpha: f32,
107        generation: u64,
108        bounds: Rect,
109    },
110    PopOpacity,
111    /// Begins an isolated Gaussian-filtered subtree. The generation and
112    /// bounds describe source pixels only; sigma is a compositor property.
113    PushBlur {
114        layer: LayerId,
115        blur: GaussianBlur,
116        generation: u64,
117        bounds: Rect,
118    },
119    /// Begins an isolated arbitrary-subtree drop shadow. The child source is
120    /// emitted once and the compositor draws the blurred alpha behind it.
121    PushDropShadow {
122        layer: LayerId,
123        shadow: DropShadowEffect,
124        generation: u64,
125        bounds: Rect,
126    },
127    /// Begins an isolated straight-RGBA color-matrix stage.
128    PushColorFilter {
129        layer: LayerId,
130        filter: ColorFilter,
131        generation: u64,
132        bounds: Rect,
133    },
134    /// Begins an isolated blend group. The source is emitted once and the
135    /// compositor combines it with the destination in painter order.
136    PushBlend {
137        layer: LayerId,
138        mode: BlendMode,
139        generation: u64,
140        bounds: Rect,
141    },
142    /// Begins an isolated shader-mask stage. The shader is evaluated in the
143    /// mask's logical coordinate space and blended over the child source.
144    PushShaderMask {
145        layer: LayerId,
146        shader: Brush,
147        blend_mode: BlendMode,
148        mask_size: Size,
149        mask_transform: Transform,
150        generation: u64,
151        bounds: Rect,
152    },
153    /// Begins a backdrop stage. The backend samples the already painted
154    /// destination in `bounds`, filters it, and then composites the filtered
155    /// pixels before the child source is painted.
156    PushBackdropFilter {
157        layer: LayerId,
158        blur: GaussianBlur,
159        blend_mode: BlendMode,
160        enabled: bool,
161        generation: u64,
162        bounds: Rect,
163    },
164    PopEffect,
165}
166/// Renderer-neutral raster filtering policy. Ordinary UI images default to
167/// linear sampling; pixel art can opt into nearest-neighbour sampling.
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
169pub enum ImageSampling {
170    #[default]
171    Linear,
172    Nearest,
173}
174#[derive(Clone, Debug, Default, PartialEq)]
175pub struct DisplayList {
176    commands: Vec<PaintCommand>,
177}
178impl DisplayList {
179    #[must_use]
180    pub const fn new() -> Self {
181        Self {
182            commands: Vec::new(),
183        }
184    }
185    pub fn push(&mut self, command: PaintCommand) {
186        if self.commands.len() == self.commands.capacity() {
187            // Retained pictures usually hold one or two commands. Doubling
188            // from one slot avoids Vec's four-command (576-byte) minimum.
189            self.commands.reserve_exact(self.commands.len().max(1));
190        }
191        self.commands.push(command);
192    }
193    pub fn extend_from(&mut self, other: &Self) {
194        self.commands.extend(other.commands.iter().cloned());
195    }
196
197    /// Splits selected retained surface partitions out of this display list.
198    ///
199    /// Commands inside an active selected partition are omitted from `parent`
200    /// and returned under that partition's stable identity. Nested selected
201    /// partitions are assigned to the deepest selected surface, so a submenu
202    /// can be detached independently from its owning menu. Partition markers
203    /// never reach either output.
204    #[must_use]
205    pub fn detach_surface_partitions(
206        &self,
207        selected: &std::collections::HashSet<SurfacePartitionId>,
208    ) -> (Self, std::collections::HashMap<SurfacePartitionId, Self>) {
209        let mut parent = Self::new();
210        let mut detached = std::collections::HashMap::<SurfacePartitionId, Self>::new();
211        let mut stack = Vec::<SurfacePartitionId>::new();
212        for command in &self.commands {
213            match command {
214                PaintCommand::PushSurfacePartition { id } => stack.push(*id),
215                PaintCommand::PopSurfacePartition => {
216                    let _ = stack.pop();
217                }
218                command => {
219                    let target = stack.iter().rev().copied().find(|id| selected.contains(id));
220                    if let Some(id) = target {
221                        detached.entry(id).or_default().push(command.clone());
222                    } else {
223                        parent.push(command.clone());
224                    }
225                }
226            }
227        }
228        (parent, detached)
229    }
230
231    /// Wraps this list in one logical translation without mutating retained
232    /// command geometry.
233    #[must_use]
234    pub fn translated(&self, offset: incular_core::Offset) -> Self {
235        if self.is_empty() || offset == incular_core::Offset::ZERO {
236            return self.clone();
237        }
238        let mut output = Self::new();
239        output.push(PaintCommand::PushTransform {
240            transform: Transform::translation(offset),
241        });
242        output.extend_from(self);
243        output.push(PaintCommand::PopTransform);
244        output
245    }
246
247    /// Conservatively proves that the first visual operation covers the entire
248    /// target with an opaque axis-aligned rectangle.
249    ///
250    /// This is intentionally stricter than a general coverage analysis. A
251    /// backend may use it before presenting through an opaque native surface;
252    /// returning `false` means "not proven" and must never be interpreted as
253    /// transparency being unnecessary.
254    #[must_use]
255    pub fn begins_with_opaque_surface_rect(&self, surface_size: Size) -> bool {
256        if !surface_size.width.is_finite()
257            || !surface_size.height.is_finite()
258            || surface_size.width <= 0.0
259            || surface_size.height <= 0.0
260        {
261            return false;
262        }
263        let mut transforms = vec![Transform::IDENTITY];
264        for command in &self.commands {
265            match command {
266                PaintCommand::PushTransform { transform } => {
267                    let next = transforms
268                        .last()
269                        .copied()
270                        .expect("root transform")
271                        .then(*transform);
272                    if !next.is_translation() {
273                        return false;
274                    }
275                    transforms.push(next);
276                }
277                PaintCommand::PopTransform => {
278                    if transforms.len() <= 1 {
279                        return false;
280                    }
281                    transforms.pop();
282                }
283                PaintCommand::PushSurfacePartition { .. } | PaintCommand::PopSurfacePartition => {}
284                PaintCommand::Rect { rect, color } => {
285                    if color.alpha != u8::MAX {
286                        return false;
287                    }
288                    let world = transforms
289                        .last()
290                        .expect("root transform")
291                        .transform_rect_bbox(*rect);
292                    const EPSILON: f32 = 0.01;
293                    return world.origin.x <= EPSILON
294                        && world.origin.y <= EPSILON
295                        && world.origin.x + world.size.width >= surface_size.width - EPSILON
296                        && world.origin.y + world.size.height >= surface_size.height - EPSILON;
297                }
298                // Any clip, effect, or visual primitive before a full opaque
299                // rectangle makes complete pixel coverage impossible to prove
300                // with this intentionally cheap predicate.
301                _ => return false,
302            }
303        }
304        false
305    }
306    #[must_use]
307    pub fn commands(&self) -> &[PaintCommand] {
308        &self.commands
309    }
310    #[must_use]
311    pub fn len(&self) -> usize {
312        self.commands.len()
313    }
314    #[must_use]
315    pub fn is_empty(&self) -> bool {
316        self.commands.is_empty()
317    }
318}
319#[derive(Default)]
320pub struct Canvas {
321    list: DisplayList,
322    saves: Vec<SaveKind>,
323}
324#[derive(Clone, Copy)]
325enum SaveKind {
326    Transform,
327    Clip,
328}
329impl Canvas {
330    pub fn rect(&mut self, rect: Rect, color: Color) {
331        self.list.push(PaintCommand::Rect { rect, color });
332    }
333    pub fn rrect(&mut self, rrect: RRect, brush: impl Into<Brush>) {
334        self.list.push(PaintCommand::RRect {
335            rrect,
336            brush: brush.into(),
337        });
338    }
339    pub fn border(&mut self, rrect: RRect, border: Border) {
340        self.list.push(PaintCommand::Border { rrect, border });
341    }
342    pub fn fill_path(&mut self, path: Arc<Path>, brush: impl Into<Brush>, fill_rule: FillRule) {
343        self.list.push(PaintCommand::FillPath {
344            path,
345            brush: brush.into(),
346            fill_rule,
347        });
348    }
349    pub fn stroke_path(&mut self, path: Arc<Path>, brush: impl Into<Brush>, stroke: Stroke) {
350        self.list.push(PaintCommand::StrokePath {
351            path,
352            brush: brush.into(),
353            stroke,
354        });
355    }
356    /// Lowers a [`Paint`] to the existing renderer-neutral path command.
357    ///
358    /// Blend and color-filter metadata remain available on the paint for a
359    /// compositor/backend that needs them; the retained path geometry itself
360    /// is never duplicated into a second command representation.
361    pub fn draw_path(&mut self, path: Arc<Path>, paint: &Paint, fill_rule: FillRule) {
362        match paint.style_value() {
363            PaintStyle::Fill => self.fill_path(path, paint.brush_value().clone(), fill_rule),
364            PaintStyle::Stroke => {
365                self.stroke_path(path, paint.brush_value().clone(), paint.stroke_value())
366            }
367        }
368    }
369    pub fn image(&mut self, image: ImageHandle, source: Rect, destination: Rect) {
370        self.image_with_sampling(image, source, destination, ImageSampling::Linear);
371    }
372    pub fn image_with_sampling(
373        &mut self,
374        image: ImageHandle,
375        source: Rect,
376        destination: Rect,
377        sampling: ImageSampling,
378    ) {
379        self.list.push(PaintCommand::Image {
380            image,
381            source,
382            destination,
383            sampling,
384        });
385    }
386    pub fn glyph_run(&mut self, run: Arc<GlyphRun>, color: Color) {
387        self.list.push(PaintCommand::GlyphRun { run, color });
388    }
389    pub fn save_transform(&mut self, transform: Transform) {
390        self.saves.push(SaveKind::Transform);
391        self.list.push(PaintCommand::PushTransform { transform });
392    }
393    pub fn save_clip(&mut self, rect: Rect) {
394        self.saves.push(SaveKind::Clip);
395        self.list.push(PaintCommand::PushClip { rect });
396    }
397    pub fn save_clip_rrect(&mut self, rrect: RRect) {
398        self.saves.push(SaveKind::Clip);
399        self.list.push(PaintCommand::PushClipRRect { rrect });
400    }
401    pub fn save_clip_oval(&mut self, rect: Rect) {
402        self.saves.push(SaveKind::Clip);
403        self.list.push(PaintCommand::PushClipOval { rect });
404    }
405    pub fn save_clip_path(&mut self, path: Arc<Path>, fill_rule: FillRule) {
406        self.saves.push(SaveKind::Clip);
407        self.list
408            .push(PaintCommand::PushClipPath { path, fill_rule });
409    }
410    pub fn restore(&mut self) {
411        match self.saves.pop().expect("unbalanced canvas restore") {
412            SaveKind::Transform => self.list.push(PaintCommand::PopTransform),
413            SaveKind::Clip => self.list.push(PaintCommand::PopClip),
414        }
415    }
416    #[must_use]
417    pub fn finish(self) -> DisplayList {
418        assert!(self.saves.is_empty(), "unbalanced display list");
419        self.list
420    }
421}