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]
427#[derive(Default)]
428pub enum BlendMode {
429 #[default]
431 Alpha,
432 Add,
434 Multiply,
436 Overlay,
438}
439
440#[derive(Clone, Copy, Debug, PartialEq, Eq)]
441#[non_exhaustive]
442pub enum TextOverflow {
443 Visible,
444 Clip,
445 Ellipsis,
446}
447
448#[derive(Clone, Copy, Debug, PartialEq, Default)]
450pub enum StrokeJoin {
451 #[default]
452 Miter,
454 Round,
456 Bevel,
458}
459
460#[derive(Clone, Copy, Debug, PartialEq, Default)]
462pub enum StrokeCap {
463 #[default]
464 Butt,
466 Round,
469 Square,
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn subcompose_scope_unbounded_has_infinite_max() {
480 let s = SubcomposeScope::UNBOUNDED;
481 assert!(!s.max_width.is_finite());
482 assert!(!s.max_height.is_finite());
483 assert_eq!(s.min_width, 0.0);
484 assert_eq!(s.min_height, 0.0);
485 }
486
487 #[test]
488 fn subcompose_scope_new_round_trips() {
489 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
490 assert_eq!(s.min_width, 10.0);
491 assert_eq!(s.max_width, 200.0);
492 assert_eq!(s.min_height, 20.0);
493 assert_eq!(s.max_height, 300.0);
494 }
495
496 #[test]
497 fn box_with_constraints_scope_bounded_predicates() {
498 let bounded = BoxWithConstraintsScope {
499 min_width: 0.0,
500 max_width: 360.0,
501 min_height: 0.0,
502 max_height: 640.0,
503 };
504 assert!(bounded.has_bounded_width());
505 assert!(bounded.has_bounded_height());
506
507 let unbounded = BoxWithConstraintsScope {
508 min_width: 0.0,
509 max_width: f32::INFINITY,
510 min_height: 0.0,
511 max_height: f32::INFINITY,
512 };
513 assert!(!unbounded.has_bounded_width());
514 assert!(!unbounded.has_bounded_height());
515 }
516
517 #[test]
518 fn view_kind_subcompose_layout_holds_closure() {
519 let v: View = View {
520 id: 0,
521 kind: ViewKind::SubcomposeLayout {
522 content: std::sync::Arc::new(|scope| {
523 let _ = scope.max_width;
524 vec![(0, View::new(0, ViewKind::Box))]
525 }),
526 },
527 modifier: Modifier::default(),
528 children: vec![],
529 scope_key: None,
530 semantics: None,
531 };
532 match &v.kind {
533 ViewKind::SubcomposeLayout { .. } => {}
534 _ => panic!("expected SubcomposeLayout"),
535 }
536 }
537
538 #[test]
539 fn view_kind_subcompose_layout_supports_multiple_slots() {
540 let v: View = View {
541 id: 0,
542 kind: ViewKind::SubcomposeLayout {
543 content: std::sync::Arc::new(|_scope| {
544 vec![
545 (1, View::new(0, ViewKind::Box)),
546 (2, View::new(0, ViewKind::Box)),
547 (3, View::new(0, ViewKind::Box)),
548 ]
549 }),
550 },
551 modifier: Modifier::default(),
552 children: vec![],
553 scope_key: None,
554 semantics: None,
555 };
556 if let ViewKind::SubcomposeLayout { content } = &v.kind {
557 let slots = content(&SubcomposeScope::UNBOUNDED);
558 assert_eq!(slots.len(), 3);
559 assert_eq!(slots[0].0, 1);
560 assert_eq!(slots[1].0, 2);
561 assert_eq!(slots[2].0, 3);
562 } else {
563 panic!("expected SubcomposeLayout");
564 }
565 }
566}