Skip to main content

motion_core/drawing/
canvas.rs

1//! Bounded command recording for drawable content.
2
3use super::*;
4
5/// A bounded command recorder passed to content.
6///
7/// The first failure poisons the recording, even when a callback ignores the
8/// returned error. Geometry is checked and budget charged before storage grows.
9pub struct Canvas<'a> {
10    commands: Vec<DrawCommand>,
11    state: &'a mut ValidationState,
12    path: &'a str,
13    depth: usize,
14    error: Option<ValidationError>,
15}
16
17impl<'a> Canvas<'a> {
18    /// Records evaluated instances without expanding them into path geometry.
19    pub fn instances(
20        &mut self,
21        batch: Arc<dyn crate::instances::InstanceBatch>,
22        transform: Transform,
23    ) -> Result<(), ValidationError> {
24        let result = (|| {
25            batch.validate()?;
26            crate::display_list::mapped_bounds(batch.bounds(), transform, self.path)?;
27            self.push(DrawCommand::ParticleInstances { batch, transform })
28        })();
29        if let Err(error) = &result {
30            self.error = Some(error.clone());
31        }
32        result
33    }
34
35    /// Records a shared font-unit/path with its local transform.
36    pub fn fill_path(
37        &mut self,
38        geometry: Arc<Path2D>,
39        transform: Transform,
40        paint: impl Into<Paint>,
41        rule: FillRule,
42    ) -> Result<(), ValidationError> {
43        let result =
44            crate::display_list::mapped_bounds(geometry.bounding_box(), transform, self.path)
45                .and_then(|_| {
46                    self.push(DrawCommand::Fill {
47                        path: geometry,
48                        transform,
49                        paint: paint.into(),
50                        rule,
51                    })
52                });
53        if let Err(error) = &result {
54            self.error = Some(error.clone());
55        }
56        result
57    }
58
59    /// Creates a local recorder borrowing the caller's structural budget and diagnostic path.
60    /// Commands and path verbs are charged at `depth` before they enter the result.
61    pub fn new(state: &'a mut ValidationState, path: &'a str, depth: usize) -> Self {
62        Self {
63            commands: Vec::new(),
64            state,
65            path,
66            depth,
67            error: None,
68        }
69    }
70
71    /// Records a filled rectangle without narrowing geometry or color.
72    pub fn fill_rect(
73        &mut self,
74        bounds: Rect,
75        paint: impl Into<Paint>,
76    ) -> Result<(), ValidationError> {
77        self.push(DrawCommand::FillRect {
78            bounds,
79            transform: Transform::IDENTITY,
80            paint: paint.into(),
81        })
82    }
83
84    /// Fills geometry, retaining its explicit contour rule.
85    ///
86    /// Each path verb also consumes one shared work-budget unit at command
87    /// depth. Rectangles retain the existing constant-cost fill fast path.
88    pub fn fill(
89        &mut self,
90        shape: &impl Shape,
91        paint: impl Into<Paint>,
92        rule: FillRule,
93    ) -> Result<(), ValidationError> {
94        if let Some(error) = &self.error {
95            return Err(error.clone());
96        }
97        let bounds = match shape.bounding_box() {
98            Ok(bounds) => bounds,
99            Err(error) => {
100                self.error = Some(error.clone());
101                return Err(error);
102            }
103        };
104        if let Err(mut error) = validate_rect(bounds, "", "bounds") {
105            error.path = format!("{}.commands[{}]", self.path, self.commands.len());
106            self.error = Some(error.clone());
107            return Err(error);
108        }
109        let paint = paint.into();
110        if let Some(bounds) = shape.as_rect() {
111            return self.fill_rect(bounds, paint);
112        }
113        let path = match shape.path() {
114            Ok(path) => path,
115            Err(error) => {
116                self.error = Some(error.clone());
117                return Err(error);
118            }
119        };
120        self.push(DrawCommand::Fill {
121            path: Arc::new(path),
122            transform: Transform::IDENTITY,
123            paint,
124            rule,
125        })
126    }
127
128    /// Expands a bounded line/curve stroke into a NonZero fill.
129    ///
130    /// Source verbs and generated outline verbs charge the shared structural
131    /// budget independently. Invalid parameters are checked even at zero width.
132    /// Errors poison the recording even if the caller ignores the Result.
133    pub fn stroke(
134        &mut self,
135        shape: &impl Shape,
136        style: &StrokeStyle,
137        paint: impl Into<Paint>,
138    ) -> Result<(), ValidationError> {
139        if let Some(error) = &self.error {
140            return Err(error.clone());
141        }
142        let path = format!("{}.commands[{}].stroke", self.path, self.commands.len());
143        let result = (|| {
144            style.validate(&path)?;
145            validate_rect(shape.bounding_box()?, &path, "bounds")?;
146            let geometry = shape.path()?;
147            for index in 0..geometry.verbs().len() {
148                self.state
149                    .enter_with(|| format!("{path}.source.verbs[{index}]"), self.depth)?;
150            }
151            let outline = style.outline(&geometry, &path)?;
152            if outline.verbs().is_empty() {
153                return Ok(());
154            }
155            self.push(DrawCommand::Fill {
156                path: Arc::new(outline),
157                transform: Transform::IDENTITY,
158                paint: paint.into(),
159                rule: FillRule::NonZero,
160            })
161        })();
162        if let Err(error) = &result {
163            self.error = Some(error.clone());
164        }
165        result
166    }
167
168    fn push(&mut self, command: DrawCommand) -> Result<(), ValidationError> {
169        if let Some(error) = &self.error {
170            return Err(error.clone());
171        }
172        let index = self.commands.len();
173        let path = || format!("{}.commands[{index}]", self.path);
174        let (bounds, verbs) = match &command {
175            DrawCommand::ParticleInstances { batch, .. } => (batch.bounds(), batch.len()),
176            DrawCommand::FillRect { bounds, .. } => (*bounds, 0),
177            DrawCommand::Fill { path, .. } => (path.bounding_box(), path.verbs().len()),
178            DrawCommand::PushFilterLayer { .. }
179            | DrawCommand::PushLayer { .. }
180            | DrawCommand::PushRectLayer { .. }
181            | DrawCommand::PushShapeLayer { .. }
182            | DrawCommand::PopLayer
183            | DrawCommand::BeginMask { .. }
184            | DrawCommand::EndMask => {
185                let error = ValidationError::invalid(
186                    path(),
187                    "commands",
188                    "Canvas records local fills, not compiled layer boundaries",
189                );
190                self.error = Some(error.clone());
191                return Err(error);
192            }
193        };
194        if let DrawCommand::FillRect { paint, .. } | DrawCommand::Fill { paint, .. } = &command
195            && let Err(mut error) = paint.validate()
196        {
197            error.path = path();
198            self.error = Some(error.clone());
199            return Err(error);
200        }
201        let result = validate_rect(bounds, "", "bounds")
202            .map_err(|mut error| {
203                error.path = path();
204                error
205            })
206            .and_then(|()| self.state.enter_with(path, self.depth))
207            .and_then(|()| {
208                for verb in 0..verbs {
209                    self.state
210                        .enter_with(|| format!("{}.verbs[{verb}]", path()), self.depth)?;
211                }
212                Ok(())
213            });
214        if let Err(error) = result {
215            self.error = Some(error.clone());
216            return Err(error);
217        }
218        self.commands.push(command);
219        Ok(())
220    }
221
222    /// Invokes one drawable and returns its complete validated command sequence.
223    /// The first recorder failure wins even when ignored by the callback; no partial
224    /// commands escape on error. Callback panics propagate to the caller.
225    pub fn record(
226        mut self,
227        drawable: &dyn Drawable,
228        context: &DrawContext,
229    ) -> Result<Vec<DrawCommand>, ValidationError> {
230        let path = self.path;
231        let result = drawable.draw(context, &mut self);
232        let commands = self.finish()?;
233        result.map_err(|mut error| {
234            error.path = if error.path.is_empty() {
235                format!("{path}.draw")
236            } else {
237                format!("{path}.draw.{}", error.path)
238            };
239            error
240        })?;
241        Ok(commands)
242    }
243
244    pub(crate) fn finish(self) -> Result<Vec<DrawCommand>, ValidationError> {
245        self.error.map_or(Ok(self.commands), Err)
246    }
247}