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