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    /// Moves every retained surface partition (an open popup, tooltip or
290    /// menu) after the rest of the list, as Flutter's `Overlay` paints its
291    /// entries above the routes beneath it: a popup is never covered by the
292    /// widgets painted after its anchor.
293    ///
294    /// A lifted partition keeps the transforms that enclosed it, so it stays
295    /// where it was placed, and leaves the clips and effects of its
296    /// ancestors behind, so an ancestor's clip does not cut it off. Its
297    /// markers are kept, so [`Self::detach_surface_partitions`] can still
298    /// move it to a native surface. Nested partitions are lifted within
299    /// their parent. A list without partitions is returned unchanged.
300    #[must_use]
301    pub fn lift_surface_partitions(self) -> Self {
302        if !self
303            .commands()
304            .iter()
305            .any(|command| matches!(command, PaintCommand::PushSurfacePartition { .. }))
306        {
307            return self;
308        }
309        Self::lift_partitions_in(self.commands())
310    }
311
312    fn lift_partitions_in(commands: &[PaintCommand]) -> Self {
313        let mut base = Self::new();
314        let mut lifted = Vec::<Self>::new();
315        let mut transforms = Vec::<Transform>::new();
316        let mut index = 0;
317        while index < commands.len() {
318            match &commands[index] {
319                PaintCommand::PushSurfacePartition { .. } => {
320                    // The partition runs to its matching pop.
321                    let start = index;
322                    let mut depth = 0usize;
323                    while index < commands.len() {
324                        match commands[index] {
325                            PaintCommand::PushSurfacePartition { .. } => depth += 1,
326                            PaintCommand::PopSurfacePartition => {
327                                depth -= 1;
328                                if depth == 0 {
329                                    break;
330                                }
331                            }
332                            _ => {}
333                        }
334                        index += 1;
335                    }
336                    let end = index.min(commands.len() - 1);
337                    let mut entry = Self::new();
338                    for transform in &transforms {
339                        entry.push(PaintCommand::PushTransform {
340                            transform: *transform,
341                        });
342                    }
343                    entry.push(commands[start].clone());
344                    entry.extend_from(&Self::lift_partitions_in(&commands[start + 1..end]));
345                    if matches!(commands[end], PaintCommand::PopSurfacePartition) && end > start {
346                        entry.push(PaintCommand::PopSurfacePartition);
347                    }
348                    for _ in &transforms {
349                        entry.push(PaintCommand::PopTransform);
350                    }
351                    lifted.push(entry);
352                }
353                command => {
354                    match command {
355                        PaintCommand::PushTransform { transform } => transforms.push(*transform),
356                        PaintCommand::PopTransform => {
357                            let _ = transforms.pop();
358                        }
359                        _ => {}
360                    }
361                    base.push(command.clone());
362                }
363            }
364            index += 1;
365        }
366        for entry in &lifted {
367            base.extend_from(entry);
368        }
369        base
370    }
371
372    /// Wraps this list in one logical translation without mutating retained
373    /// command geometry.
374    #[must_use]
375    pub fn translated(&self, offset: incular_core::Offset) -> Self {
376        if self.is_empty() || offset == incular_core::Offset::ZERO {
377            return self.clone();
378        }
379        let mut output = Self::new();
380        output.push(PaintCommand::PushTransform {
381            transform: Transform::translation(offset),
382        });
383        output.extend_from(self);
384        output.push(PaintCommand::PopTransform);
385        output
386    }
387
388    /// Conservatively proves that the first visual operation covers the entire
389    /// target with an opaque axis-aligned rectangle.
390    ///
391    /// This is intentionally stricter than a general coverage analysis. A
392    /// backend may use it before presenting through an opaque native surface;
393    /// returning `false` means "not proven" and must never be interpreted as
394    /// transparency being unnecessary.
395    #[must_use]
396    pub fn begins_with_opaque_surface_rect(&self, surface_size: Size) -> bool {
397        if !surface_size.width.is_finite()
398            || !surface_size.height.is_finite()
399            || surface_size.width <= 0.0
400            || surface_size.height <= 0.0
401        {
402            return false;
403        }
404        let mut transforms = vec![Transform::IDENTITY];
405        for command in self.commands() {
406            match command {
407                PaintCommand::PushTransform { transform } => {
408                    let next = transforms
409                        .last()
410                        .copied()
411                        .expect("root transform")
412                        .then(*transform);
413                    if !next.is_translation() {
414                        return false;
415                    }
416                    transforms.push(next);
417                }
418                PaintCommand::PopTransform => {
419                    if transforms.len() <= 1 {
420                        return false;
421                    }
422                    transforms.pop();
423                }
424                PaintCommand::PushSurfacePartition { .. } | PaintCommand::PopSurfacePartition => {}
425                PaintCommand::Rect { rect, color } => {
426                    if color.alpha != u8::MAX {
427                        return false;
428                    }
429                    let world = transforms
430                        .last()
431                        .expect("root transform")
432                        .transform_rect_bbox(*rect);
433                    const EPSILON: f32 = 0.01;
434                    return world.origin.x <= EPSILON
435                        && world.origin.y <= EPSILON
436                        && world.origin.x + world.size.width >= surface_size.width - EPSILON
437                        && world.origin.y + world.size.height >= surface_size.height - EPSILON;
438                }
439                // Any clip, effect, or visual primitive before a full opaque
440                // rectangle makes complete pixel coverage impossible to prove
441                // with this intentionally cheap predicate.
442                _ => return false,
443            }
444        }
445        false
446    }
447    #[must_use]
448    pub fn commands(&self) -> &[PaintCommand] {
449        self.commands.as_deref().map_or(&[], Vec::as_slice)
450    }
451    #[must_use]
452    pub fn len(&self) -> usize {
453        self.commands().len()
454    }
455    #[must_use]
456    pub fn is_empty(&self) -> bool {
457        self.commands().is_empty()
458    }
459    /// Removes every command, keeping the allocation for the next recording.
460    pub fn clear(&mut self) {
461        if let Some(commands) = self.commands.as_mut() {
462            match std::sync::Arc::get_mut(commands) {
463                Some(unique) => unique.clear(),
464                None => self.commands = None,
465            }
466        }
467    }
468}
469#[derive(Default)]
470pub struct Canvas {
471    list: DisplayList,
472    saves: Vec<SaveKind>,
473}
474#[derive(Clone, Copy)]
475enum SaveKind {
476    Transform,
477    Clip,
478}
479impl Canvas {
480    pub fn rect(&mut self, rect: Rect, color: Color) {
481        self.list.push(PaintCommand::Rect { rect, color });
482    }
483    pub fn rrect(&mut self, rrect: RRect, brush: impl Into<Brush>) {
484        self.list.push(PaintCommand::RRect {
485            rrect,
486            brush: brush.into(),
487        });
488    }
489    pub fn border(&mut self, rrect: RRect, border: Border) {
490        self.list.push(PaintCommand::Border { rrect, border });
491    }
492    pub fn fill_path(&mut self, path: Arc<Path>, brush: impl Into<Brush>, fill_rule: FillRule) {
493        self.list.push(PaintCommand::FillPath {
494            path,
495            brush: brush.into(),
496            fill_rule,
497        });
498    }
499    pub fn stroke_path(&mut self, path: Arc<Path>, brush: impl Into<Brush>, stroke: Stroke) {
500        self.list.push(PaintCommand::StrokePath {
501            path,
502            brush: brush.into(),
503            stroke,
504        });
505    }
506    /// Lowers a [`Paint`] to the existing renderer-neutral path command.
507    ///
508    /// Blend and color-filter metadata remain available on the paint for a
509    /// compositor/backend that needs them; the retained path geometry itself
510    /// is never duplicated into a second command representation.
511    pub fn draw_path(&mut self, path: Arc<Path>, paint: &Paint, fill_rule: FillRule) {
512        match paint.style_value() {
513            PaintStyle::Fill => self.fill_path(path, paint.brush_value().clone(), fill_rule),
514            PaintStyle::Stroke => {
515                self.stroke_path(path, paint.brush_value().clone(), paint.stroke_value())
516            }
517        }
518    }
519    pub fn image(&mut self, image: ImageHandle, source: Rect, destination: Rect) {
520        self.image_with_sampling(image, source, destination, ImageSampling::Linear);
521    }
522    pub fn image_with_sampling(
523        &mut self,
524        image: ImageHandle,
525        source: Rect,
526        destination: Rect,
527        sampling: ImageSampling,
528    ) {
529        self.list.push(PaintCommand::Image {
530            image,
531            source,
532            destination,
533            sampling,
534        });
535    }
536    pub fn glyph_run(&mut self, run: Arc<GlyphRun>, color: Color) {
537        self.list.push(PaintCommand::GlyphRun { run, color });
538    }
539    pub fn save_transform(&mut self, transform: Transform) {
540        self.saves.push(SaveKind::Transform);
541        self.list.push(PaintCommand::PushTransform { transform });
542    }
543    pub fn save_clip(&mut self, rect: Rect) {
544        self.saves.push(SaveKind::Clip);
545        self.list.push(PaintCommand::PushClip { rect });
546    }
547    pub fn save_clip_rrect(&mut self, rrect: RRect) {
548        self.saves.push(SaveKind::Clip);
549        self.list.push(PaintCommand::PushClipRRect { rrect });
550    }
551    pub fn save_clip_oval(&mut self, rect: Rect) {
552        self.saves.push(SaveKind::Clip);
553        self.list.push(PaintCommand::PushClipOval { rect });
554    }
555    pub fn save_clip_path(&mut self, path: Arc<Path>, fill_rule: FillRule) {
556        self.saves.push(SaveKind::Clip);
557        self.list
558            .push(PaintCommand::PushClipPath { path, fill_rule });
559    }
560    pub fn restore(&mut self) {
561        match self.saves.pop().expect("unbalanced canvas restore") {
562            SaveKind::Transform => self.list.push(PaintCommand::PopTransform),
563            SaveKind::Clip => self.list.push(PaintCommand::PopClip),
564        }
565    }
566    #[must_use]
567    pub fn finish(self) -> DisplayList {
568        assert!(self.saves.is_empty(), "unbalanced display list");
569        self.list
570    }
571}