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)]
453#[non_exhaustive]
454#[derive(Default)]
455pub enum BlendMode {
456 #[default]
458 Alpha,
459 Add,
461 Multiply,
463 Overlay,
465}
466
467#[derive(Clone, Copy, Debug, PartialEq, Eq)]
468#[non_exhaustive]
469pub enum TextOverflow {
470 Visible,
471 Clip,
472 Ellipsis,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq, Default)]
477pub enum StrokeJoin {
478 #[default]
479 Miter,
481 Round,
483 Bevel,
485}
486
487#[derive(Clone, Copy, Debug, PartialEq, Default)]
489pub enum StrokeCap {
490 #[default]
491 Butt,
493 Round,
496 Square,
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn subcompose_scope_unbounded_has_infinite_max() {
507 let s = SubcomposeScope::UNBOUNDED;
508 assert!(!s.max_width.0.is_finite());
509 assert!(!s.max_height.0.is_finite());
510 assert_eq!(s.min_width, Dp(0.0));
511 assert_eq!(s.min_height, Dp(0.0));
512 }
513
514 #[test]
515 fn subcompose_scope_new_round_trips() {
516 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
517 assert_eq!(s.min_width, Dp(10.0));
518 assert_eq!(s.max_width, Dp(200.0));
519 assert_eq!(s.min_height, Dp(20.0));
520 assert_eq!(s.max_height, Dp(300.0));
521 }
522
523 #[test]
524 fn box_with_constraints_scope_bounded_predicates() {
525 let bounded = BoxWithConstraintsScope {
526 min_width: Dp(0.0),
527 max_width: Dp(360.0),
528 min_height: Dp(0.0),
529 max_height: Dp(640.0),
530 };
531 assert!(bounded.has_bounded_width());
532 assert!(bounded.has_bounded_height());
533
534 let unbounded = BoxWithConstraintsScope {
535 min_width: Dp(0.0),
536 max_width: Dp(f32::INFINITY),
537 min_height: Dp(0.0),
538 max_height: Dp(f32::INFINITY),
539 };
540 assert!(!unbounded.has_bounded_width());
541 assert!(!unbounded.has_bounded_height());
542 }
543
544 #[test]
545 fn view_kind_subcompose_layout_holds_closure() {
546 let v: View = View {
547 id: 0,
548 kind: ViewKind::SubcomposeLayout {
549 content: std::sync::Arc::new(|scope| {
550 let _ = scope.max_width;
551 vec![(0, View::new(0, ViewKind::Box))]
552 }),
553 },
554 modifier: Modifier::default(),
555 children: vec![],
556 scope_key: None,
557 semantics: None,
558 };
559 match &v.kind {
560 ViewKind::SubcomposeLayout { .. } => {}
561 _ => panic!("expected SubcomposeLayout"),
562 }
563 }
564
565 #[test]
566 fn view_kind_subcompose_layout_supports_multiple_slots() {
567 let v: View = View {
568 id: 0,
569 kind: ViewKind::SubcomposeLayout {
570 content: std::sync::Arc::new(|_scope| {
571 vec![
572 (1, View::new(0, ViewKind::Box)),
573 (2, View::new(0, ViewKind::Box)),
574 (3, View::new(0, ViewKind::Box)),
575 ]
576 }),
577 },
578 modifier: Modifier::default(),
579 children: vec![],
580 scope_key: None,
581 semantics: None,
582 };
583 if let ViewKind::SubcomposeLayout { content } = &v.kind {
584 let slots = content(&SubcomposeScope::UNBOUNDED);
585 assert_eq!(slots.len(), 3);
586 assert_eq!(slots[0].0, 1);
587 assert_eq!(slots[1].0, 2);
588 assert_eq!(slots[2].0, 3);
589 } else {
590 panic!("expected SubcomposeLayout");
591 }
592 }
593}