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 shape_px: [Px; 4],
483 },
484 Arc {
486 rect: Rect,
487 start_angle: f32,
488 sweep_angle: f32,
489 stroke_width: Px,
490 brush: Brush,
491 cap: StrokeCap,
492 },
493 VectorMesh {
501 mesh: Arc<VectorMeshData>,
502 transform: [f32; 6],
503 paint: PaintDesc,
504 clip: Option<u32>,
507 blend: BlendMode,
508 },
509 VectorOverlay {
513 meshes: Arc<[VectorMeshData]>,
514 },
515 PushVectorClip {
524 mesh: Arc<VectorMeshData>,
525 op: ClipOp,
526 },
527 PopVectorClip,
529 Callback {
531 rect: Rect,
532 payload: PaintCallbackPayload,
533 },
534}
535
536#[derive(Clone, Debug, Default)]
538pub struct VectorMeshData {
539 pub vertices: Arc<[VectorVertex]>,
540 pub indices: Arc<[u32]>,
541}
542
543#[derive(Clone, Copy, Debug, PartialEq)]
547#[repr(C)]
548pub struct VectorVertex {
549 pub pos: [f32; 2],
550 pub color: [f32; 4],
551 pub uv: [f32; 2],
552}
553
554#[derive(Clone, Copy, Debug, PartialEq)]
557#[non_exhaustive]
558pub enum PaintDesc {
559 Solid,
561 Linear {
563 start: Vec2,
564 end: Vec2,
565 start_color: Color,
566 end_color: Color,
567 },
568 Radial {
570 center: Vec2,
571 radius: f32,
572 start_color: Color,
573 end_color: Color,
574 },
575 Sweep {
577 center: Vec2,
578 start_color: Color,
579 end_color: Color,
580 },
581}
582
583#[derive(Clone, Copy, Debug, PartialEq, Eq)]
589#[non_exhaustive]
590#[derive(Default)]
591pub enum BlendMode {
592 #[default]
594 Alpha,
595 Screen,
597 Overlay,
599 Darken,
601 Lighten,
603 ColorDodge,
605 ColorBurn,
607 HardLight,
609 SoftLight,
611 Difference,
615 Exclusion,
617 Hue,
619 Saturation,
621 Color,
623 Luminosity,
625 Add,
627 Multiply,
629}
630
631impl BlendMode {
632 pub fn needs_isolation(self) -> bool {
636 !matches!(
637 self,
638 BlendMode::Alpha
639 | BlendMode::Add
640 | BlendMode::Multiply
641 | BlendMode::Screen
642 | BlendMode::Darken
643 | BlendMode::Lighten
644 )
645 }
646
647 pub fn shader_mode(self) -> u32 {
649 match self {
650 BlendMode::Alpha => 0,
651 BlendMode::Add => 1,
652 BlendMode::Multiply => 2,
653 BlendMode::Screen => 3,
654 BlendMode::Overlay => 4,
655 BlendMode::Darken => 5,
656 BlendMode::Lighten => 6,
657 BlendMode::ColorDodge => 7,
658 BlendMode::ColorBurn => 8,
659 BlendMode::HardLight => 9,
660 BlendMode::SoftLight => 10,
661 BlendMode::Difference => 11,
662 BlendMode::Exclusion => 12,
663 BlendMode::Hue => 13,
664 BlendMode::Saturation => 14,
665 BlendMode::Color => 15,
666 BlendMode::Luminosity => 16,
667 }
668 }
669}
670
671#[derive(Clone, Copy, Debug, PartialEq, Eq)]
672#[non_exhaustive]
673pub enum TextOverflow {
674 Visible,
675 Clip,
676 Ellipsis,
677}
678
679#[derive(Clone, Copy, Debug, PartialEq, Default)]
681pub enum StrokeJoin {
682 #[default]
683 Miter,
685 Round,
687 Bevel,
689}
690
691#[derive(Clone, Copy, Debug, PartialEq, Default)]
693pub enum StrokeCap {
694 #[default]
695 Butt,
697 Round,
700 Square,
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn subcompose_scope_unbounded_has_infinite_max() {
711 let s = SubcomposeScope::UNBOUNDED;
712 assert!(!s.max_width.0.is_finite());
713 assert!(!s.max_height.0.is_finite());
714 assert_eq!(s.min_width, Dp(0.0));
715 assert_eq!(s.min_height, Dp(0.0));
716 }
717
718 #[test]
719 fn subcompose_scope_new_round_trips() {
720 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
721 assert_eq!(s.min_width, Dp(10.0));
722 assert_eq!(s.max_width, Dp(200.0));
723 assert_eq!(s.min_height, Dp(20.0));
724 assert_eq!(s.max_height, Dp(300.0));
725 }
726
727 #[test]
728 fn box_with_constraints_scope_bounded_predicates() {
729 let bounded = BoxWithConstraintsScope {
730 min_width: Dp(0.0),
731 max_width: Dp(360.0),
732 min_height: Dp(0.0),
733 max_height: Dp(640.0),
734 };
735 assert!(bounded.has_bounded_width());
736 assert!(bounded.has_bounded_height());
737
738 let unbounded = BoxWithConstraintsScope {
739 min_width: Dp(0.0),
740 max_width: Dp(f32::INFINITY),
741 min_height: Dp(0.0),
742 max_height: Dp(f32::INFINITY),
743 };
744 assert!(!unbounded.has_bounded_width());
745 assert!(!unbounded.has_bounded_height());
746 }
747
748 #[test]
749 fn view_kind_subcompose_layout_holds_closure() {
750 let v: View = View {
751 id: 0,
752 kind: ViewKind::SubcomposeLayout {
753 content: std::sync::Arc::new(|scope| {
754 let _ = scope.max_width;
755 vec![(0, View::new(0, ViewKind::Box))]
756 }),
757 },
758 modifier: Modifier::default(),
759 children: vec![],
760 scope_key: None,
761 semantics: None,
762 };
763 match &v.kind {
764 ViewKind::SubcomposeLayout { .. } => {}
765 _ => panic!("expected SubcomposeLayout"),
766 }
767 }
768
769 #[test]
770 fn view_kind_subcompose_layout_supports_multiple_slots() {
771 let v: View = View {
772 id: 0,
773 kind: ViewKind::SubcomposeLayout {
774 content: std::sync::Arc::new(|_scope| {
775 vec![
776 (1, View::new(0, ViewKind::Box)),
777 (2, View::new(0, ViewKind::Box)),
778 (3, View::new(0, ViewKind::Box)),
779 ]
780 }),
781 },
782 modifier: Modifier::default(),
783 children: vec![],
784 scope_key: None,
785 semantics: None,
786 };
787 if let ViewKind::SubcomposeLayout { content } = &v.kind {
788 let slots = content(&SubcomposeScope::UNBOUNDED);
789 assert_eq!(slots.len(), 3);
790 assert_eq!(slots[0].0, 1);
791 assert_eq!(slots[1].0, 2);
792 assert_eq!(slots[2].0, 3);
793 } else {
794 panic!("expected SubcomposeLayout");
795 }
796 }
797}