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