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)]
126#[non_exhaustive]
127pub enum ImageFit {
128 Contain,
130 Cover,
132 FitWidth,
134 FitHeight,
136 FillBounds,
138 Inside,
140 None,
142 Tile,
146}
147
148#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
153pub enum ImageAlignment {
154 #[default]
156 Center,
157 Begin,
159 End,
161}
162
163impl ImageFit {
164 pub fn scale_factor(self, src: (f32, f32), dst: (f32, f32)) -> (f32, f32) {
168 let fill_w = dst.0 / src.0;
169 let fill_h = dst.1 / src.1;
170 let uniform = |v: f32| (v, v);
171 match self {
172 Self::Contain => uniform(fill_w.min(fill_h)),
173 Self::Cover => uniform(fill_w.max(fill_h)),
174 Self::FitWidth => uniform(fill_w),
175 Self::FitHeight => uniform(fill_h),
176 Self::FillBounds => (fill_w, fill_h),
177 Self::Inside => {
178 if src.0 <= dst.0 && src.1 <= dst.1 {
179 uniform(1.0)
180 } else {
181 uniform(fill_w.min(fill_h))
182 }
183 }
184 Self::None | Self::Tile => uniform(1.0),
185 }
186 }
187}
188
189#[derive(Clone)]
190pub struct OverlayEntry {
191 pub id: u64,
192 pub view: Box<View>,
193}
194
195#[derive(Clone)]
196#[non_exhaustive]
197pub enum ViewKind {
198 Box,
199 Row,
200 Column,
201 ZStack,
202 OverlayHost,
203 Text {
204 text: String,
205 color: Color,
206 font_size: Sp,
207 soft_wrap: bool,
208 max_lines: Option<usize>,
209 overflow: TextOverflow,
210 font_family: Option<&'static str>,
211 annotations: Option<Arc<[TextSpan]>>,
212 text_align: TextAlign,
213 font_weight: FontWeight,
214 font_style: FontStyle,
215 text_decoration: TextDecoration,
216 letter_spacing: Sp,
217 line_height: Sp,
218 url: Option<Arc<str>>,
220 font_variation_settings: Option<Arc<str>>,
222 draw_style: DrawStyle,
224 },
225
226 Image {
227 handle: ImageHandle,
228 tint: Color,
229 fit: ImageFit,
230 filter: ImageFilter,
231 source_rect: Option<ImageSourceRect>,
232 alignment: ImageAlignment,
233 },
234 SubcomposeLayout {
244 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
245 },
246}
247
248impl std::fmt::Debug for ViewKind {
249 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
250 match self {
251 Self::Box => f.write_str("Box"),
252 Self::Row => f.write_str("Row"),
253 Self::Column => f.write_str("Column"),
254 Self::ZStack => f.write_str("ZStack"),
255 Self::OverlayHost => f.write_str("OverlayHost"),
256
257 Self::Image {
258 handle,
259 tint,
260 fit,
261 filter,
262 source_rect,
263 alignment,
264 } => f
265 .debug_struct("Image")
266 .field("handle", handle)
267 .field("tint", tint)
268 .field("fit", fit)
269 .field("filter", filter)
270 .field("source_rect", source_rect)
271 .field("alignment", alignment)
272 .finish(),
273 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
274 Self::Text { text, .. } => write!(f, "Text({:?})", text),
275 }
276 }
277}
278
279#[derive(Clone, Debug)]
280pub struct View {
281 pub id: ViewId,
282 pub kind: ViewKind,
283 pub modifier: Modifier,
284 pub children: Vec<View>,
285 pub semantics: Option<crate::semantics::Semantics>,
286 pub scope_key: Option<String>,
290}
291
292impl View {
293 pub fn new(id: ViewId, kind: ViewKind) -> Self {
294 View {
295 id,
296 kind,
297 modifier: Modifier::default(),
298 children: vec![],
299 semantics: None,
300 scope_key: None,
301 }
302 }
303 pub fn modifier(mut self, m: Modifier) -> Self {
304 self.modifier = m;
305 self
306 }
307 pub fn disabled(mut self) -> Self {
309 self.modifier.disabled = true;
310 self
311 }
312 pub fn with_children(mut self, kids: Vec<View>) -> Self {
313 self.children = kids;
314 self
315 }
316 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
317 self.children = kids.into();
318 self
319 }
320 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
321 self.semantics = Some(s);
322 self
323 }
324}
325
326#[derive(Clone, Debug, Default)]
328pub struct Scene {
329 pub clear_color: Color,
330 pub nodes: Vec<SceneNode>,
331}
332
333#[derive(Clone, Debug, PartialEq)]
335pub struct TextExtraStyle {
336 pub text_direction: TextDirection,
337 pub font_synthesis: FontSynthesis,
338 pub baseline_shift: BaselineShift,
339 pub draw_style: DrawStyle,
340}
341
342impl Default for TextExtraStyle {
343 fn default() -> Self {
344 Self {
345 text_direction: TextDirection::Ltr,
346 font_synthesis: FontSynthesis::Unspecified,
347 baseline_shift: BaselineShift::Unspecified,
348 draw_style: DrawStyle::Fill,
349 }
350 }
351}
352
353#[derive(Clone, Copy, Debug)]
354pub struct PaintCallbackInfo {
355 pub viewport: Rect,
357 pub clip_rect: Rect,
359 pub pixels_per_point: f32,
361 pub screen_size_px: [u32; 2],
363}
364
365pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
366
367#[derive(Clone, Debug)]
371#[non_exhaustive]
372pub enum SceneNode {
373 Rect {
374 rect: Rect,
375 brush: Brush,
376 radius: [Px; 4],
377 },
378 Border {
379 rect: Rect,
380 brush: Brush,
381 width: Px,
382 radius: [Px; 4],
383 },
384 Text {
385 rect: Rect,
386 text: Arc<str>,
387 color: Color,
388 size: Px,
389 font_family: Option<&'static str>,
390 text_align: TextAlign,
391 font_weight: FontWeight,
392 font_style: FontStyle,
393 text_decoration: TextDecoration,
394 letter_spacing: Px,
395 line_height: Px,
396 extra_style: TextExtraStyle,
398 url: Option<Arc<str>>,
400 font_variation_settings: Option<Arc<str>>,
402 },
403 Ellipse {
404 rect: Rect,
405 brush: Brush,
406 },
407 EllipseBorder {
408 rect: Rect,
409 brush: Brush,
410 width: Px,
411 },
412 PushClip {
413 rect: Rect,
414 radius: [Px; 4],
415 op: ClipOp,
416 },
417 PopClip,
418 PushTransform {
419 transform: Transform,
420 },
421 PopTransform,
422 Image {
423 rect: Rect,
424 handle: ImageHandle,
425 tint: Color,
426 fit: ImageFit,
427 filter: ImageFilter,
428 source_rect: Option<ImageSourceRect>,
429 alignment: ImageAlignment,
430 },
431 Coverage {
438 rect: Rect,
439 handle: ImageHandle,
440 color: Color,
441 },
442 Shadow {
445 rect: Rect,
446 radius: [Px; 4],
447 elevation: Px,
448 color: Color,
449 },
450 BeginLayer {
457 rect: Rect,
458 layer_id: u32,
459 alpha: f32,
460 blur_radius_x: Px,
461 blur_radius_y: Px,
462 rectangle_edge: bool,
463 },
464 EndLayer {
466 layer_id: u32,
467 },
468 CompositeShadow {
473 layer_id: u32,
474 blur_px: Px,
475 offset_px: (Px, Px),
476 color: Color,
477 },
478 Arc {
480 rect: Rect,
481 start_angle: f32,
482 sweep_angle: f32,
483 stroke_width: Px,
484 brush: Brush,
485 cap: StrokeCap,
486 },
487 VectorMesh {
495 mesh: Arc<VectorMeshData>,
496 transform: [f32; 6],
497 paint: PaintDesc,
498 clip: Option<u32>,
501 blend: BlendMode,
502 },
503 VectorOverlay {
507 meshes: Arc<[VectorMeshData]>,
508 },
509 PushVectorClip {
518 mesh: Arc<VectorMeshData>,
519 op: ClipOp,
520 },
521 PopVectorClip,
523 Callback {
525 rect: Rect,
526 payload: PaintCallbackPayload,
527 },
528}
529
530#[derive(Clone, Debug, Default)]
532pub struct VectorMeshData {
533 pub vertices: Arc<[VectorVertex]>,
534 pub indices: Arc<[u32]>,
535}
536
537#[derive(Clone, Copy, Debug, PartialEq)]
541#[repr(C)]
542pub struct VectorVertex {
543 pub pos: [f32; 2],
544 pub color: [f32; 4],
545 pub uv: [f32; 2],
546}
547
548#[derive(Clone, Copy, Debug, PartialEq)]
551#[non_exhaustive]
552pub enum PaintDesc {
553 Solid,
555 Linear {
557 start: Vec2,
558 end: Vec2,
559 start_color: Color,
560 end_color: Color,
561 },
562 Radial {
564 center: Vec2,
565 radius: f32,
566 start_color: Color,
567 end_color: Color,
568 },
569 Sweep {
571 center: Vec2,
572 start_color: Color,
573 end_color: Color,
574 },
575}
576
577#[derive(Clone, Copy, Debug, PartialEq, Eq)]
583#[non_exhaustive]
584#[derive(Default)]
585pub enum BlendMode {
586 #[default]
588 Alpha,
589 Screen,
591 Overlay,
593 Darken,
595 Lighten,
597 ColorDodge,
599 ColorBurn,
601 HardLight,
603 SoftLight,
605 Difference,
609 Exclusion,
611 Hue,
613 Saturation,
615 Color,
617 Luminosity,
619 Add,
621 Multiply,
623}
624
625impl BlendMode {
626 pub fn needs_isolation(self) -> bool {
630 !matches!(
631 self,
632 BlendMode::Alpha
633 | BlendMode::Add
634 | BlendMode::Multiply
635 | BlendMode::Screen
636 | BlendMode::Darken
637 | BlendMode::Lighten
638 )
639 }
640
641 pub fn shader_mode(self) -> u32 {
643 match self {
644 BlendMode::Alpha => 0,
645 BlendMode::Add => 1,
646 BlendMode::Multiply => 2,
647 BlendMode::Screen => 3,
648 BlendMode::Overlay => 4,
649 BlendMode::Darken => 5,
650 BlendMode::Lighten => 6,
651 BlendMode::ColorDodge => 7,
652 BlendMode::ColorBurn => 8,
653 BlendMode::HardLight => 9,
654 BlendMode::SoftLight => 10,
655 BlendMode::Difference => 11,
656 BlendMode::Exclusion => 12,
657 BlendMode::Hue => 13,
658 BlendMode::Saturation => 14,
659 BlendMode::Color => 15,
660 BlendMode::Luminosity => 16,
661 }
662 }
663}
664
665#[derive(Clone, Copy, Debug, PartialEq, Eq)]
666#[non_exhaustive]
667pub enum TextOverflow {
668 Visible,
669 Clip,
670 Ellipsis,
671}
672
673#[derive(Clone, Copy, Debug, PartialEq, Default)]
675pub enum StrokeJoin {
676 #[default]
677 Miter,
679 Round,
681 Bevel,
683}
684
685#[derive(Clone, Copy, Debug, PartialEq, Default)]
687pub enum StrokeCap {
688 #[default]
689 Butt,
691 Round,
694 Square,
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn subcompose_scope_unbounded_has_infinite_max() {
705 let s = SubcomposeScope::UNBOUNDED;
706 assert!(!s.max_width.0.is_finite());
707 assert!(!s.max_height.0.is_finite());
708 assert_eq!(s.min_width, Dp(0.0));
709 assert_eq!(s.min_height, Dp(0.0));
710 }
711
712 #[test]
713 fn subcompose_scope_new_round_trips() {
714 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
715 assert_eq!(s.min_width, Dp(10.0));
716 assert_eq!(s.max_width, Dp(200.0));
717 assert_eq!(s.min_height, Dp(20.0));
718 assert_eq!(s.max_height, Dp(300.0));
719 }
720
721 #[test]
722 fn box_with_constraints_scope_bounded_predicates() {
723 let bounded = BoxWithConstraintsScope {
724 min_width: Dp(0.0),
725 max_width: Dp(360.0),
726 min_height: Dp(0.0),
727 max_height: Dp(640.0),
728 };
729 assert!(bounded.has_bounded_width());
730 assert!(bounded.has_bounded_height());
731
732 let unbounded = BoxWithConstraintsScope {
733 min_width: Dp(0.0),
734 max_width: Dp(f32::INFINITY),
735 min_height: Dp(0.0),
736 max_height: Dp(f32::INFINITY),
737 };
738 assert!(!unbounded.has_bounded_width());
739 assert!(!unbounded.has_bounded_height());
740 }
741
742 #[test]
743 fn view_kind_subcompose_layout_holds_closure() {
744 let v: View = View {
745 id: 0,
746 kind: ViewKind::SubcomposeLayout {
747 content: std::sync::Arc::new(|scope| {
748 let _ = scope.max_width;
749 vec![(0, View::new(0, ViewKind::Box))]
750 }),
751 },
752 modifier: Modifier::default(),
753 children: vec![],
754 scope_key: None,
755 semantics: None,
756 };
757 match &v.kind {
758 ViewKind::SubcomposeLayout { .. } => {}
759 _ => panic!("expected SubcomposeLayout"),
760 }
761 }
762
763 #[test]
764 fn view_kind_subcompose_layout_supports_multiple_slots() {
765 let v: View = View {
766 id: 0,
767 kind: ViewKind::SubcomposeLayout {
768 content: std::sync::Arc::new(|_scope| {
769 vec![
770 (1, View::new(0, ViewKind::Box)),
771 (2, View::new(0, ViewKind::Box)),
772 (3, View::new(0, ViewKind::Box)),
773 ]
774 }),
775 },
776 modifier: Modifier::default(),
777 children: vec![],
778 scope_key: None,
779 semantics: None,
780 };
781 if let ViewKind::SubcomposeLayout { content } = &v.kind {
782 let slots = content(&SubcomposeScope::UNBOUNDED);
783 assert_eq!(slots.len(), 3);
784 assert_eq!(slots[0].0, 1);
785 assert_eq!(slots[1].0, 2);
786 assert_eq!(slots[2].0, 3);
787 } else {
788 panic!("expected SubcomposeLayout");
789 }
790 }
791}