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 {
338 rect: Rect,
339 layer_id: u32,
340 alpha: f32,
341 blur_radius_x: f32,
342 blur_radius_y: f32,
343 rectangle_edge: bool,
344 },
345 EndLayer {
347 layer_id: u32,
348 },
349 CompositeShadow {
354 layer_id: u32,
355 blur_px: f32,
356 offset_px: (f32, f32),
357 color: Color,
358 },
359 Arc {
361 rect: Rect,
362 start_angle: f32,
363 sweep_angle: f32,
364 stroke_width: f32,
365 color: Color,
366 cap: StrokeCap,
367 },
368 VectorMesh {
374 mesh: Arc<VectorMeshData>,
375 transform: [f32; 6],
376 paint: PaintDesc,
377 clip: Option<u32>,
380 blend: BlendMode,
381 },
382 VectorOverlay {
386 meshes: Arc<[VectorMeshData]>,
387 },
388 PushVectorClip {
392 mesh: Arc<VectorMeshData>,
393 },
394 PopVectorClip,
396}
397
398#[derive(Clone, Debug, Default)]
400pub struct VectorMeshData {
401 pub vertices: Arc<[VectorVertex]>,
402 pub indices: Arc<[u32]>,
403}
404
405#[derive(Clone, Copy, Debug, PartialEq)]
409#[repr(C)]
410pub struct VectorVertex {
411 pub pos: [f32; 2],
412 pub color: [f32; 4],
413 pub uv: [f32; 2],
414}
415
416#[derive(Clone, Copy, Debug, PartialEq)]
418#[non_exhaustive]
419pub enum PaintDesc {
420 Solid,
422 Linear {
424 start: Vec2,
425 end: Vec2,
426 start_color: Color,
427 end_color: Color,
428 },
429}
430
431#[derive(Clone, Copy, Debug, PartialEq, Eq)]
434#[non_exhaustive]
435#[derive(Default)]
436pub enum BlendMode {
437 #[default]
439 Alpha,
440 Add,
442 Multiply,
444 Overlay,
446}
447
448#[derive(Clone, Copy, Debug, PartialEq, Eq)]
449#[non_exhaustive]
450pub enum TextOverflow {
451 Visible,
452 Clip,
453 Ellipsis,
454}
455
456#[derive(Clone, Copy, Debug, PartialEq, Default)]
458pub enum StrokeJoin {
459 #[default]
460 Miter,
462 Round,
464 Bevel,
466}
467
468#[derive(Clone, Copy, Debug, PartialEq, Default)]
470pub enum StrokeCap {
471 #[default]
472 Butt,
474 Round,
477 Square,
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn subcompose_scope_unbounded_has_infinite_max() {
488 let s = SubcomposeScope::UNBOUNDED;
489 assert!(!s.max_width.is_finite());
490 assert!(!s.max_height.is_finite());
491 assert_eq!(s.min_width, 0.0);
492 assert_eq!(s.min_height, 0.0);
493 }
494
495 #[test]
496 fn subcompose_scope_new_round_trips() {
497 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
498 assert_eq!(s.min_width, 10.0);
499 assert_eq!(s.max_width, 200.0);
500 assert_eq!(s.min_height, 20.0);
501 assert_eq!(s.max_height, 300.0);
502 }
503
504 #[test]
505 fn box_with_constraints_scope_bounded_predicates() {
506 let bounded = BoxWithConstraintsScope {
507 min_width: 0.0,
508 max_width: 360.0,
509 min_height: 0.0,
510 max_height: 640.0,
511 };
512 assert!(bounded.has_bounded_width());
513 assert!(bounded.has_bounded_height());
514
515 let unbounded = BoxWithConstraintsScope {
516 min_width: 0.0,
517 max_width: f32::INFINITY,
518 min_height: 0.0,
519 max_height: f32::INFINITY,
520 };
521 assert!(!unbounded.has_bounded_width());
522 assert!(!unbounded.has_bounded_height());
523 }
524
525 #[test]
526 fn view_kind_subcompose_layout_holds_closure() {
527 let v: View = View {
528 id: 0,
529 kind: ViewKind::SubcomposeLayout {
530 content: std::sync::Arc::new(|scope| {
531 let _ = scope.max_width;
532 vec![(0, View::new(0, ViewKind::Box))]
533 }),
534 },
535 modifier: Modifier::default(),
536 children: vec![],
537 scope_key: None,
538 semantics: None,
539 };
540 match &v.kind {
541 ViewKind::SubcomposeLayout { .. } => {}
542 _ => panic!("expected SubcomposeLayout"),
543 }
544 }
545
546 #[test]
547 fn view_kind_subcompose_layout_supports_multiple_slots() {
548 let v: View = View {
549 id: 0,
550 kind: ViewKind::SubcomposeLayout {
551 content: std::sync::Arc::new(|_scope| {
552 vec![
553 (1, View::new(0, ViewKind::Box)),
554 (2, View::new(0, ViewKind::Box)),
555 (3, View::new(0, ViewKind::Box)),
556 ]
557 }),
558 },
559 modifier: Modifier::default(),
560 children: vec![],
561 scope_key: None,
562 semantics: None,
563 };
564 if let ViewKind::SubcomposeLayout { content } = &v.kind {
565 let slots = content(&SubcomposeScope::UNBOUNDED);
566 assert_eq!(slots.len(), 3);
567 assert_eq!(slots[0].0, 1);
568 assert_eq!(slots[1].0, 2);
569 assert_eq!(slots[2].0, 3);
570 } else {
571 panic!("expected SubcomposeLayout");
572 }
573 }
574}