Skip to main content

urushi/view/canvas/
mod.rs

1//! Free-positioned, renderer-neutral drawing inside a finite [`Canvas`].
2//!
3//! A Canvas carries exactly one [`CanvasSizing`] value. Ordinary canvases use
4//! viewport sizing and consume finite allocation; built-in presentations may
5//! internally bind intrinsic requirements without deriving them from items.
6//!
7//! A Canvas owns immutable [`CanvasItem`] values. Resolution first fixes the
8//! surface size, then calls each item once with a frame-scoped [`CanvasContext`].
9//! Items record `View`, text, cell-space primitives, [`LineNetwork`], or
10//! sparse-cell commands in paint order. After sizing, Canvas assembly
11//! rasterizes one command at a time through a common internal contract and
12//! immediately applies its cells through that command's [`Composition`].
13//!
14//! ```
15//! use urushi::{
16//!     Available, Canvas, CanvasContext, CanvasItem, Composition, Position,
17//!     Size, TextStyle, View, resolve,
18//! };
19//!
20//! #[derive(Debug, Clone, PartialEq)]
21//! struct Label(&'static str);
22//!
23//! impl CanvasItem for Label {
24//!     fn draw(&self, canvas: &mut CanvasContext) {
25//!         // Responsive items see the final size, not a provisional measure.
26//!         let x = canvas.size().width().saturating_sub(self.0.len()) as i64;
27//!         canvas.text_with(
28//!             Position::new(x, 0),
29//!             self.0,
30//!             TextStyle::new().bold(),
31//!             Composition::Overlay,
32//!         );
33//!     }
34//! }
35//!
36//! // Explicit extents make an otherwise-unbounded Canvas finite.
37//! let view = View::canvas(Canvas::new().extent(Size::new(8, 2)).item(Label("ok")));
38//! assert_eq!(resolve(&view, Available::NONE).unwrap().size(), Size::new(8, 2));
39//!
40//! // A parent allocation takes precedence over an explicit extent. Omit an
41//! // extent only when that axis will always receive a finite allocation.
42//! let allocated = View::canvas(Canvas::new().item(Label("ok")));
43//! assert_eq!(resolve(&allocated, Available::size(12, 3)).unwrap().size(), Size::new(12, 3));
44//! assert!(resolve(&allocated, Available::NONE).is_err());
45//! ```
46
47mod assemble;
48mod cell;
49mod cell_primitives;
50mod command;
51mod context;
52mod line_network;
53pub(crate) mod sizing;
54
55use std::any::Any;
56use std::fmt;
57
58use super::geometry::Size;
59
60pub use cell::{CanvasCell, CellContribution, Composition, PositionedCell};
61pub use context::CanvasContext;
62pub use line_network::{LineContinuations, LineGlyphs, LineNetwork};
63pub(crate) use sizing::CanvasRequirements;
64pub use sizing::CanvasSizing;
65
66pub(in crate::view) use assemble::compose_canvas;
67
68/// A signed cell position relative to a Canvas's top-left corner.
69#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
70pub struct Position {
71    pub x: i64,
72    pub y: i64,
73}
74
75impl Position {
76    pub const fn new(x: i64, y: i64) -> Self {
77        Self { x, y }
78    }
79}
80
81/// Immutable frame data that records drawing commands after Canvas size is known.
82pub trait CanvasItem: fmt::Debug + Send + Sync + 'static {
83    /// Records this item's commands after the Canvas size is final.
84    fn draw(&self, context: &mut CanvasContext);
85}
86
87trait ErasedItem: fmt::Debug + Send + Sync {
88    fn draw(&self, context: &mut CanvasContext);
89    fn clone_box(&self) -> Box<dyn ErasedItem>;
90    fn equals(&self, other: &dyn ErasedItem) -> bool;
91    fn as_any(&self) -> &dyn Any;
92}
93
94impl<T> ErasedItem for T
95where
96    T: CanvasItem + Clone + PartialEq,
97{
98    fn draw(&self, context: &mut CanvasContext) {
99        CanvasItem::draw(self, context);
100    }
101
102    fn clone_box(&self) -> Box<dyn ErasedItem> {
103        Box::new(self.clone())
104    }
105
106    fn equals(&self, other: &dyn ErasedItem) -> bool {
107        other.as_any().downcast_ref::<T>() == Some(self)
108    }
109
110    fn as_any(&self) -> &dyn Any {
111        self
112    }
113}
114
115#[derive(Debug)]
116struct Item(Box<dyn ErasedItem>);
117
118impl Clone for Item {
119    fn clone(&self) -> Self {
120        Self(self.0.clone_box())
121    }
122}
123
124impl PartialEq for Item {
125    fn eq(&self, other: &Self) -> bool {
126        self.0.equals(&*other.0)
127    }
128}
129
130/// A finite drawing surface and its ordered, owned frame items.
131#[derive(Debug, Clone, Default, PartialEq)]
132pub struct Canvas {
133    sizing: CanvasSizing,
134    items: Vec<Item>,
135}
136
137impl Canvas {
138    pub const fn new() -> Self {
139        Self {
140            sizing: CanvasSizing::viewport(),
141            items: Vec::new(),
142        }
143    }
144
145    /// Replaces this Canvas's complete sizing policy.
146    ///
147    /// Ordinary callers use viewport sizing. Built-in presentations may bind
148    /// an opaque intrinsic value internally; sizing remains independent of the
149    /// Canvas's ordered items in either case.
150    #[must_use]
151    pub fn sizing(mut self, sizing: CanvasSizing) -> Self {
152        self.sizing = sizing;
153        self
154    }
155
156    /// States viewport sizing's extent on an unbounded width axis.
157    ///
158    /// # Panics
159    ///
160    /// Panics when a crate-provided intrinsic sizing value is already installed.
161    pub const fn width(mut self, width: usize) -> Self {
162        self.sizing.set_viewport_width(width);
163        self
164    }
165
166    /// States viewport sizing's extent on an unbounded height axis.
167    ///
168    /// # Panics
169    ///
170    /// Panics when a crate-provided intrinsic sizing value is already installed.
171    pub const fn height(mut self, height: usize) -> Self {
172        self.sizing.set_viewport_height(height);
173        self
174    }
175
176    /// States both unbounded-axis extents of viewport sizing.
177    ///
178    /// # Panics
179    ///
180    /// Panics when a crate-provided intrinsic sizing value is already installed.
181    pub const fn extent(mut self, size: Size) -> Self {
182        self.sizing.set_viewport_extent(size);
183        self
184    }
185
186    /// Appends one owned item to the frame's drawing order.
187    pub fn item<T>(mut self, item: T) -> Self
188    where
189        T: CanvasItem + Clone + PartialEq,
190    {
191        self.items.push(Item(Box::new(item)));
192        self
193    }
194
195    /// Returns the directly owned items whose concrete type is `T`.
196    ///
197    /// This is the typed extension boundary for an adapter that owns a
198    /// [`CanvasItem`] implementation. Core layout remains unaware of that
199    /// item's meaning; the adapter can recover its own immutable values from
200    /// a composed [`Canvas`] without exposing `Any` or a fallible downcast.
201    pub fn items<T>(&self) -> impl Iterator<Item = &T>
202    where
203        T: CanvasItem + Clone + PartialEq,
204    {
205        self.items
206            .iter()
207            .filter_map(|item| item.0.as_any().downcast_ref::<T>())
208    }
209
210    pub(in crate::view) const fn explicit_width(&self) -> Option<usize> {
211        self.sizing.explicit_width()
212    }
213
214    pub(in crate::view) const fn explicit_height(&self) -> Option<usize> {
215        self.sizing.explicit_height()
216    }
217
218    pub(in crate::view) const fn uses_viewport_sizing(&self) -> bool {
219        self.sizing.is_viewport()
220    }
221
222    pub(in crate::view) fn width_requirements(&self) -> sizing::Requirements {
223        self.sizing.width_requirements()
224    }
225
226    pub(in crate::view) fn height_requirements(&self, width: usize) -> sizing::Requirements {
227        self.sizing.height_requirements(width)
228    }
229
230    pub(super) fn draw(&self, size: Size) -> CanvasContext {
231        let mut context = CanvasContext::new(size);
232        for item in &self.items {
233            item.0.draw(&mut context);
234        }
235        context
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use std::sync::{Arc, Mutex};
242
243    use super::sizing::{CanvasMeasure, CanvasRequirements};
244    use super::*;
245    use crate::{Align, Available, BlockStyle, Grapheme, View, measure, resolve};
246
247    #[derive(Debug, Clone, PartialEq, Eq)]
248    enum Event {
249        Width,
250        Height(usize),
251        Draw(Size),
252    }
253
254    #[derive(Debug, Clone)]
255    struct StagedMeasure {
256        width: CanvasRequirements,
257        height: CanvasRequirements,
258        events: Arc<Mutex<Vec<Event>>>,
259    }
260
261    impl PartialEq for StagedMeasure {
262        fn eq(&self, other: &Self) -> bool {
263            self.width == other.width && self.height == other.height
264        }
265    }
266
267    impl CanvasMeasure for StagedMeasure {
268        fn width_requirements(&self) -> CanvasRequirements {
269            self.events.lock().unwrap().push(Event::Width);
270            self.width
271        }
272
273        fn height_requirements(&self, width: usize) -> CanvasRequirements {
274            self.events.lock().unwrap().push(Event::Height(width));
275            self.height
276        }
277    }
278
279    #[derive(Debug, Clone)]
280    struct DrawPastHeight(Arc<Mutex<Vec<Event>>>);
281
282    impl PartialEq for DrawPastHeight {
283        fn eq(&self, _other: &Self) -> bool {
284            true
285        }
286    }
287
288    impl CanvasItem for DrawPastHeight {
289        fn draw(&self, context: &mut CanvasContext) {
290            self.0.lock().unwrap().push(Event::Draw(context.size()));
291            context.cells([PositionedCell::new(
292                Position::new(0, 2),
293                CellContribution::new().symbol(Grapheme::new("x")),
294            )]);
295        }
296    }
297
298    #[derive(Debug, Clone, PartialEq, Eq)]
299    struct Label(&'static str);
300
301    impl CanvasItem for Label {
302        fn draw(&self, _context: &mut CanvasContext) {}
303    }
304
305    #[derive(Debug, Clone, PartialEq, Eq)]
306    struct Marker;
307
308    impl CanvasItem for Marker {
309        fn draw(&self, _context: &mut CanvasContext) {}
310    }
311
312    #[test]
313    fn owned_items_can_be_inspected_by_their_concrete_type() {
314        let canvas = Canvas::new()
315            .item(Label("first"))
316            .item(Marker)
317            .item(Label("second"));
318
319        assert_eq!(
320            canvas.items::<Label>().collect::<Vec<_>>(),
321            [&Label("first"), &Label("second")]
322        );
323        assert_eq!(canvas.items::<Marker>().count(), 1);
324    }
325
326    #[test]
327    fn intrinsic_measurement_is_staged_once_before_drawing() {
328        let events = Arc::new(Mutex::new(Vec::new()));
329        let sizing = CanvasSizing::intrinsic(StagedMeasure {
330            width: CanvasRequirements::new(8, 3),
331            height: CanvasRequirements::new(4, 2),
332            events: Arc::clone(&events),
333        });
334        let view = View::canvas(
335            Canvas::new()
336                .sizing(sizing)
337                .item(DrawPastHeight(Arc::clone(&events))),
338        );
339
340        assert_eq!(measure(&view), Size::new(8, 4));
341        assert_eq!(
342            *events.lock().unwrap(),
343            [Event::Width, Event::Height(8)],
344            "measurement never invokes Canvas items"
345        );
346        events.lock().unwrap().clear();
347
348        let resolved = resolve(&view, Available::size(5, 2)).unwrap();
349
350        assert_eq!(resolved.size(), Size::new(5, 2));
351        assert_eq!(
352            *events.lock().unwrap(),
353            [Event::Width, Event::Height(5), Event::Draw(Size::new(5, 2))]
354        );
355        assert!(
356            resolved
357                .rows()
358                .iter()
359                .flatten()
360                .all(|cell| cell.symbol() != "x"),
361            "drawing beyond the selected height is cropped without reflow"
362        );
363    }
364
365    #[test]
366    fn intrinsic_equality_ignores_allocation_identity() {
367        let first = CanvasSizing::intrinsic(StagedMeasure {
368            width: CanvasRequirements::new(8, 3),
369            height: CanvasRequirements::new(4, 2),
370            events: Arc::new(Mutex::new(Vec::new())),
371        });
372        let same_value_in_another_allocation = CanvasSizing::intrinsic(StagedMeasure {
373            width: CanvasRequirements::new(8, 3),
374            height: CanvasRequirements::new(4, 2),
375            events: Arc::new(Mutex::new(Vec::new())),
376        });
377
378        assert_eq!(first, same_value_in_another_allocation);
379    }
380
381    #[test]
382    fn intrinsic_floors_win_before_the_final_safety_crop() {
383        let events = Arc::new(Mutex::new(Vec::new()));
384        let sizing = CanvasSizing::intrinsic(StagedMeasure {
385            width: CanvasRequirements::new(8, 3),
386            height: CanvasRequirements::new(4, 3),
387            events: Arc::clone(&events),
388        });
389        let view = View::canvas(
390            Canvas::new()
391                .sizing(sizing)
392                .item(DrawPastHeight(Arc::clone(&events))),
393        );
394
395        let resolved = resolve(&view, Available::size(1, 1)).unwrap();
396
397        assert_eq!(resolved.size(), Size::new(1, 1));
398        assert_eq!(
399            *events.lock().unwrap(),
400            [Event::Width, Event::Height(3), Event::Draw(Size::new(3, 3))]
401        );
402    }
403
404    #[test]
405    fn selected_width_height_floors_propagate_through_ancestor_claims() {
406        let first_events = Arc::new(Mutex::new(Vec::new()));
407        let second_events = Arc::new(Mutex::new(Vec::new()));
408        let intrinsic = |height, floor, events: &Arc<Mutex<Vec<Event>>>| {
409            View::canvas(
410                Canvas::new()
411                    .sizing(CanvasSizing::intrinsic(StagedMeasure {
412                        width: CanvasRequirements::new(1, 0),
413                        height: CanvasRequirements::new(height, floor),
414                        events: Arc::clone(events),
415                    }))
416                    .item(DrawPastHeight(Arc::clone(events))),
417            )
418        };
419        let view = View::column(
420            Align::Left,
421            [
422                View::block(BlockStyle::new(), intrinsic(10, 8, &first_events)),
423                intrinsic(10, 0, &second_events),
424            ],
425        );
426
427        let resolved = resolve(&view, Available::size(3, 10)).unwrap();
428
429        assert_eq!(resolved.size(), Size::new(3, 10));
430        assert_eq!(
431            first_events.lock().unwrap().last(),
432            Some(&Event::Draw(Size::new(1, 8)))
433        );
434        assert_eq!(
435            second_events.lock().unwrap().last(),
436            Some(&Event::Draw(Size::new(3, 2)))
437        );
438    }
439}