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