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 {
32 fn minimum(value: f32) -> f32 {
33 if value.is_nan() || value < 0.0 || !value.is_finite() {
34 0.0
35 } else {
36 value
37 }
38 }
39 fn maximum(value: f32) -> f32 {
40 if value.is_nan() || value < 0.0 {
41 if value.is_nan() { f32::INFINITY } else { 0.0 }
42 } else {
43 value
44 }
45 }
46 let mut scope = Self {
47 min_width: Dp(minimum(min_width.0)),
48 max_width: Dp(maximum(max_width.0)),
49 min_height: Dp(minimum(min_height.0)),
50 max_height: Dp(maximum(max_height.0)),
51 };
52 if scope.min_width.0 > scope.max_width.0 {
53 scope.min_width = scope.max_width;
54 }
55 if scope.min_height.0 > scope.max_height.0 {
56 scope.min_height = scope.max_height;
57 }
58 scope
59 }
60
61 pub fn has_bounded_width(&self) -> bool {
62 self.max_width.0.is_finite()
63 }
64
65 pub fn has_bounded_height(&self) -> bool {
66 self.max_height.0.is_finite()
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq)]
73pub struct BoxWithConstraintsScope {
74 pub min_width: Dp,
75 pub max_width: Dp,
76 pub min_height: Dp,
77 pub max_height: Dp,
78}
79
80impl BoxWithConstraintsScope {
81 pub fn has_bounded_width(&self) -> bool {
83 self.max_width.0.is_finite()
84 }
85
86 pub fn has_bounded_height(&self) -> bool {
88 self.max_height.0.is_finite()
89 }
90}
91
92pub type ViewId = u64;
93
94pub type ImageHandle = u64;
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
96pub enum ImageFilter {
97 #[default]
98 Linear,
99 Nearest,
100}
101
102#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
103pub struct ImageSourceRect {
104 pub x: u32,
105 pub y: u32,
106 pub width: u32,
107 pub height: u32,
108}
109
110impl ImageSourceRect {
111 pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
112 Self {
113 x,
114 y,
115 width,
116 height,
117 }
118 }
119
120 pub const fn is_empty(self) -> bool {
121 self.width == 0 || self.height == 0
122 }
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
126#[non_exhaustive]
127pub enum ImageFit {
128 #[default]
130 Contain,
131 Cover,
133 FitWidth,
135 FitHeight,
137 FillBounds,
139 Inside,
141 None,
143 Tile,
147}
148
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
154pub enum ImageAlignment {
155 #[default]
157 Center,
158 Begin,
160 End,
162}
163
164impl ImageFit {
165 pub fn scale_factor(self, src: (f32, f32), dst: (f32, f32)) -> (f32, f32) {
169 let fill_w = dst.0 / src.0;
170 let fill_h = dst.1 / src.1;
171 let uniform = |v: f32| (v, v);
172 match self {
173 Self::Contain => uniform(fill_w.min(fill_h)),
174 Self::Cover => uniform(fill_w.max(fill_h)),
175 Self::FitWidth => uniform(fill_w),
176 Self::FitHeight => uniform(fill_h),
177 Self::FillBounds => (fill_w, fill_h),
178 Self::Inside => {
179 if src.0 <= dst.0 && src.1 <= dst.1 {
180 uniform(1.0)
181 } else {
182 uniform(fill_w.min(fill_h))
183 }
184 }
185 Self::None | Self::Tile => uniform(1.0),
186 }
187 }
188}
189
190#[derive(Clone)]
191pub struct OverlayEntry {
192 pub id: u64,
193 pub view: Box<View>,
194}
195
196#[derive(Clone)]
197#[non_exhaustive]
198pub enum ViewKind {
199 Box,
200 Row,
201 Column,
202 ZStack,
203 OverlayHost,
204 Text {
205 text: String,
206 color: Color,
207 font_size: Sp,
208 soft_wrap: bool,
209 max_lines: Option<usize>,
210 overflow: TextOverflow,
211 font_family: Option<&'static str>,
212 annotations: Option<Arc<[TextSpan]>>,
213 text_align: TextAlign,
214 font_weight: FontWeight,
215 font_style: FontStyle,
216 text_decoration: TextDecoration,
217 letter_spacing: Sp,
218 line_height: Sp,
219 url: Option<Arc<str>>,
221 font_variation_settings: Option<Arc<str>>,
223 draw_style: DrawStyle,
225 },
226
227 Image {
228 handle: ImageHandle,
229 tint: Color,
230 fit: ImageFit,
231 filter: ImageFilter,
232 source_rect: Option<ImageSourceRect>,
233 alignment: ImageAlignment,
234 },
235 SubcomposeLayout {
245 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
246 },
247}
248
249impl std::fmt::Debug for ViewKind {
250 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
251 match self {
252 Self::Box => f.write_str("Box"),
253 Self::Row => f.write_str("Row"),
254 Self::Column => f.write_str("Column"),
255 Self::ZStack => f.write_str("ZStack"),
256 Self::OverlayHost => f.write_str("OverlayHost"),
257
258 Self::Image {
259 handle,
260 tint,
261 fit,
262 filter,
263 source_rect,
264 alignment,
265 } => f
266 .debug_struct("Image")
267 .field("handle", handle)
268 .field("tint", tint)
269 .field("fit", fit)
270 .field("filter", filter)
271 .field("source_rect", source_rect)
272 .field("alignment", alignment)
273 .finish(),
274 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
275 Self::Text { text, .. } => write!(f, "Text({:?})", text),
276 }
277 }
278}
279
280#[derive(Clone, Debug)]
281pub struct View {
282 pub id: ViewId,
283 pub kind: ViewKind,
284 pub modifier: Modifier,
285 pub children: Vec<View>,
286 pub semantics: Option<crate::semantics::Semantics>,
287 pub scope_key: Option<String>,
291}
292
293impl View {
294 pub fn new(id: ViewId, kind: ViewKind) -> Self {
295 View {
296 id,
297 kind,
298 modifier: Modifier::default(),
299 children: vec![],
300 semantics: None,
301 scope_key: None,
302 }
303 }
304 pub fn modifier(mut self, m: Modifier) -> Self {
305 self.modifier = m;
306 self
307 }
308 pub fn disabled(mut self) -> Self {
310 self.modifier.disabled = true;
311 self
312 }
313 pub fn with_children(mut self, kids: Vec<View>) -> Self {
314 self.children = kids;
315 self
316 }
317 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
318 self.children = kids.into();
319 self
320 }
321 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
322 self.semantics = Some(s);
323 self
324 }
325}
326
327#[derive(Clone, Debug, Default)]
329pub struct Scene {
330 pub clear_color: Color,
331 pub nodes: Vec<SceneNode>,
332}
333
334#[derive(Clone, Debug, PartialEq, Default)]
339pub struct TextPaintStyle {
340 pub font_family: Option<&'static str>,
341 pub text_align: TextAlign,
342 pub font_weight: FontWeight,
343 pub font_style: FontStyle,
344 pub text_decoration: TextDecoration,
345 pub letter_spacing: Px,
346 pub line_height: Px,
347 pub text_direction: TextDirection,
348 pub font_synthesis: FontSynthesis,
350 pub baseline_shift: BaselineShift,
351 pub draw_style: DrawStyle,
353 pub url: Option<Arc<str>>,
355 pub font_variation_settings: Option<Arc<str>>,
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Default)]
363pub struct ImagePaintStyle {
364 pub fit: ImageFit,
365 pub filter: ImageFilter,
366 pub source_rect: Option<ImageSourceRect>,
368 pub alignment: ImageAlignment,
369}
370
371#[derive(Clone, Copy, Debug)]
372pub struct PaintCallbackInfo {
373 pub viewport: Rect,
375 pub clip_rect: Rect,
377 pub pixels_per_point: f32,
379 pub screen_size_px: [u32; 2],
381}
382
383pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
384
385#[derive(Clone, Debug)]
389#[non_exhaustive]
390pub enum SceneNode {
391 Rect {
392 rect: Rect,
393 brush: Brush,
394 radius: [Px; 4],
395 },
396 Border {
397 rect: Rect,
398 brush: Brush,
399 width: Px,
400 radius: [Px; 4],
401 },
402 Text {
403 rect: Rect,
404 text: Arc<str>,
405 color: Color,
406 size: Px,
407 style: TextPaintStyle,
408 },
409 Ellipse {
410 rect: Rect,
411 brush: Brush,
412 },
413 EllipseBorder {
414 rect: Rect,
415 brush: Brush,
416 width: Px,
417 },
418 PushClip {
419 rect: Rect,
420 radius: [Px; 4],
421 op: ClipOp,
422 },
423 PopClip,
424 PushTransform {
425 transform: Transform,
426 },
427 PopTransform,
428 Image {
429 rect: Rect,
430 handle: ImageHandle,
431 tint: Color,
432 style: ImagePaintStyle,
433 },
434 Coverage {
441 rect: Rect,
442 handle: ImageHandle,
443 color: Color,
444 },
445 Shadow {
448 rect: Rect,
449 radius: [Px; 4],
450 elevation: Px,
451 color: Color,
452 },
453 BeginLayer {
460 rect: Rect,
461 layer_id: u32,
462 alpha: f32,
463 blur_radius_x: Px,
464 blur_radius_y: Px,
465 rectangle_edge: bool,
466 },
467 EndLayer {
469 layer_id: u32,
470 },
471 CompositeShadow {
476 layer_id: u32,
477 blur_px: Px,
478 offset_px: (Px, Px),
479 color: Color,
480 },
481 Arc {
483 rect: Rect,
484 start_angle: f32,
485 sweep_angle: f32,
486 stroke_width: Px,
487 brush: Brush,
488 cap: StrokeCap,
489 },
490 VectorMesh {
498 mesh: Arc<VectorMeshData>,
499 transform: [f32; 6],
500 paint: PaintDesc,
501 clip: Option<u32>,
504 blend: BlendMode,
505 },
506 VectorOverlay {
510 meshes: Arc<[VectorMeshData]>,
511 },
512 PushVectorClip {
521 mesh: Arc<VectorMeshData>,
522 op: ClipOp,
523 },
524 PopVectorClip,
526 Callback {
528 rect: Rect,
529 payload: PaintCallbackPayload,
530 },
531}
532
533#[derive(Clone, Debug, Default)]
535pub struct VectorMeshData {
536 pub vertices: Arc<[VectorVertex]>,
537 pub indices: Arc<[u32]>,
538}
539
540#[derive(Clone, Copy, Debug, PartialEq)]
544#[repr(C)]
545pub struct VectorVertex {
546 pub pos: [f32; 2],
547 pub color: [f32; 4],
548 pub uv: [f32; 2],
549}
550
551#[derive(Clone, Copy, Debug, PartialEq)]
554#[non_exhaustive]
555pub enum PaintDesc {
556 Solid,
558 Linear {
560 start: Vec2,
561 end: Vec2,
562 start_color: Color,
563 end_color: Color,
564 },
565 Radial {
567 center: Vec2,
568 radius: f32,
569 start_color: Color,
570 end_color: Color,
571 },
572 Sweep {
574 center: Vec2,
575 start_color: Color,
576 end_color: Color,
577 },
578}
579
580#[derive(Clone, Copy, Debug, PartialEq, Eq)]
586#[non_exhaustive]
587#[derive(Default)]
588pub enum BlendMode {
589 #[default]
591 Alpha,
592 Screen,
594 Overlay,
596 Darken,
598 Lighten,
600 ColorDodge,
602 ColorBurn,
604 HardLight,
606 SoftLight,
608 Difference,
612 Exclusion,
614 Hue,
616 Saturation,
618 Color,
620 Luminosity,
622 Add,
624 Multiply,
626}
627
628impl BlendMode {
629 pub fn needs_isolation(self) -> bool {
633 !matches!(
634 self,
635 BlendMode::Alpha
636 | BlendMode::Add
637 | BlendMode::Multiply
638 | BlendMode::Screen
639 | BlendMode::Darken
640 | BlendMode::Lighten
641 )
642 }
643
644 pub fn shader_mode(self) -> u32 {
646 match self {
647 BlendMode::Alpha => 0,
648 BlendMode::Add => 1,
649 BlendMode::Multiply => 2,
650 BlendMode::Screen => 3,
651 BlendMode::Overlay => 4,
652 BlendMode::Darken => 5,
653 BlendMode::Lighten => 6,
654 BlendMode::ColorDodge => 7,
655 BlendMode::ColorBurn => 8,
656 BlendMode::HardLight => 9,
657 BlendMode::SoftLight => 10,
658 BlendMode::Difference => 11,
659 BlendMode::Exclusion => 12,
660 BlendMode::Hue => 13,
661 BlendMode::Saturation => 14,
662 BlendMode::Color => 15,
663 BlendMode::Luminosity => 16,
664 }
665 }
666}
667
668#[derive(Clone, Copy, Debug, PartialEq, Eq)]
669#[non_exhaustive]
670pub enum TextOverflow {
671 Visible,
672 Clip,
673 Ellipsis,
674}
675
676#[derive(Clone, Copy, Debug, PartialEq, Default)]
678pub enum StrokeJoin {
679 #[default]
680 Miter,
682 Round,
684 Bevel,
686}
687
688#[derive(Clone, Copy, Debug, PartialEq, Default)]
690pub enum StrokeCap {
691 #[default]
692 Butt,
694 Round,
697 Square,
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
707 fn subcompose_scope_unbounded_has_infinite_max() {
708 let s = SubcomposeScope::UNBOUNDED;
709 assert!(!s.max_width.0.is_finite());
710 assert!(!s.max_height.0.is_finite());
711 assert_eq!(s.min_width, Dp(0.0));
712 assert_eq!(s.min_height, Dp(0.0));
713 }
714
715 #[test]
716 fn subcompose_scope_new_round_trips() {
717 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
718 assert_eq!(s.min_width, Dp(10.0));
719 assert_eq!(s.max_width, Dp(200.0));
720 assert_eq!(s.min_height, Dp(20.0));
721 assert_eq!(s.max_height, Dp(300.0));
722 }
723
724 #[test]
725 fn box_with_constraints_scope_bounded_predicates() {
726 let bounded = BoxWithConstraintsScope {
727 min_width: Dp(0.0),
728 max_width: Dp(360.0),
729 min_height: Dp(0.0),
730 max_height: Dp(640.0),
731 };
732 assert!(bounded.has_bounded_width());
733 assert!(bounded.has_bounded_height());
734
735 let unbounded = BoxWithConstraintsScope {
736 min_width: Dp(0.0),
737 max_width: Dp(f32::INFINITY),
738 min_height: Dp(0.0),
739 max_height: Dp(f32::INFINITY),
740 };
741 assert!(!unbounded.has_bounded_width());
742 assert!(!unbounded.has_bounded_height());
743 }
744
745 #[test]
746 fn view_kind_subcompose_layout_holds_closure() {
747 let v: View = View {
748 id: 0,
749 kind: ViewKind::SubcomposeLayout {
750 content: std::sync::Arc::new(|scope| {
751 let _ = scope.max_width;
752 vec![(0, View::new(0, ViewKind::Box))]
753 }),
754 },
755 modifier: Modifier::default(),
756 children: vec![],
757 scope_key: None,
758 semantics: None,
759 };
760 match &v.kind {
761 ViewKind::SubcomposeLayout { .. } => {}
762 _ => panic!("expected SubcomposeLayout"),
763 }
764 }
765
766 #[test]
767 fn view_kind_subcompose_layout_supports_multiple_slots() {
768 let v: View = View {
769 id: 0,
770 kind: ViewKind::SubcomposeLayout {
771 content: std::sync::Arc::new(|_scope| {
772 vec![
773 (1, View::new(0, ViewKind::Box)),
774 (2, View::new(0, ViewKind::Box)),
775 (3, View::new(0, ViewKind::Box)),
776 ]
777 }),
778 },
779 modifier: Modifier::default(),
780 children: vec![],
781 scope_key: None,
782 semantics: None,
783 };
784 if let ViewKind::SubcomposeLayout { content } = &v.kind {
785 let slots = content(&SubcomposeScope::UNBOUNDED);
786 assert_eq!(slots.len(), 3);
787 assert_eq!(slots[0].0, 1);
788 assert_eq!(slots[1].0, 2);
789 assert_eq!(slots[2].0, 3);
790 } else {
791 panic!("expected SubcomposeLayout");
792 }
793 }
794}