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, 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
83#[derive(Clone)]
84pub struct OverlayEntry {
85 pub id: u64,
86 pub view: Box<View>,
87}
88
89#[derive(Clone)]
90#[non_exhaustive]
91pub enum ViewKind {
92 Box,
93 Row,
94 Column,
95 ZStack,
96 OverlayHost,
97 Text {
98 text: String,
99 color: Color,
100 font_size: Sp,
101 soft_wrap: bool,
102 max_lines: Option<usize>,
103 overflow: TextOverflow,
104 font_family: Option<&'static str>,
105 annotations: Option<Arc<[TextSpan]>>,
106 text_align: TextAlign,
107 font_weight: FontWeight,
108 font_style: FontStyle,
109 text_decoration: TextDecoration,
110 letter_spacing: Sp,
111 line_height: Sp,
112 url: Option<Arc<str>>,
114 font_variation_settings: Option<Arc<str>>,
116 draw_style: DrawStyle,
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}
138
139impl std::fmt::Debug for ViewKind {
140 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
141 match self {
142 Self::Box => f.write_str("Box"),
143 Self::Row => f.write_str("Row"),
144 Self::Column => f.write_str("Column"),
145 Self::ZStack => f.write_str("ZStack"),
146 Self::OverlayHost => f.write_str("OverlayHost"),
147
148 Self::Image { .. } => f.write_str("Image"),
149 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
150 Self::Text { text, .. } => write!(f, "Text({:?})", text),
151 }
152 }
153}
154
155#[derive(Clone, Debug)]
156pub struct View {
157 pub id: ViewId,
158 pub kind: ViewKind,
159 pub modifier: Modifier,
160 pub children: Vec<View>,
161 pub semantics: Option<crate::semantics::Semantics>,
162 pub scope_key: Option<String>,
166}
167
168impl View {
169 pub fn new(id: ViewId, kind: ViewKind) -> Self {
170 View {
171 id,
172 kind,
173 modifier: Modifier::default(),
174 children: vec![],
175 semantics: None,
176 scope_key: None,
177 }
178 }
179 pub fn modifier(mut self, m: Modifier) -> Self {
180 self.modifier = m;
181 self
182 }
183 pub fn disabled(mut self) -> Self {
185 self.modifier.disabled = true;
186 self
187 }
188 pub fn with_children(mut self, kids: Vec<View>) -> Self {
189 self.children = kids;
190 self
191 }
192 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
193 self.children = kids.into();
194 self
195 }
196 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
197 self.semantics = Some(s);
198 self
199 }
200}
201
202#[derive(Clone, Debug, Default)]
204pub struct Scene {
205 pub clear_color: Color,
206 pub nodes: Vec<SceneNode>,
207}
208
209#[derive(Clone, Debug, PartialEq)]
211pub struct TextExtraStyle {
212 pub text_direction: TextDirection,
213 pub font_synthesis: FontSynthesis,
214 pub baseline_shift: BaselineShift,
215 pub draw_style: DrawStyle,
216}
217
218impl Default for TextExtraStyle {
219 fn default() -> Self {
220 Self {
221 text_direction: TextDirection::Ltr,
222 font_synthesis: FontSynthesis::Unspecified,
223 baseline_shift: BaselineShift::Unspecified,
224 draw_style: DrawStyle::Fill,
225 }
226 }
227}
228
229#[derive(Clone, Copy, Debug)]
230pub struct PaintCallbackInfo {
231 pub viewport: Rect,
233 pub clip_rect: Rect,
235 pub pixels_per_point: f32,
237 pub screen_size_px: [u32; 2],
239}
240
241pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
242
243#[derive(Clone, Debug)]
247#[non_exhaustive]
248pub enum SceneNode {
249 Rect {
250 rect: Rect,
251 brush: Brush,
252 radius: [Px; 4],
253 },
254 Border {
255 rect: Rect,
256 brush: Brush,
257 width: Px,
258 radius: [Px; 4],
259 },
260 Text {
261 rect: Rect,
262 text: Arc<str>,
263 color: Color,
264 size: Px,
265 font_family: Option<&'static str>,
266 text_align: TextAlign,
267 font_weight: FontWeight,
268 font_style: FontStyle,
269 text_decoration: TextDecoration,
270 letter_spacing: Px,
271 line_height: Px,
272 extra_style: TextExtraStyle,
274 url: Option<Arc<str>>,
276 font_variation_settings: Option<Arc<str>>,
278 },
279 Ellipse {
280 rect: Rect,
281 brush: Brush,
282 },
283 EllipseBorder {
284 rect: Rect,
285 brush: Brush,
286 width: Px,
287 },
288 PushClip {
289 rect: Rect,
290 radius: [Px; 4],
291 op: ClipOp,
292 },
293 PopClip,
294 PushTransform {
295 transform: Transform,
296 },
297 PopTransform,
298 Image {
299 rect: Rect,
300 handle: ImageHandle,
301 tint: Color,
302 fit: ImageFit,
303 },
304 Coverage {
311 rect: Rect,
312 handle: ImageHandle,
313 color: Color,
314 },
315 Shadow {
318 rect: Rect,
319 radius: [Px; 4],
320 elevation: Px,
321 color: Color,
322 },
323 BeginLayer {
330 rect: Rect,
331 layer_id: u32,
332 alpha: f32,
333 blur_radius_x: Px,
334 blur_radius_y: Px,
335 rectangle_edge: bool,
336 },
337 EndLayer {
339 layer_id: u32,
340 },
341 CompositeShadow {
346 layer_id: u32,
347 blur_px: Px,
348 offset_px: (Px, Px),
349 color: Color,
350 },
351 Arc {
353 rect: Rect,
354 start_angle: f32,
355 sweep_angle: f32,
356 stroke_width: Px,
357 brush: Brush,
358 cap: StrokeCap,
359 },
360 VectorMesh {
368 mesh: Arc<VectorMeshData>,
369 transform: [f32; 6],
370 paint: PaintDesc,
371 clip: Option<u32>,
374 blend: BlendMode,
375 },
376 VectorOverlay {
380 meshes: Arc<[VectorMeshData]>,
381 },
382 PushVectorClip {
391 mesh: Arc<VectorMeshData>,
392 op: ClipOp,
393 },
394 PopVectorClip,
396 Callback {
398 rect: Rect,
399 payload: PaintCallbackPayload,
400 },
401}
402
403#[derive(Clone, Debug, Default)]
405pub struct VectorMeshData {
406 pub vertices: Arc<[VectorVertex]>,
407 pub indices: Arc<[u32]>,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq)]
414#[repr(C)]
415pub struct VectorVertex {
416 pub pos: [f32; 2],
417 pub color: [f32; 4],
418 pub uv: [f32; 2],
419}
420
421#[derive(Clone, Copy, Debug, PartialEq)]
424#[non_exhaustive]
425pub enum PaintDesc {
426 Solid,
428 Linear {
430 start: Vec2,
431 end: Vec2,
432 start_color: Color,
433 end_color: Color,
434 },
435 Radial {
437 center: Vec2,
438 radius: f32,
439 start_color: Color,
440 end_color: Color,
441 },
442 Sweep {
444 center: Vec2,
445 start_color: Color,
446 end_color: Color,
447 },
448}
449
450#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456#[non_exhaustive]
457#[derive(Default)]
458pub enum BlendMode {
459 #[default]
461 Alpha,
462 Screen,
464 Overlay,
466 Darken,
468 Lighten,
470 ColorDodge,
472 ColorBurn,
474 HardLight,
476 SoftLight,
478 Difference,
482 Exclusion,
484 Hue,
486 Saturation,
488 Color,
490 Luminosity,
492 Add,
494 Multiply,
496}
497
498impl BlendMode {
499 pub fn needs_isolation(self) -> bool {
503 !matches!(
504 self,
505 BlendMode::Alpha
506 | BlendMode::Add
507 | BlendMode::Multiply
508 | BlendMode::Screen
509 | BlendMode::Darken
510 | BlendMode::Lighten
511 )
512 }
513
514 pub fn shader_mode(self) -> u32 {
516 match self {
517 BlendMode::Alpha => 0,
518 BlendMode::Add => 1,
519 BlendMode::Multiply => 2,
520 BlendMode::Screen => 3,
521 BlendMode::Overlay => 4,
522 BlendMode::Darken => 5,
523 BlendMode::Lighten => 6,
524 BlendMode::ColorDodge => 7,
525 BlendMode::ColorBurn => 8,
526 BlendMode::HardLight => 9,
527 BlendMode::SoftLight => 10,
528 BlendMode::Difference => 11,
529 BlendMode::Exclusion => 12,
530 BlendMode::Hue => 13,
531 BlendMode::Saturation => 14,
532 BlendMode::Color => 15,
533 BlendMode::Luminosity => 16,
534 }
535 }
536}
537
538#[derive(Clone, Copy, Debug, PartialEq, Eq)]
539#[non_exhaustive]
540pub enum TextOverflow {
541 Visible,
542 Clip,
543 Ellipsis,
544}
545
546#[derive(Clone, Copy, Debug, PartialEq, Default)]
548pub enum StrokeJoin {
549 #[default]
550 Miter,
552 Round,
554 Bevel,
556}
557
558#[derive(Clone, Copy, Debug, PartialEq, Default)]
560pub enum StrokeCap {
561 #[default]
562 Butt,
564 Round,
567 Square,
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn subcompose_scope_unbounded_has_infinite_max() {
578 let s = SubcomposeScope::UNBOUNDED;
579 assert!(!s.max_width.0.is_finite());
580 assert!(!s.max_height.0.is_finite());
581 assert_eq!(s.min_width, Dp(0.0));
582 assert_eq!(s.min_height, Dp(0.0));
583 }
584
585 #[test]
586 fn subcompose_scope_new_round_trips() {
587 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
588 assert_eq!(s.min_width, Dp(10.0));
589 assert_eq!(s.max_width, Dp(200.0));
590 assert_eq!(s.min_height, Dp(20.0));
591 assert_eq!(s.max_height, Dp(300.0));
592 }
593
594 #[test]
595 fn box_with_constraints_scope_bounded_predicates() {
596 let bounded = BoxWithConstraintsScope {
597 min_width: Dp(0.0),
598 max_width: Dp(360.0),
599 min_height: Dp(0.0),
600 max_height: Dp(640.0),
601 };
602 assert!(bounded.has_bounded_width());
603 assert!(bounded.has_bounded_height());
604
605 let unbounded = BoxWithConstraintsScope {
606 min_width: Dp(0.0),
607 max_width: Dp(f32::INFINITY),
608 min_height: Dp(0.0),
609 max_height: Dp(f32::INFINITY),
610 };
611 assert!(!unbounded.has_bounded_width());
612 assert!(!unbounded.has_bounded_height());
613 }
614
615 #[test]
616 fn view_kind_subcompose_layout_holds_closure() {
617 let v: View = View {
618 id: 0,
619 kind: ViewKind::SubcomposeLayout {
620 content: std::sync::Arc::new(|scope| {
621 let _ = scope.max_width;
622 vec![(0, View::new(0, ViewKind::Box))]
623 }),
624 },
625 modifier: Modifier::default(),
626 children: vec![],
627 scope_key: None,
628 semantics: None,
629 };
630 match &v.kind {
631 ViewKind::SubcomposeLayout { .. } => {}
632 _ => panic!("expected SubcomposeLayout"),
633 }
634 }
635
636 #[test]
637 fn view_kind_subcompose_layout_supports_multiple_slots() {
638 let v: View = View {
639 id: 0,
640 kind: ViewKind::SubcomposeLayout {
641 content: std::sync::Arc::new(|_scope| {
642 vec![
643 (1, View::new(0, ViewKind::Box)),
644 (2, View::new(0, ViewKind::Box)),
645 (3, View::new(0, ViewKind::Box)),
646 ]
647 }),
648 },
649 modifier: Modifier::default(),
650 children: vec![],
651 scope_key: None,
652 semantics: None,
653 };
654 if let ViewKind::SubcomposeLayout { content } = &v.kind {
655 let slots = content(&SubcomposeScope::UNBOUNDED);
656 assert_eq!(slots.len(), 3);
657 assert_eq!(slots[0].0, 1);
658 assert_eq!(slots[1].0, 2);
659 assert_eq!(slots[2].0, 3);
660 } else {
661 panic!("expected SubcomposeLayout");
662 }
663 }
664}