1use crate::units::{Dp, Px, Sp};
2use crate::{
3 BaselineShift, Brush, ClipOp, Color, DrawStyle, FontStyle, FontSynthesis, FontWeight, Modifier,
4 Rect, TextAlign, TextDecoration, TextDirection, TextSpan, Transform, Vec2,
5};
6use std::{fmt::Formatter, rc::Rc, sync::Arc};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct SubcomposeScope {
12 pub min_width: Dp,
13 pub max_width: Dp,
14 pub min_height: Dp,
15 pub max_height: Dp,
16}
17
18impl SubcomposeScope {
19 pub const UNBOUNDED: Self = Self {
22 min_width: Dp(0.0),
23 max_width: Dp(f32::INFINITY),
24 min_height: Dp(0.0),
25 max_height: Dp(f32::INFINITY),
26 };
27
28 pub fn new(min_width: Dp, max_width: Dp, min_height: Dp, max_height: Dp) -> Self {
30 Self {
31 min_width,
32 max_width,
33 min_height,
34 max_height,
35 }
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct BoxWithConstraintsScope {
43 pub min_width: Dp,
44 pub max_width: Dp,
45 pub min_height: Dp,
46 pub max_height: Dp,
47}
48
49impl BoxWithConstraintsScope {
50 pub fn has_bounded_width(&self) -> bool {
52 self.max_width.0.is_finite()
53 }
54
55 pub fn has_bounded_height(&self) -> bool {
57 self.max_height.0.is_finite()
58 }
59}
60
61pub type ViewId = u64;
62
63pub type ImageHandle = u64;
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum ImageFit {
67 Contain,
69 Cover,
71 FitWidth,
73 FitHeight,
75 FillBounds,
77 Inside,
79 None,
81}
82
83pub type Callback = Rc<dyn Fn()>;
84
85#[derive(Clone)]
86pub struct OverlayEntry {
87 pub id: u64,
88 pub view: Box<View>,
89}
90
91#[derive(Clone)]
92#[non_exhaustive]
93pub enum ViewKind {
94 Box,
95 Row,
96 Column,
97 ZStack,
98 OverlayHost,
99 Text {
100 text: String,
101 color: Color,
102 font_size: Sp,
103 soft_wrap: bool,
104 max_lines: Option<usize>,
105 overflow: TextOverflow,
106 font_family: Option<&'static str>,
107 annotations: Option<Arc<[TextSpan]>>,
108 text_align: TextAlign,
109 font_weight: FontWeight,
110 font_style: FontStyle,
111 text_decoration: TextDecoration,
112 letter_spacing: Sp,
113 line_height: Sp,
114 url: Option<Arc<str>>,
116 font_variation_settings: Option<Arc<str>>,
118 },
119
120 Image {
121 handle: ImageHandle,
122 tint: Color, fit: ImageFit,
124 },
125 SubcomposeLayout {
135 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
136 },
137 Expander {
140 expanded: bool,
141 on_toggle: Option<Callback>,
142 },
143 TreeRow {
146 depth: usize,
147 has_children: bool,
148 is_expanded: bool,
149 is_selected: bool,
150 on_toggle: Option<Callback>,
151 on_select: Option<Callback>,
152 },
153}
154
155impl std::fmt::Debug for ViewKind {
156 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::Box => f.write_str("Box"),
159 Self::Row => f.write_str("Row"),
160 Self::Column => f.write_str("Column"),
161 Self::ZStack => f.write_str("ZStack"),
162 Self::OverlayHost => f.write_str("OverlayHost"),
163
164 Self::Image { .. } => f.write_str("Image"),
165 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
166 Self::Text { text, .. } => write!(f, "Text({:?})", text),
167
168 Self::Expander { expanded, .. } => {
169 if *expanded {
170 write!(f, "Expander(expanded)")
171 } else {
172 write!(f, "Expander(collapsed)")
173 }
174 }
175 Self::TreeRow {
176 depth,
177 has_children,
178 is_expanded,
179 is_selected,
180 ..
181 } => {
182 write!(
183 f,
184 "TreeRow(depth={}, children={}, expanded={}, selected={})",
185 depth, has_children, is_expanded, is_selected
186 )
187 }
188 }
189 }
190}
191
192#[derive(Clone, Debug)]
193pub struct View {
194 pub id: ViewId,
195 pub kind: ViewKind,
196 pub modifier: Modifier,
197 pub children: Vec<View>,
198 pub semantics: Option<crate::semantics::Semantics>,
199 pub scope_key: Option<String>,
203}
204
205impl View {
206 pub fn new(id: ViewId, kind: ViewKind) -> Self {
207 View {
208 id,
209 kind,
210 modifier: Modifier::default(),
211 children: vec![],
212 semantics: None,
213 scope_key: None,
214 }
215 }
216 pub fn modifier(mut self, m: Modifier) -> Self {
217 self.modifier = m;
218 self
219 }
220 pub fn disabled(mut self) -> Self {
222 self.modifier.disabled = true;
223 self
224 }
225 pub fn with_children(mut self, kids: Vec<View>) -> Self {
226 self.children = kids;
227 self
228 }
229 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
230 self.children = kids.into();
231 self
232 }
233 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
234 self.semantics = Some(s);
235 self
236 }
237}
238
239#[derive(Clone, Debug, Default)]
241pub struct Scene {
242 pub clear_color: Color,
243 pub nodes: Vec<SceneNode>,
244}
245
246#[derive(Clone, Debug, PartialEq)]
248pub struct TextExtraStyle {
249 pub text_direction: TextDirection,
250 pub font_synthesis: FontSynthesis,
251 pub baseline_shift: BaselineShift,
252 pub draw_style: DrawStyle,
253}
254
255impl Default for TextExtraStyle {
256 fn default() -> Self {
257 Self {
258 text_direction: TextDirection::Ltr,
259 font_synthesis: FontSynthesis::Unspecified,
260 baseline_shift: BaselineShift::Unspecified,
261 draw_style: DrawStyle::Fill,
262 }
263 }
264}
265
266#[derive(Clone, Copy, Debug)]
267pub struct PaintCallbackInfo {
268 pub viewport: Rect,
270 pub clip_rect: Rect,
272 pub pixels_per_point: f32,
274 pub screen_size_px: [u32; 2],
276}
277
278pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
279
280#[derive(Clone, Debug)]
284#[non_exhaustive]
285pub enum SceneNode {
286 Rect {
287 rect: Rect,
288 brush: Brush,
289 radius: [Px; 4],
290 },
291 Border {
292 rect: Rect,
293 color: Color,
294 width: Px,
295 radius: [Px; 4],
296 },
297 Text {
298 rect: Rect,
299 text: Arc<str>,
300 color: Color,
301 size: Px,
302 font_family: Option<&'static str>,
303 text_align: TextAlign,
304 font_weight: FontWeight,
305 font_style: FontStyle,
306 text_decoration: TextDecoration,
307 letter_spacing: Px,
308 line_height: Px,
309 extra_style: TextExtraStyle,
311 url: Option<Arc<str>>,
313 font_variation_settings: Option<Arc<str>>,
315 },
316 Ellipse {
317 rect: Rect,
318 brush: Brush,
319 },
320 EllipseBorder {
321 rect: Rect,
322 color: Color,
323 width: Px,
324 },
325 PushClip {
326 rect: Rect,
327 radius: [Px; 4],
328 op: ClipOp,
329 },
330 PopClip,
331 PushTransform {
332 transform: Transform,
333 },
334 PopTransform,
335 Image {
336 rect: Rect,
337 handle: ImageHandle,
338 tint: Color,
339 fit: ImageFit,
340 },
341 Coverage {
348 rect: Rect,
349 handle: ImageHandle,
350 color: Color,
351 },
352 Shadow {
355 rect: Rect,
356 radius: [Px; 4],
357 elevation: Px,
358 color: Color,
359 },
360 BeginLayer {
367 rect: Rect,
368 layer_id: u32,
369 alpha: f32,
370 blur_radius_x: Px,
371 blur_radius_y: Px,
372 rectangle_edge: bool,
373 },
374 EndLayer {
376 layer_id: u32,
377 },
378 CompositeShadow {
383 layer_id: u32,
384 blur_px: Px,
385 offset_px: (Px, Px),
386 color: Color,
387 },
388 Arc {
390 rect: Rect,
391 start_angle: f32,
392 sweep_angle: f32,
393 stroke_width: Px,
394 color: Color,
395 cap: StrokeCap,
396 },
397 VectorMesh {
405 mesh: Arc<VectorMeshData>,
406 transform: [f32; 6],
407 paint: PaintDesc,
408 clip: Option<u32>,
411 blend: BlendMode,
412 },
413 VectorOverlay {
417 meshes: Arc<[VectorMeshData]>,
418 },
419 PushVectorClip {
428 mesh: Arc<VectorMeshData>,
429 op: ClipOp,
430 },
431 PopVectorClip,
433 Callback {
435 rect: Rect,
436 payload: PaintCallbackPayload,
437 },
438}
439
440#[derive(Clone, Debug, Default)]
442pub struct VectorMeshData {
443 pub vertices: Arc<[VectorVertex]>,
444 pub indices: Arc<[u32]>,
445}
446
447#[derive(Clone, Copy, Debug, PartialEq)]
451#[repr(C)]
452pub struct VectorVertex {
453 pub pos: [f32; 2],
454 pub color: [f32; 4],
455 pub uv: [f32; 2],
456}
457
458#[derive(Clone, Copy, Debug, PartialEq)]
460#[non_exhaustive]
461pub enum PaintDesc {
462 Solid,
464 Linear {
466 start: Vec2,
467 end: Vec2,
468 start_color: Color,
469 end_color: Color,
470 },
471}
472
473#[derive(Clone, Copy, Debug, PartialEq, Eq)]
476#[non_exhaustive]
477#[derive(Default)]
478pub enum BlendMode {
479 #[default]
481 Alpha,
482 Add,
484 Multiply,
486 Overlay,
488}
489
490#[derive(Clone, Copy, Debug, PartialEq, Eq)]
491#[non_exhaustive]
492pub enum TextOverflow {
493 Visible,
494 Clip,
495 Ellipsis,
496}
497
498#[derive(Clone, Copy, Debug, PartialEq, Default)]
500pub enum StrokeJoin {
501 #[default]
502 Miter,
504 Round,
506 Bevel,
508}
509
510#[derive(Clone, Copy, Debug, PartialEq, Default)]
512pub enum StrokeCap {
513 #[default]
514 Butt,
516 Round,
519 Square,
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn subcompose_scope_unbounded_has_infinite_max() {
530 let s = SubcomposeScope::UNBOUNDED;
531 assert!(!s.max_width.0.is_finite());
532 assert!(!s.max_height.0.is_finite());
533 assert_eq!(s.min_width, Dp(0.0));
534 assert_eq!(s.min_height, Dp(0.0));
535 }
536
537 #[test]
538 fn subcompose_scope_new_round_trips() {
539 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
540 assert_eq!(s.min_width, Dp(10.0));
541 assert_eq!(s.max_width, Dp(200.0));
542 assert_eq!(s.min_height, Dp(20.0));
543 assert_eq!(s.max_height, Dp(300.0));
544 }
545
546 #[test]
547 fn box_with_constraints_scope_bounded_predicates() {
548 let bounded = BoxWithConstraintsScope {
549 min_width: Dp(0.0),
550 max_width: Dp(360.0),
551 min_height: Dp(0.0),
552 max_height: Dp(640.0),
553 };
554 assert!(bounded.has_bounded_width());
555 assert!(bounded.has_bounded_height());
556
557 let unbounded = BoxWithConstraintsScope {
558 min_width: Dp(0.0),
559 max_width: Dp(f32::INFINITY),
560 min_height: Dp(0.0),
561 max_height: Dp(f32::INFINITY),
562 };
563 assert!(!unbounded.has_bounded_width());
564 assert!(!unbounded.has_bounded_height());
565 }
566
567 #[test]
568 fn view_kind_subcompose_layout_holds_closure() {
569 let v: View = View {
570 id: 0,
571 kind: ViewKind::SubcomposeLayout {
572 content: std::sync::Arc::new(|scope| {
573 let _ = scope.max_width;
574 vec![(0, View::new(0, ViewKind::Box))]
575 }),
576 },
577 modifier: Modifier::default(),
578 children: vec![],
579 scope_key: None,
580 semantics: None,
581 };
582 match &v.kind {
583 ViewKind::SubcomposeLayout { .. } => {}
584 _ => panic!("expected SubcomposeLayout"),
585 }
586 }
587
588 #[test]
589 fn view_kind_subcompose_layout_supports_multiple_slots() {
590 let v: View = View {
591 id: 0,
592 kind: ViewKind::SubcomposeLayout {
593 content: std::sync::Arc::new(|_scope| {
594 vec![
595 (1, View::new(0, ViewKind::Box)),
596 (2, View::new(0, ViewKind::Box)),
597 (3, View::new(0, ViewKind::Box)),
598 ]
599 }),
600 },
601 modifier: Modifier::default(),
602 children: vec![],
603 scope_key: None,
604 semantics: None,
605 };
606 if let ViewKind::SubcomposeLayout { content } = &v.kind {
607 let slots = content(&SubcomposeScope::UNBOUNDED);
608 assert_eq!(slots.len(), 3);
609 assert_eq!(slots[0].0, 1);
610 assert_eq!(slots[1].0, 2);
611 assert_eq!(slots[2].0, 3);
612 } else {
613 panic!("expected SubcomposeLayout");
614 }
615 }
616}