ailloli_ui_runtime/scene/draw_cmd.rs
1//! Renderer-neutral drawing commands emitted by retained views.
2
3use std::sync::Arc;
4
5use ailloli_ui_core::{
6 Border, BoxShadow, Color, IconId, Point, Radius, Rect, StrokeStyle, TextDecoration,
7};
8use ailloli_ui_text::PreparedTextLayout;
9
10/// Filled axis-aligned rectangle draw primitive.
11///
12/// Coordinates are window-space logical pixels; color is linear RGBA.
13///
14/// # Examples
15///
16/// ```
17/// use ailloli_ui_core::{Color, Rect};
18/// use ailloli_ui_runtime::scene::DrawRect;
19/// let draw = DrawRect { rect: Rect::new(1.0, 2.0, 30.0, 10.0), color: Color::WHITE };
20/// assert_eq!(draw.rect.w, 30.0);
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct DrawRect {
24 /// Filled rectangle in logical pixels.
25 pub rect: Rect,
26 /// Linear-RGBA fill color.
27 pub color: Color,
28}
29
30/// Filled rounded-rectangle draw primitive.
31///
32/// # Examples
33///
34/// ```
35/// use ailloli_ui_core::{Color, Rect};
36/// use ailloli_ui_runtime::scene::DrawRRect;
37/// let draw = DrawRRect { rect: Rect::new(0.0, 0.0, 20.0, 10.0), radius: 4.0, color: Color::WHITE };
38/// assert_eq!(draw.radius, 4.0);
39/// ```
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct DrawRRect {
42 /// Filled rectangle in window-space logical pixels.
43 pub rect: Rect,
44 /// Corner radius in logical pixels (clamped by the renderer).
45 pub radius: f32,
46 /// Linear-RGBA fill color.
47 pub color: Color,
48}
49
50/// Layout-aware border drawn around a rounded rectangle.
51///
52/// # Examples
53///
54/// ```
55/// use ailloli_ui_core::{Border, Color, Radius, Rect};
56/// use ailloli_ui_runtime::scene::DrawBorder;
57/// let draw = DrawBorder {
58/// rect: Rect::new(0.0, 0.0, 40.0, 20.0),
59/// radius: Radius::uniform(3.0),
60/// border: Border::new(1.0, Color::WHITE),
61/// };
62/// assert_eq!(draw.border.widths.left, 1.0);
63/// ```
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct DrawBorder {
66 /// Border box in window-space logical pixels.
67 pub rect: Rect,
68 /// Per-corner radii in logical pixels.
69 pub radius: Radius,
70 /// Per-edge widths, colors, and stroke style.
71 pub border: Border,
72}
73
74/// Paint-only shadow associated with a rounded source rectangle.
75///
76/// # Examples
77///
78/// ```
79/// use ailloli_ui_core::{BoxShadow, Radius, Rect};
80/// use ailloli_ui_runtime::scene::DrawBoxShadow;
81/// let draw = DrawBoxShadow {
82/// rect: Rect::new(0.0, 0.0, 40.0, 20.0),
83/// radius: Radius::uniform(2.0),
84/// shadow: BoxShadow::sm(),
85/// };
86/// assert_eq!(draw.shadow.blur_radius, 2.0);
87/// ```
88#[derive(Debug, Clone, Copy, PartialEq)]
89pub struct DrawBoxShadow {
90 /// Source box in window-space logical pixels.
91 pub rect: Rect,
92 /// Source-box corner radii in logical pixels.
93 pub radius: Radius,
94 /// Shadow offset, blur, spread, color, and inset flag.
95 pub shadow: BoxShadow,
96}
97
98/// Circular progress-ring primitive.
99///
100/// Numeric inputs are stored verbatim. The conventional fraction range is
101/// `0.0..=1.0`, but this type does not clamp it or reject negative/non-finite
102/// thicknesses and angles.
103///
104/// # Examples
105///
106/// ```
107/// use ailloli_ui_core::{Color, Rect};
108/// use ailloli_ui_runtime::scene::DrawRingProgress;
109/// let ring = DrawRingProgress {
110/// rect: Rect::new(0.0, 0.0, 24.0, 24.0), thickness: 2.0, fraction: 0.5,
111/// track_color: Color::BLACK, fill_color: Color::WHITE,
112/// start_angle: -std::f32::consts::FRAC_PI_2,
113/// };
114/// assert_eq!(ring.fraction, 0.5);
115/// ```
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct DrawRingProgress {
118 /// Outer ring rectangle in window-space logical pixels.
119 pub rect: Rect,
120 /// Requested stroke thickness in logical pixels.
121 pub thickness: f32,
122 /// Requested filled revolution fraction; conventionally `0.0..=1.0`.
123 pub fraction: f32,
124 /// Linear-RGBA color of the complete background track.
125 pub track_color: Color,
126 /// Linear-RGBA color of the fractional foreground arc.
127 pub fill_color: Color,
128 /// Radians, with `-FRAC_PI_2` meaning top-center start.
129 pub start_angle: f32,
130}
131
132/// Connected line segments rendered with one stroke style.
133///
134/// Zero or one point cannot form a segment but is accepted; allocation size is
135/// proportional to the caller-provided point count.
136///
137/// # Examples
138///
139/// ```
140/// use ailloli_ui_core::{Color, Point, StrokeStyle};
141/// use ailloli_ui_runtime::scene::DrawPolyline;
142/// let line = DrawPolyline {
143/// points: vec![Point::new(0.0, 0.0), Point::new(10.0, 5.0)],
144/// stroke: StrokeStyle::new(2.0, Color::WHITE),
145/// };
146/// assert_eq!(line.points.len(), 2);
147/// ```
148#[derive(Debug, Clone, PartialEq)]
149pub struct DrawPolyline {
150 /// Vertices in window-space logical pixels, in traversal order.
151 pub points: Vec<Point>,
152 /// Width, color, cap, join, and miter settings for every segment.
153 pub stroke: StrokeStyle,
154}
155
156/// Prepared shaped text positioned by its first baseline.
157///
158/// The shared layout keeps source text, glyphs, and line metrics alive. Color
159/// is the uniform paint color; styled layouts may additionally carry per-glyph
160/// colors. All positions and metrics are logical pixels.
161///
162/// # Examples
163///
164/// ```
165/// use std::sync::Arc;
166/// use ailloli_ui_core::{Color, FontId, TextDecoration, TextStyle};
167/// use ailloli_ui_runtime::scene::DrawText;
168/// use ailloli_ui_text::{TextLayoutParams, TextSystem};
169/// let mut system = TextSystem::new();
170/// let layout = system.layout_cached(TextLayoutParams::new(
171/// "hello", TextStyle::new(FontId::Ui, 14, Color::WHITE),
172/// ));
173/// let draw = DrawText { pos: [5.0, 20.0], color: Color::WHITE,
174/// decoration: TextDecoration::None, layout };
175/// assert_eq!(draw.layout.text(), "hello");
176/// ```
177#[derive(Debug, Clone)]
178pub struct DrawText {
179 /// `[x, first_baseline_y]` in window-space logical pixels.
180 pub pos: [f32; 2],
181 /// Uniform linear-RGBA paint color.
182 pub color: Color,
183 /// Requested paint-only text decoration.
184 pub decoration: TextDecoration,
185 /// Shared, renderer-ready shaped layout.
186 pub layout: Arc<PreparedTextLayout>,
187}
188
189/// Provides the operations defined for DrawText.
190impl DrawText {
191 /// Returns logical-pixel underline rectangles for non-empty visual lines.
192 ///
193 /// Decorations other than underline return an empty vector. `dpr` is
194 /// clamped to at least `0.01` using floating-point `max`; NaN therefore also
195 /// becomes `0.01`, while positive infinity can yield non-finite snapped
196 /// coordinates. Each underline is at least one physical pixel thick, uses
197 /// the line width, and is vertically snapped to the effective DPR.
198 ///
199 /// # Examples
200 ///
201 /// ```
202 /// use ailloli_ui_core::{Color, FontId, TextDecoration, TextStyle};
203 /// use ailloli_ui_runtime::scene::DrawText;
204 /// use ailloli_ui_text::{TextLayoutParams, TextSystem};
205 /// let mut system = TextSystem::new();
206 /// let layout = system.layout_cached(TextLayoutParams::new(
207 /// "underlined", TextStyle::new(FontId::Ui, 14, Color::WHITE),
208 /// ));
209 /// let draw = DrawText { pos: [0.0, layout.lines[0].baseline_y], color: Color::WHITE,
210 /// decoration: TextDecoration::Underline, layout };
211 /// assert_eq!(draw.decoration_rects(2.0).len(), 1);
212 /// assert!(draw.decoration_rects(2.0)[0].h >= 0.5);
213 /// ```
214 pub fn decoration_rects(&self, dpr: f32) -> Vec<Rect> {
215 if self.decoration != TextDecoration::Underline {
216 return Vec::new();
217 }
218 let dpr = dpr.max(0.01);
219 let first_baseline = self
220 .layout
221 .lines
222 .first()
223 .map(|line| line.baseline_y)
224 .unwrap_or(0.0);
225 let origin_y = self.pos[1] - first_baseline;
226 self.layout
227 .lines
228 .iter()
229 .filter(|line| line.width > 0.0)
230 .map(|line| {
231 let thickness = (line.descent / 3.0).max(1.0 / dpr);
232 let y = origin_y + line.baseline_y + (line.descent * 0.5).max(thickness * 0.5);
233 let snapped_y = (y * dpr).round() / dpr;
234 Rect::new(self.pos[0], snapped_y, line.width, thickness)
235 })
236 .collect()
237 }
238}
239
240/// Icon or image draw primitive.
241///
242/// Rotation is in radians around the renderer-defined image center. Values are
243/// stored verbatim and tint uses linear RGBA.
244///
245/// # Examples
246///
247/// ```
248/// use ailloli_ui_core::{Color, IconId, Rect};
249/// use ailloli_ui_runtime::scene::DrawImage;
250/// let image = DrawImage {
251/// rect: Rect::new(0.0, 0.0, 16.0, 16.0), icon: IconId::Check,
252/// tint: Color::WHITE, rotation_rad: 0.0,
253/// };
254/// assert_eq!(image.rotation_rad, 0.0);
255/// ```
256#[derive(Debug, Clone, PartialEq)]
257pub struct DrawImage {
258 /// Destination rectangle in window-space logical pixels.
259 pub rect: Rect,
260 /// Provider-neutral image/icon identifier.
261 pub icon: IconId,
262 /// Linear-RGBA multiplicative tint.
263 pub tint: Color,
264 /// Clockwise rotation in radians; zero means unrotated.
265 pub rotation_rad: f32,
266}
267
268/// Single paint primitive emitted during the paint phase.
269///
270/// Commands retain their data and are executed later by a renderer. The enum
271/// does not validate geometry, clamp colors, or guarantee backend support.
272///
273/// # Examples
274///
275/// ```
276/// use ailloli_ui_core::{Color, Rect};
277/// use ailloli_ui_runtime::scene::{DrawCmd, DrawRect};
278/// let cmd = DrawCmd::Rect(DrawRect { rect: Rect::new(0.0, 0.0, 8.0, 8.0), color: Color::WHITE });
279/// assert!(matches!(cmd, DrawCmd::Rect(_)));
280/// ```
281#[derive(Debug, Clone)]
282pub enum DrawCmd {
283 /// Solid rectangle.
284 Rect(DrawRect),
285 /// Rounded rectangle.
286 RRect(DrawRRect),
287 /// Layout-aware box border.
288 Border(DrawBorder),
289 /// Paint-only box shadow.
290 BoxShadow(DrawBoxShadow),
291 /// Circular progress ring with a complete track and fractional fill.
292 RingProgress(DrawRingProgress),
293 /// Stroked polyline.
294 Polyline(DrawPolyline),
295 /// Shaped text at a baseline position.
296 Text(DrawText),
297 /// Icon or image in a rect.
298 Image(DrawImage),
299}