1use crate::{
2 BaselineShift, Brush, ClipOp, Color, DrawStyle, FontStyle, FontSynthesis, FontWeight, Modifier,
3 Rect, TextAlign, TextDecoration, TextDirection, TextSpan, Transform,
4};
5use std::{fmt::Formatter, rc::Rc, sync::Arc};
6
7#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct SubcomposeScope {
11 pub min_width: f32,
12 pub max_width: f32,
13 pub min_height: f32,
14 pub max_height: f32,
15}
16
17impl SubcomposeScope {
18 pub const UNBOUNDED: Self = Self {
21 min_width: 0.0,
22 max_width: f32::INFINITY,
23 min_height: 0.0,
24 max_height: f32::INFINITY,
25 };
26
27 pub fn new(min_width: f32, max_width: f32, min_height: f32, max_height: f32) -> Self {
29 Self {
30 min_width,
31 max_width,
32 min_height,
33 max_height,
34 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
41pub struct BoxWithConstraintsScope {
42 pub min_width: f32,
43 pub max_width: f32,
44 pub min_height: f32,
45 pub max_height: f32,
46}
47
48impl BoxWithConstraintsScope {
49 pub fn has_bounded_width(&self) -> bool {
51 self.max_width.is_finite()
52 }
53
54 pub fn has_bounded_height(&self) -> bool {
56 self.max_height.is_finite()
57 }
58}
59
60pub type ViewId = u64;
61
62pub type ImageHandle = u64;
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[non_exhaustive]
65pub enum ImageFit {
66 Contain,
67 Cover,
68 FitWidth,
69 FitHeight,
70}
71
72pub type Callback = Rc<dyn Fn()>;
73
74#[derive(Clone)]
75pub struct OverlayEntry {
76 pub id: u64,
77 pub view: Box<View>,
78}
79
80#[derive(Clone)]
81#[non_exhaustive]
82pub enum ViewKind {
83 Box,
84 Row,
85 Column,
86 Stack,
87 ZStack,
88 OverlayHost,
89 Text {
90 text: String,
91 color: Color,
92 font_size: f32,
93 soft_wrap: bool,
94 max_lines: Option<usize>,
95 overflow: TextOverflow,
96 font_family: Option<&'static str>,
97 annotations: Option<Arc<[TextSpan]>>,
98 text_align: TextAlign,
99 font_weight: FontWeight,
100 font_style: FontStyle,
101 text_decoration: TextDecoration,
102 letter_spacing: f32,
103 line_height: f32,
104 url: Option<Arc<str>>,
106 font_variation_settings: Option<Arc<str>>,
108 },
109
110 Image {
111 handle: ImageHandle,
112 tint: Color, fit: ImageFit,
114 },
115 SubcomposeLayout {
125 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
126 },
127 Expander {
130 expanded: bool,
131 on_toggle: Option<Callback>,
132 },
133 TreeRow {
136 depth: usize,
137 has_children: bool,
138 is_expanded: bool,
139 is_selected: bool,
140 on_toggle: Option<Callback>,
141 on_select: Option<Callback>,
142 },
143}
144
145impl std::fmt::Debug for ViewKind {
146 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147 match self {
148 Self::Box => f.write_str("Box"),
149 Self::Row => f.write_str("Row"),
150 Self::Column => f.write_str("Column"),
151 Self::Stack => f.write_str("Stack"),
152 Self::ZStack => f.write_str("ZStack"),
153 Self::OverlayHost => f.write_str("OverlayHost"),
154
155 Self::Image { .. } => f.write_str("Image"),
156 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
157 Self::Text { text, .. } => write!(f, "Text({:?})", text),
158
159 Self::Expander { expanded, .. } => {
160 if *expanded {
161 write!(f, "Expander(expanded)")
162 } else {
163 write!(f, "Expander(collapsed)")
164 }
165 }
166 Self::TreeRow {
167 depth,
168 has_children,
169 is_expanded,
170 is_selected,
171 ..
172 } => {
173 write!(
174 f,
175 "TreeRow(depth={}, children={}, expanded={}, selected={})",
176 depth, has_children, is_expanded, is_selected
177 )
178 }
179 }
180 }
181}
182
183#[derive(Clone, Debug)]
184pub struct View {
185 pub id: ViewId,
186 pub kind: ViewKind,
187 pub modifier: Modifier,
188 pub children: Vec<View>,
189 pub semantics: Option<crate::semantics::Semantics>,
190 pub scope_key: Option<String>,
194}
195
196impl View {
197 pub fn new(id: ViewId, kind: ViewKind) -> Self {
198 View {
199 id,
200 kind,
201 modifier: Modifier::default(),
202 children: vec![],
203 semantics: None,
204 scope_key: None,
205 }
206 }
207 pub fn modifier(mut self, m: Modifier) -> Self {
208 self.modifier = m;
209 self
210 }
211 pub fn disabled(mut self) -> Self {
213 self.modifier.disabled = true;
214 self
215 }
216 pub fn with_children(mut self, kids: Vec<View>) -> Self {
217 self.children = kids;
218 self
219 }
220 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
221 self.semantics = Some(s);
222 self
223 }
224}
225
226#[derive(Clone, Debug, Default)]
228pub struct Scene {
229 pub clear_color: Color,
230 pub nodes: Vec<SceneNode>,
231}
232
233#[derive(Clone, Debug, PartialEq)]
235pub struct TextExtraStyle {
236 pub text_direction: TextDirection,
237 pub font_synthesis: FontSynthesis,
238 pub baseline_shift: BaselineShift,
239 pub draw_style: DrawStyle,
240}
241
242impl Default for TextExtraStyle {
243 fn default() -> Self {
244 Self {
245 text_direction: TextDirection::Ltr,
246 font_synthesis: FontSynthesis::Unspecified,
247 baseline_shift: BaselineShift::Unspecified,
248 draw_style: DrawStyle::Fill,
249 }
250 }
251}
252
253#[derive(Clone, Debug)]
254#[non_exhaustive]
255pub enum SceneNode {
256 Rect {
257 rect: Rect,
258 brush: Brush,
259 radius: [f32; 4],
260 },
261 Border {
262 rect: Rect,
263 color: Color,
264 width: f32,
265 radius: [f32; 4],
266 },
267 Text {
268 rect: Rect,
269 text: Arc<str>,
270 color: Color,
271 size: f32,
272 font_family: Option<&'static str>,
273 text_align: TextAlign,
274 font_weight: FontWeight,
275 font_style: FontStyle,
276 text_decoration: TextDecoration,
277 letter_spacing: f32,
278 line_height: f32,
279 extra_style: TextExtraStyle,
281 url: Option<Arc<str>>,
283 font_variation_settings: Option<Arc<str>>,
285 },
286 Ellipse {
287 rect: Rect,
288 brush: Brush,
289 },
290 EllipseBorder {
291 rect: Rect,
292 color: Color,
293 width: f32, },
295 PushClip {
296 rect: Rect,
297 radius: [f32; 4],
298 op: ClipOp,
299 },
300 PopClip,
301 PushTransform {
302 transform: Transform,
303 },
304 PopTransform,
305 Image {
306 rect: Rect,
307 handle: ImageHandle,
308 tint: Color,
309 fit: ImageFit,
310 },
311 Shadow {
314 rect: Rect,
315 radius: [f32; 4],
316 elevation: f32,
317 color: Color,
318 },
319 BeginLayer {
327 rect: Rect,
328 layer_id: u32,
329 alpha: f32,
330 blur_radius_x: f32,
331 blur_radius_y: f32,
332 rectangle_edge: bool,
333 },
334 EndLayer {
336 layer_id: u32,
337 },
338 CompositeShadow {
343 layer_id: u32,
344 blur_px: f32,
345 offset_px: (f32, f32),
346 color: Color,
347 },
348 Arc {
350 rect: Rect,
351 start_angle: f32,
352 sweep_angle: f32,
353 stroke_width: f32,
354 color: Color,
355 cap: StrokeCap,
356 },
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360#[non_exhaustive]
361pub enum TextOverflow {
362 Visible,
363 Clip,
364 Ellipsis,
365}
366
367#[derive(Clone, Copy, Debug, PartialEq, Default)]
369pub enum StrokeJoin {
370 #[default]
371 Miter,
373 Round,
375 Bevel,
377}
378
379#[derive(Clone, Copy, Debug, PartialEq, Default)]
381pub enum StrokeCap {
382 #[default]
383 Butt,
385 Round,
388 Square,
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn subcompose_scope_unbounded_has_infinite_max() {
399 let s = SubcomposeScope::UNBOUNDED;
400 assert!(!s.max_width.is_finite());
401 assert!(!s.max_height.is_finite());
402 assert_eq!(s.min_width, 0.0);
403 assert_eq!(s.min_height, 0.0);
404 }
405
406 #[test]
407 fn subcompose_scope_new_round_trips() {
408 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
409 assert_eq!(s.min_width, 10.0);
410 assert_eq!(s.max_width, 200.0);
411 assert_eq!(s.min_height, 20.0);
412 assert_eq!(s.max_height, 300.0);
413 }
414
415 #[test]
416 fn box_with_constraints_scope_bounded_predicates() {
417 let bounded = BoxWithConstraintsScope {
418 min_width: 0.0,
419 max_width: 360.0,
420 min_height: 0.0,
421 max_height: 640.0,
422 };
423 assert!(bounded.has_bounded_width());
424 assert!(bounded.has_bounded_height());
425
426 let unbounded = BoxWithConstraintsScope {
427 min_width: 0.0,
428 max_width: f32::INFINITY,
429 min_height: 0.0,
430 max_height: f32::INFINITY,
431 };
432 assert!(!unbounded.has_bounded_width());
433 assert!(!unbounded.has_bounded_height());
434 }
435
436 #[test]
437 fn view_kind_subcompose_layout_holds_closure() {
438 let v: View = View {
439 id: 0,
440 kind: ViewKind::SubcomposeLayout {
441 content: std::sync::Arc::new(|scope| {
442 let _ = scope.max_width;
443 vec![(0, View::new(0, ViewKind::Box))]
444 }),
445 },
446 modifier: Modifier::default(),
447 children: vec![],
448 scope_key: None,
449 semantics: None,
450 };
451 match &v.kind {
452 ViewKind::SubcomposeLayout { .. } => {}
453 _ => panic!("expected SubcomposeLayout"),
454 }
455 }
456
457 #[test]
458 fn view_kind_subcompose_layout_supports_multiple_slots() {
459 let v: View = View {
460 id: 0,
461 kind: ViewKind::SubcomposeLayout {
462 content: std::sync::Arc::new(|_scope| {
463 vec![
464 (1, View::new(0, ViewKind::Box)),
465 (2, View::new(0, ViewKind::Box)),
466 (3, View::new(0, ViewKind::Box)),
467 ]
468 }),
469 },
470 modifier: Modifier::default(),
471 children: vec![],
472 scope_key: None,
473 semantics: None,
474 };
475 if let ViewKind::SubcomposeLayout { content } = &v.kind {
476 let slots = content(&SubcomposeScope::UNBOUNDED);
477 assert_eq!(slots.len(), 3);
478 assert_eq!(slots[0].0, 1);
479 assert_eq!(slots[1].0, 2);
480 assert_eq!(slots[2].0, 3);
481 } else {
482 panic!("expected SubcomposeLayout");
483 }
484 }
485}