Skip to main content

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}