rosace_core/render_object.rs
1use crate::types::{Point, Size};
2
3/// Placeholder canvas type for Phase 1.
4///
5/// The real Skia-backed canvas is provided by `rosace-render`; this stub
6/// keeps `rosace-core` free of rendering dependencies during the component
7/// model phase.
8pub struct Canvas;
9
10/// Describes a bound along a single layout axis.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub enum AxisBound {
13 /// The axis has a finite pixel limit.
14 Bounded(f32),
15 /// The axis is unconstrained — the child may be any size.
16 Unbounded,
17 /// The axis must shrink to fit its content (intrinsic sizing).
18 Shrink,
19}
20
21/// Layout constraints passed down the render tree during the measure pass.
22///
23/// Distinct from `rosace_trace::event::TraceConstraints`, which is a
24/// simplified snapshot for tracing purposes only.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct Constraints {
27 /// Minimum allowed width in logical pixels.
28 pub min_width: f32,
29 /// Maximum allowed width.
30 pub max_width: AxisBound,
31 /// Minimum allowed height in logical pixels.
32 pub min_height: f32,
33 /// Maximum allowed height.
34 pub max_height: AxisBound,
35}
36
37impl Constraints {
38 /// Loose constraints: minimum is zero, maximum is the given dimensions.
39 ///
40 /// The child may be any size up to `width` × `height`.
41 pub fn loose(width: f32, height: f32) -> Self {
42 Constraints {
43 min_width: 0.0,
44 max_width: AxisBound::Bounded(width),
45 min_height: 0.0,
46 max_height: AxisBound::Bounded(height),
47 }
48 }
49
50 /// Tight constraints: minimum equals maximum at the given dimensions.
51 ///
52 /// The child must be exactly `width` × `height`.
53 pub fn tight(width: f32, height: f32) -> Self {
54 Constraints {
55 min_width: width,
56 max_width: AxisBound::Bounded(width),
57 min_height: height,
58 max_height: AxisBound::Bounded(height),
59 }
60 }
61
62 /// Fully unbounded constraints: the child may take any size on both axes.
63 pub fn unbounded() -> Self {
64 Constraints {
65 min_width: 0.0,
66 max_width: AxisBound::Unbounded,
67 min_height: 0.0,
68 max_height: AxisBound::Unbounded,
69 }
70 }
71
72 /// Returns the maximum width as `f32`, or [`f32::INFINITY`] if the axis is
73 /// unbounded or shrink-to-fit.
74 pub fn max_width_f32(&self) -> f32 {
75 match &self.max_width {
76 AxisBound::Bounded(v) => *v,
77 _ => f32::INFINITY,
78 }
79 }
80
81 /// Returns the maximum height as `f32`, or [`f32::INFINITY`] if the axis is
82 /// unbounded or shrink-to-fit.
83 pub fn max_height_f32(&self) -> f32 {
84 match &self.max_height {
85 AxisBound::Bounded(v) => *v,
86 _ => f32::INFINITY,
87 }
88 }
89
90 /// Clamp `size` so that it satisfies these constraints.
91 ///
92 /// Width and height are each clamped to `[min, max]`.
93 pub fn constrain(&self, size: Size) -> Size {
94 let width = size.width.max(self.min_width).min(self.max_width_f32());
95 let height = size.height.max(self.min_height).min(self.max_height_f32());
96 Size { width, height }
97 }
98
99 /// Returns `true` when both axes are tightly bounded (`min == max`).
100 pub fn is_tight(&self) -> bool {
101 let w_tight = matches!(&self.max_width, AxisBound::Bounded(v) if (v - self.min_width).abs() < f32::EPSILON);
102 let h_tight = matches!(&self.max_height, AxisBound::Bounded(v) if (v - self.min_height).abs() < f32::EPSILON);
103 w_tight && h_tight
104 }
105}
106
107/// The core trait implemented by every node in the render tree.
108///
109/// Each `RenderObject` is responsible for measuring itself given layout
110/// `Constraints`, painting into a `Canvas`, and reporting hit-test results.
111pub trait RenderObject: 'static {
112 /// Measures the object under the given constraints and returns its size.
113 fn layout(&mut self, constraints: Constraints) -> Size;
114
115 /// Paints the object into `canvas` at the origin, occupying `size`.
116 fn paint(&self, canvas: &mut Canvas, size: Size);
117
118 /// Returns `true` if `point` lies within the object's bounding box.
119 ///
120 /// The default implementation uses the full axis-aligned bounding box
121 /// anchored at the origin.
122 fn hit_test(&self, point: Point, size: Size) -> bool {
123 point.x >= 0.0
124 && point.x <= size.width
125 && point.y >= 0.0
126 && point.y <= size.height
127 }
128}