motion_core/drawing/context.rs
1//! Shared drawing inputs and drawable contracts.
2
3use super::*;
4
5/// Explicit preview approximation policy. Final preserves production rendering.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
7pub enum RenderQuality {
8 /// Interactive preview; content may use bounded lower-detail approximations.
9 Draft,
10 /// Full-quality production rendering and export.
11 #[default]
12 Final,
13}
14
15/// Drawing inputs for the frontend.
16///
17/// Device scale and structural limits are drawing-specific; time, quality,
18/// assets, fonts, diagnostics and compute caches live in the shared
19/// [`Evaluation`]. Neither implicitly discovers resources.
20#[derive(Clone, Debug)]
21pub struct DrawContext {
22 pub(crate) device_scale: f64,
23 limits: crate::ValidationLimits,
24 evaluation: std::sync::Arc<Evaluation>,
25 font_usage: crate::FontUsage,
26 mark_invalid_geometry: bool,
27}
28
29impl Default for DrawContext {
30 fn default() -> Self {
31 Self {
32 device_scale: 1.0,
33 limits: crate::ValidationLimits::default(),
34 evaluation: std::sync::Arc::default(),
35 font_usage: crate::FontUsage::default(),
36 mark_invalid_geometry: false,
37 }
38 }
39}
40
41impl DrawContext {
42 /// Creates drawing inputs with a finite, strictly positive device scale.
43 pub fn new(device_scale: f64) -> Result<Self, ValidationError> {
44 if !device_scale.is_finite() || device_scale <= 0.0 {
45 return Err(ValidationError::invalid(
46 "drawing",
47 "device_scale",
48 "Require a finite, positive device scale",
49 ));
50 }
51 Ok(Self {
52 device_scale,
53 ..Self::default()
54 })
55 }
56
57 /// Replaces the shared evaluation environment.
58 pub fn with_evaluation(mut self, evaluation: std::sync::Arc<Evaluation>) -> Self {
59 self.evaluation = evaluation;
60 self
61 }
62
63 /// Shared time, quality, resources, diagnostics and caches.
64 pub fn evaluation(&self) -> &std::sync::Arc<Evaluation> {
65 &self.evaluation
66 }
67
68 fn edit(mut self, edit: impl FnOnce(Evaluation) -> Evaluation) -> Self {
69 let current = std::sync::Arc::try_unwrap(self.evaluation)
70 .unwrap_or_else(|shared| Evaluation::clone(&shared));
71 self.evaluation = std::sync::Arc::new(edit(current));
72 self
73 }
74
75 /// Selects an explicit approximation policy for deferred content recording.
76 pub fn with_quality(self, quality: RenderQuality) -> Self {
77 self.edit(|evaluation| evaluation.with_quality(quality))
78 }
79
80 /// Production quality is the default for every standalone drawing context.
81 pub fn render_quality(&self) -> RenderQuality {
82 self.evaluation.quality()
83 }
84
85 /// Supplies scene-local signed time to deferred content effects.
86 pub fn with_time(self, time: crate::Time) -> Self {
87 self.edit(|evaluation| evaluation.with_time(time))
88 }
89
90 /// Scene-local time; direct static recording defaults to zero.
91 pub fn time(&self) -> crate::Time {
92 self.evaluation.time()
93 }
94
95 /// Device pixels per logical unit; does not implicitly modify local geometry.
96 pub fn device_scale(&self) -> f64 {
97 self.device_scale
98 }
99
100 /// Supplies a shared explicit font snapshot to paragraph bounds and drawing.
101 pub fn with_fonts(self, fonts: impl Into<crate::asset::environment::FontEnvironment>) -> Self {
102 self.edit(|evaluation| evaluation.with_fonts(fonts))
103 }
104
105 /// Font environment for paragraph text, optional for geometry-only drawing.
106 pub fn fonts(&self) -> Option<&crate::asset::environment::FontEnvironment> {
107 self.evaluation.fonts()
108 }
109
110 /// Captures the same lazy/preloaded owner used by the enclosing session.
111 pub fn with_assets(self, assets: &crate::Assets) -> Self {
112 self.edit(|evaluation| evaluation.with_assets(assets))
113 }
114
115 /// Explicit resource owner; an unconfigured context does not resolve relative files.
116 pub fn assets(&self) -> &crate::Assets {
117 self.evaluation.assets()
118 }
119
120 /// Records a drawable's local commands without rendering or color quantization.
121 ///
122 /// Limits charge the drawable at depth one and every command at depth two.
123 /// Callback errors and ignored canvas errors both fail the recording.
124 /// This local buffer is not a scene DisplayList; use [`crate::Node::compile`]
125 /// for layout, global transforms and isolated subtree opacity.
126 ///
127 /// ```
128 /// use motion_core::prelude::*;
129 /// use motion_core::{
130 /// DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
131 /// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
132 /// };
133 /// let bounds = Rect::new(0.1, 0.2, 10.3, 20.4);
134 /// let color = Color::from_rgb_float(1.25, -0.125, 0.333);
135 /// let commands =
136 /// DrawContext::new(2.0)?.record(&bounds.fill(color), ValidationLimits::default())?;
137 /// assert_eq!(
138 /// commands,
139 /// vec![DrawCommand::FillRect {
140 /// bounds,
141 /// transform: Transform::IDENTITY,
142 /// paint: Paint::Solid(color)
143 /// }]
144 /// );
145 /// # Ok::<(), motion_core::ValidationError>(())
146 /// ```
147 pub fn record(
148 &self,
149 drawable: &dyn Drawable,
150 limits: ValidationLimits,
151 ) -> Result<Vec<DrawCommand>, ValidationError> {
152 let mut state = ValidationState::new(limits)?;
153 state.enter("drawable", 1)?;
154 validate_rect(drawable.bounds(self)?, "drawable", "bounds")?;
155 Canvas::new(&mut state, "drawable", 2).record(drawable, self)
156 }
157
158 /// Supplies the enclosing structural policy to deferred layout callbacks.
159 pub fn with_validation_limits(mut self, limits: crate::ValidationLimits) -> Self {
160 self.limits = limits;
161 self
162 }
163
164 /// Structural policy supplied by the enclosing measurement or compilation.
165 pub fn validation_limits(&self) -> crate::ValidationLimits {
166 self.limits
167 }
168
169 /// Supplies partitioned compute caches shared by rendering workers.
170 pub fn with_render_cache(self, cache: crate::RenderCache) -> Self {
171 self.edit(|evaluation| evaluation.with_cache(cache))
172 }
173
174 /// Shared cache partitions, absent for standalone uncached drawing.
175 pub fn render_cache(&self) -> Option<&crate::RenderCache> {
176 self.evaluation.cache()
177 }
178
179 /// Caches immutable layout in the paragraph partition by full typed key and
180 /// declared logical payload; without a render cache the value is rebuilt.
181 pub fn cache_layout<
182 Key: Eq + std::hash::Hash + Send + Sync + 'static,
183 Value: Send + Sync + 'static,
184 >(
185 &self,
186 key: Key,
187 build: impl FnOnce() -> Value,
188 cost: impl FnOnce(&Value) -> usize,
189 ) -> std::sync::Arc<Value> {
190 match self.render_cache() {
191 Some(cache) => cache
192 .partition(crate::CachePartition::Paragraph)
193 .get_with_cost(key, build, cost),
194 None => std::sync::Arc::new(build()),
195 }
196 }
197
198 /// Carries an export-owned font observation sink into every recorded glyph.
199 pub fn with_font_usage(mut self, usage: crate::FontUsage) -> Self {
200 self.font_usage = usage;
201 self
202 }
203
204 /// Font sources actually painted through this context.
205 pub fn font_usage(&self) -> &crate::FontUsage {
206 &self.font_usage
207 }
208
209 /// Draws leaves with invalid geometry as a visible error marker instead of failing
210 /// compilation. Intended for preview; export keeps the strict default.
211 pub fn with_invalid_geometry_marker(mut self, mark: bool) -> Self {
212 self.mark_invalid_geometry = mark;
213 self
214 }
215
216 /// Whether invalid leaf geometry is drawn as an error marker.
217 pub fn marks_invalid_geometry(&self) -> bool {
218 self.mark_invalid_geometry
219 }
220
221 /// Carries request diagnostics explicitly through deferred content compilation.
222 pub fn with_diagnostics(self, diagnostics: crate::RequestDiagnostics) -> Self {
223 self.edit(|evaluation| evaluation.with_diagnostics(diagnostics))
224 }
225
226 /// Request-owned sink; cloning this drawing context preserves its identity.
227 pub fn diagnostics(&self) -> &crate::RequestDiagnostics {
228 self.evaluation.diagnostics()
229 }
230}
231
232/// Content whose bounds and drawing share the same logical coordinates.
233///
234/// Implementations must be deterministic and time independent for fixed inputs.
235/// Bounds are supplied by content. Custom content
236/// must enclose its own drawing; the frontend validates bounds and commands but
237/// cannot prove arbitrary callbacks return conservative bounds.
238///
239/// ```
240/// use motion_core::prelude::*;
241/// use motion_core::{
242/// DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
243/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
244/// };
245/// let row = Row::new()
246/// .gap(4.0)
247/// .children([
248/// Rect::new(0.0, 0.0, 10.0, 20.0).fill(Color::from_hex(0x24a49b)),
249/// Rect::new(0.0, 0.0, 5.0, 10.0).fill(Color::WHITE),
250/// ])
251/// .at(Point::new(100.0, 80.0));
252/// assert_eq!(
253/// row.measure(ValidationLimits::default())?,
254/// Rect::new(90.5, 70.0, 109.5, 90.0)
255/// );
256/// let list = row.compile(
257/// Size::new(200.0, 160.0),
258/// PixelSize::new(200, 160),
259/// ValidationLimits::default(),
260/// )?;
261/// assert_eq!(list.commands.len(), 2);
262/// # Ok::<(), motion_core::ValidationError>(())
263/// ```
264pub trait Drawable: Send + Sync {
265 /// Local layout/ink bounds before node placement; invalid content is an error.
266 fn bounds(&self, context: &DrawContext) -> Result<Rect, ValidationError>;
267
268 /// Real horizontal text baseline in local coordinates; geometry has none.
269 fn baseline(&self, _context: &DrawContext) -> Option<f64> {
270 None
271 }
272
273 /// Emits local commands; validation errors propagate without partial output.
274 fn draw(&self, context: &DrawContext, canvas: &mut Canvas<'_>) -> Result<(), ValidationError>;
275}
276
277/// Content that expands into ordinary nodes at the current drawing time.
278///
279/// Deferred content is not a leaf: it owns no bounds or commands. Measuring and
280/// compiling resolve it first, so the outer placement and inherited scene time
281/// apply to the resolved subtree.
282pub trait DeferredContent: Send + Sync {
283 /// Resolves this content to an ordinary node at the drawing time in `context`.
284 fn resolve(&self, context: &DrawContext) -> Result<crate::Node, ValidationError>;
285
286 /// Real horizontal text baseline of the resolved node in local coordinates.
287 fn baseline(&self, _context: &DrawContext) -> Option<f64> {
288 None
289 }
290}
291
292#[cfg(test)]
293mod quality_tests {
294 use super::*;
295
296 #[test]
297 fn quality_is_explicit_cloned_and_final_by_default() {
298 assert_eq!(
299 DrawContext::default().render_quality(),
300 RenderQuality::Final
301 );
302 let context = DrawContext::new(0.5)
303 .unwrap()
304 .with_quality(RenderQuality::Draft);
305 assert_eq!(context.clone().render_quality(), RenderQuality::Draft);
306 assert_eq!(context.device_scale(), 0.5);
307 }
308}