1use crate::{
2 BaselineShift, Brush, ClipOp, Color, DrawStyle, FontStyle, FontSynthesis, FontWeight, Modifier,
3 Rect, TextAlign, TextDecoration, TextDirection, TextSpan, Transform, Vec2,
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 ZStack,
87 OverlayHost,
88 Text {
89 text: String,
90 color: Color,
91 font_size: f32,
92 soft_wrap: bool,
93 max_lines: Option<usize>,
94 overflow: TextOverflow,
95 font_family: Option<&'static str>,
96 annotations: Option<Arc<[TextSpan]>>,
97 text_align: TextAlign,
98 font_weight: FontWeight,
99 font_style: FontStyle,
100 text_decoration: TextDecoration,
101 letter_spacing: f32,
102 line_height: f32,
103 url: Option<Arc<str>>,
105 font_variation_settings: Option<Arc<str>>,
107 },
108
109 Image {
110 handle: ImageHandle,
111 tint: Color, fit: ImageFit,
113 },
114 SubcomposeLayout {
124 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
125 },
126 Expander {
129 expanded: bool,
130 on_toggle: Option<Callback>,
131 },
132 TreeRow {
135 depth: usize,
136 has_children: bool,
137 is_expanded: bool,
138 is_selected: bool,
139 on_toggle: Option<Callback>,
140 on_select: Option<Callback>,
141 },
142}
143
144impl std::fmt::Debug for ViewKind {
145 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 match self {
147 Self::Box => f.write_str("Box"),
148 Self::Row => f.write_str("Row"),
149 Self::Column => f.write_str("Column"),
150 Self::ZStack => f.write_str("ZStack"),
151 Self::OverlayHost => f.write_str("OverlayHost"),
152
153 Self::Image { .. } => f.write_str("Image"),
154 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
155 Self::Text { text, .. } => write!(f, "Text({:?})", text),
156
157 Self::Expander { expanded, .. } => {
158 if *expanded {
159 write!(f, "Expander(expanded)")
160 } else {
161 write!(f, "Expander(collapsed)")
162 }
163 }
164 Self::TreeRow {
165 depth,
166 has_children,
167 is_expanded,
168 is_selected,
169 ..
170 } => {
171 write!(
172 f,
173 "TreeRow(depth={}, children={}, expanded={}, selected={})",
174 depth, has_children, is_expanded, is_selected
175 )
176 }
177 }
178 }
179}
180
181#[derive(Clone, Debug)]
182pub struct View {
183 pub id: ViewId,
184 pub kind: ViewKind,
185 pub modifier: Modifier,
186 pub children: Vec<View>,
187 pub semantics: Option<crate::semantics::Semantics>,
188 pub scope_key: Option<String>,
192}
193
194impl View {
195 pub fn new(id: ViewId, kind: ViewKind) -> Self {
196 View {
197 id,
198 kind,
199 modifier: Modifier::default(),
200 children: vec![],
201 semantics: None,
202 scope_key: None,
203 }
204 }
205 pub fn modifier(mut self, m: Modifier) -> Self {
206 self.modifier = m;
207 self
208 }
209 pub fn disabled(mut self) -> Self {
211 self.modifier.disabled = true;
212 self
213 }
214 pub fn with_children(mut self, kids: Vec<View>) -> Self {
215 self.children = kids;
216 self
217 }
218 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
219 self.children = kids.into();
220 self
221 }
222 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
223 self.semantics = Some(s);
224 self
225 }
226}
227
228#[derive(Clone, Debug, Default)]
230pub struct Scene {
231 pub clear_color: Color,
232 pub nodes: Vec<SceneNode>,
233}
234
235#[derive(Clone, Debug, PartialEq)]
237pub struct TextExtraStyle {
238 pub text_direction: TextDirection,
239 pub font_synthesis: FontSynthesis,
240 pub baseline_shift: BaselineShift,
241 pub draw_style: DrawStyle,
242}
243
244impl Default for TextExtraStyle {
245 fn default() -> Self {
246 Self {
247 text_direction: TextDirection::Ltr,
248 font_synthesis: FontSynthesis::Unspecified,
249 baseline_shift: BaselineShift::Unspecified,
250 draw_style: DrawStyle::Fill,
251 }
252 }
253}
254
255#[derive(Clone, Debug)]
256#[non_exhaustive]
257pub enum SceneNode {
258 Rect {
259 rect: Rect,
260 brush: Brush,
261 radius: [f32; 4],
262 },
263 Border {
264 rect: Rect,
265 color: Color,
266 width: f32,
267 radius: [f32; 4],
268 },
269 Text {
270 rect: Rect,
271 text: Arc<str>,
272 color: Color,
273 size: f32,
274 font_family: Option<&'static str>,
275 text_align: TextAlign,
276 font_weight: FontWeight,
277 font_style: FontStyle,
278 text_decoration: TextDecoration,
279 letter_spacing: f32,
280 line_height: f32,
281 extra_style: TextExtraStyle,
283 url: Option<Arc<str>>,
285 font_variation_settings: Option<Arc<str>>,
287 },
288 Ellipse {
289 rect: Rect,
290 brush: Brush,
291 },
292 EllipseBorder {
293 rect: Rect,
294 color: Color,
295 width: f32, },
297 PushClip {
298 rect: Rect,
299 radius: [f32; 4],
300 op: ClipOp,
301 },
302 PopClip,
303 PushTransform {
304 transform: Transform,
305 },
306 PopTransform,
307 Image {
308 rect: Rect,
309 handle: ImageHandle,
310 tint: Color,
311 fit: ImageFit,
312 },
313 Shadow {
316 rect: Rect,
317 radius: [f32; 4],
318 elevation: f32,
319 color: Color,
320 },
321 BeginLayer {
329 rect: Rect,
330 layer_id: u32,
331 alpha: f32,
332 blur_radius_x: f32,
333 blur_radius_y: f32,
334 rectangle_edge: bool,
335 },
336 EndLayer {
338 layer_id: u32,
339 },
340 CompositeShadow {
345 layer_id: u32,
346 blur_px: f32,
347 offset_px: (f32, f32),
348 color: Color,
349 },
350 Arc {
352 rect: Rect,
353 start_angle: f32,
354 sweep_angle: f32,
355 stroke_width: f32,
356 color: Color,
357 cap: StrokeCap,
358 },
359 VectorMesh {
365 mesh: Arc<VectorMeshData>,
366 transform: [f32; 6],
367 paint: PaintDesc,
368 clip: Option<u32>,
371 blend: BlendMode,
372 },
373 VectorOverlay {
378 meshes: Arc<[VectorMeshData]>,
379 },
380 PushVectorClip {
384 mesh: Arc<VectorMeshData>,
385 },
386 PopVectorClip,
388}
389
390#[derive(Clone, Debug, Default)]
392pub struct VectorMeshData {
393 pub vertices: Arc<[VectorVertex]>,
394 pub indices: Arc<[u32]>,
395}
396
397#[derive(Clone, Copy, Debug, PartialEq)]
401#[repr(C)]
402pub struct VectorVertex {
403 pub pos: [f32; 2],
404 pub color: [f32; 4],
405 pub uv: [f32; 2],
406}
407
408#[derive(Clone, Copy, Debug, PartialEq)]
410#[non_exhaustive]
411pub enum PaintDesc {
412 Solid,
414 Linear {
416 start: Vec2,
417 end: Vec2,
418 start_color: Color,
419 end_color: Color,
420 },
421}
422
423#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum BlendMode {
428 Alpha,
430 Add,
432 Multiply,
434 Overlay,
436}
437
438impl Default for BlendMode {
439 fn default() -> Self {
440 BlendMode::Alpha
441 }
442}
443
444#[derive(Clone, Copy, Debug, PartialEq, Eq)]
445#[non_exhaustive]
446pub enum TextOverflow {
447 Visible,
448 Clip,
449 Ellipsis,
450}
451
452#[derive(Clone, Copy, Debug, PartialEq, Default)]
454pub enum StrokeJoin {
455 #[default]
456 Miter,
458 Round,
460 Bevel,
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Default)]
466pub enum StrokeCap {
467 #[default]
468 Butt,
470 Round,
473 Square,
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn subcompose_scope_unbounded_has_infinite_max() {
484 let s = SubcomposeScope::UNBOUNDED;
485 assert!(!s.max_width.is_finite());
486 assert!(!s.max_height.is_finite());
487 assert_eq!(s.min_width, 0.0);
488 assert_eq!(s.min_height, 0.0);
489 }
490
491 #[test]
492 fn subcompose_scope_new_round_trips() {
493 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
494 assert_eq!(s.min_width, 10.0);
495 assert_eq!(s.max_width, 200.0);
496 assert_eq!(s.min_height, 20.0);
497 assert_eq!(s.max_height, 300.0);
498 }
499
500 #[test]
501 fn box_with_constraints_scope_bounded_predicates() {
502 let bounded = BoxWithConstraintsScope {
503 min_width: 0.0,
504 max_width: 360.0,
505 min_height: 0.0,
506 max_height: 640.0,
507 };
508 assert!(bounded.has_bounded_width());
509 assert!(bounded.has_bounded_height());
510
511 let unbounded = BoxWithConstraintsScope {
512 min_width: 0.0,
513 max_width: f32::INFINITY,
514 min_height: 0.0,
515 max_height: f32::INFINITY,
516 };
517 assert!(!unbounded.has_bounded_width());
518 assert!(!unbounded.has_bounded_height());
519 }
520
521 #[test]
522 fn view_kind_subcompose_layout_holds_closure() {
523 let v: View = View {
524 id: 0,
525 kind: ViewKind::SubcomposeLayout {
526 content: std::sync::Arc::new(|scope| {
527 let _ = scope.max_width;
528 vec![(0, View::new(0, ViewKind::Box))]
529 }),
530 },
531 modifier: Modifier::default(),
532 children: vec![],
533 scope_key: None,
534 semantics: None,
535 };
536 match &v.kind {
537 ViewKind::SubcomposeLayout { .. } => {}
538 _ => panic!("expected SubcomposeLayout"),
539 }
540 }
541
542 #[test]
543 fn view_kind_subcompose_layout_supports_multiple_slots() {
544 let v: View = View {
545 id: 0,
546 kind: ViewKind::SubcomposeLayout {
547 content: std::sync::Arc::new(|_scope| {
548 vec![
549 (1, View::new(0, ViewKind::Box)),
550 (2, View::new(0, ViewKind::Box)),
551 (3, View::new(0, ViewKind::Box)),
552 ]
553 }),
554 },
555 modifier: Modifier::default(),
556 children: vec![],
557 scope_key: None,
558 semantics: None,
559 };
560 if let ViewKind::SubcomposeLayout { content } = &v.kind {
561 let slots = content(&SubcomposeScope::UNBOUNDED);
562 assert_eq!(slots.len(), 3);
563 assert_eq!(slots[0].0, 1);
564 assert_eq!(slots[1].0, 2);
565 assert_eq!(slots[2].0, 3);
566 } else {
567 panic!("expected SubcomposeLayout");
568 }
569 }
570}