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, Copy, Debug)]
266pub struct PaintCallbackInfo {
267 pub viewport: Rect,
269 pub clip_rect: Rect,
271 pub pixels_per_point: f32,
273 pub screen_size_px: [u32; 2],
275}
276
277pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
278
279#[derive(Clone, Debug)]
280#[non_exhaustive]
281pub enum SceneNode {
282 Rect {
283 rect: Rect,
284 brush: Brush,
285 radius: [f32; 4],
286 },
287 Border {
288 rect: Rect,
289 color: Color,
290 width: f32,
291 radius: [f32; 4],
292 },
293 Text {
294 rect: Rect,
295 text: Arc<str>,
296 color: Color,
297 size: f32,
298 font_family: Option<&'static str>,
299 text_align: TextAlign,
300 font_weight: FontWeight,
301 font_style: FontStyle,
302 text_decoration: TextDecoration,
303 letter_spacing: f32,
304 line_height: f32,
305 extra_style: TextExtraStyle,
307 url: Option<Arc<str>>,
309 font_variation_settings: Option<Arc<str>>,
311 },
312 Ellipse {
313 rect: Rect,
314 brush: Brush,
315 },
316 EllipseBorder {
317 rect: Rect,
318 color: Color,
319 width: f32, },
321 PushClip {
322 rect: Rect,
323 radius: [f32; 4],
324 op: ClipOp,
325 },
326 PopClip,
327 PushTransform {
328 transform: Transform,
329 },
330 PopTransform,
331 Image {
332 rect: Rect,
333 handle: ImageHandle,
334 tint: Color,
335 fit: ImageFit,
336 },
337 Shadow {
340 rect: Rect,
341 radius: [f32; 4],
342 elevation: f32,
343 color: Color,
344 },
345 BeginLayer {
352 rect: Rect,
353 layer_id: u32,
354 alpha: f32,
355 blur_radius_x: f32,
356 blur_radius_y: f32,
357 rectangle_edge: bool,
358 },
359 EndLayer {
361 layer_id: u32,
362 },
363 CompositeShadow {
368 layer_id: u32,
369 blur_px: f32,
370 offset_px: (f32, f32),
371 color: Color,
372 },
373 Arc {
375 rect: Rect,
376 start_angle: f32,
377 sweep_angle: f32,
378 stroke_width: f32,
379 color: Color,
380 cap: StrokeCap,
381 },
382 VectorMesh {
388 mesh: Arc<VectorMeshData>,
389 transform: [f32; 6],
390 paint: PaintDesc,
391 clip: Option<u32>,
394 blend: BlendMode,
395 },
396 VectorOverlay {
400 meshes: Arc<[VectorMeshData]>,
401 },
402 PushVectorClip {
406 mesh: Arc<VectorMeshData>,
407 },
408 PopVectorClip,
410 Callback {
412 rect: Rect,
413 payload: PaintCallbackPayload,
414 },
415}
416
417#[derive(Clone, Debug, Default)]
419pub struct VectorMeshData {
420 pub vertices: Arc<[VectorVertex]>,
421 pub indices: Arc<[u32]>,
422}
423
424#[derive(Clone, Copy, Debug, PartialEq)]
428#[repr(C)]
429pub struct VectorVertex {
430 pub pos: [f32; 2],
431 pub color: [f32; 4],
432 pub uv: [f32; 2],
433}
434
435#[derive(Clone, Copy, Debug, PartialEq)]
437#[non_exhaustive]
438pub enum PaintDesc {
439 Solid,
441 Linear {
443 start: Vec2,
444 end: Vec2,
445 start_color: Color,
446 end_color: Color,
447 },
448}
449
450#[derive(Clone, Copy, Debug, PartialEq, Eq)]
453#[non_exhaustive]
454#[derive(Default)]
455pub enum BlendMode {
456 #[default]
458 Alpha,
459 Add,
461 Multiply,
463 Overlay,
465}
466
467#[derive(Clone, Copy, Debug, PartialEq, Eq)]
468#[non_exhaustive]
469pub enum TextOverflow {
470 Visible,
471 Clip,
472 Ellipsis,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq, Default)]
477pub enum StrokeJoin {
478 #[default]
479 Miter,
481 Round,
483 Bevel,
485}
486
487#[derive(Clone, Copy, Debug, PartialEq, Default)]
489pub enum StrokeCap {
490 #[default]
491 Butt,
493 Round,
496 Square,
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn subcompose_scope_unbounded_has_infinite_max() {
507 let s = SubcomposeScope::UNBOUNDED;
508 assert!(!s.max_width.is_finite());
509 assert!(!s.max_height.is_finite());
510 assert_eq!(s.min_width, 0.0);
511 assert_eq!(s.min_height, 0.0);
512 }
513
514 #[test]
515 fn subcompose_scope_new_round_trips() {
516 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
517 assert_eq!(s.min_width, 10.0);
518 assert_eq!(s.max_width, 200.0);
519 assert_eq!(s.min_height, 20.0);
520 assert_eq!(s.max_height, 300.0);
521 }
522
523 #[test]
524 fn box_with_constraints_scope_bounded_predicates() {
525 let bounded = BoxWithConstraintsScope {
526 min_width: 0.0,
527 max_width: 360.0,
528 min_height: 0.0,
529 max_height: 640.0,
530 };
531 assert!(bounded.has_bounded_width());
532 assert!(bounded.has_bounded_height());
533
534 let unbounded = BoxWithConstraintsScope {
535 min_width: 0.0,
536 max_width: f32::INFINITY,
537 min_height: 0.0,
538 max_height: f32::INFINITY,
539 };
540 assert!(!unbounded.has_bounded_width());
541 assert!(!unbounded.has_bounded_height());
542 }
543
544 #[test]
545 fn view_kind_subcompose_layout_holds_closure() {
546 let v: View = View {
547 id: 0,
548 kind: ViewKind::SubcomposeLayout {
549 content: std::sync::Arc::new(|scope| {
550 let _ = scope.max_width;
551 vec![(0, View::new(0, ViewKind::Box))]
552 }),
553 },
554 modifier: Modifier::default(),
555 children: vec![],
556 scope_key: None,
557 semantics: None,
558 };
559 match &v.kind {
560 ViewKind::SubcomposeLayout { .. } => {}
561 _ => panic!("expected SubcomposeLayout"),
562 }
563 }
564
565 #[test]
566 fn view_kind_subcompose_layout_supports_multiple_slots() {
567 let v: View = View {
568 id: 0,
569 kind: ViewKind::SubcomposeLayout {
570 content: std::sync::Arc::new(|_scope| {
571 vec![
572 (1, View::new(0, ViewKind::Box)),
573 (2, View::new(0, ViewKind::Box)),
574 (3, View::new(0, ViewKind::Box)),
575 ]
576 }),
577 },
578 modifier: Modifier::default(),
579 children: vec![],
580 scope_key: None,
581 semantics: None,
582 };
583 if let ViewKind::SubcomposeLayout { content } = &v.kind {
584 let slots = content(&SubcomposeScope::UNBOUNDED);
585 assert_eq!(slots.len(), 3);
586 assert_eq!(slots[0].0, 1);
587 assert_eq!(slots[1].0, 2);
588 assert_eq!(slots[2].0, 3);
589 } else {
590 panic!("expected SubcomposeLayout");
591 }
592 }
593}