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}