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 color: Color,
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 color: Color,
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 color: Color,
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)]
423#[non_exhaustive]
424pub enum PaintDesc {
425 Solid,
427 Linear {
429 start: Vec2,
430 end: Vec2,
431 start_color: Color,
432 end_color: Color,
433 },
434}
435
436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439#[non_exhaustive]
440#[derive(Default)]
441pub enum BlendMode {
442 #[default]
444 Alpha,
445 Add,
447 Multiply,
449 Overlay,
451}
452
453#[derive(Clone, Copy, Debug, PartialEq, Eq)]
454#[non_exhaustive]
455pub enum TextOverflow {
456 Visible,
457 Clip,
458 Ellipsis,
459}
460
461#[derive(Clone, Copy, Debug, PartialEq, Default)]
463pub enum StrokeJoin {
464 #[default]
465 Miter,
467 Round,
469 Bevel,
471}
472
473#[derive(Clone, Copy, Debug, PartialEq, Default)]
475pub enum StrokeCap {
476 #[default]
477 Butt,
479 Round,
482 Square,
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn subcompose_scope_unbounded_has_infinite_max() {
493 let s = SubcomposeScope::UNBOUNDED;
494 assert!(!s.max_width.0.is_finite());
495 assert!(!s.max_height.0.is_finite());
496 assert_eq!(s.min_width, Dp(0.0));
497 assert_eq!(s.min_height, Dp(0.0));
498 }
499
500 #[test]
501 fn subcompose_scope_new_round_trips() {
502 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
503 assert_eq!(s.min_width, Dp(10.0));
504 assert_eq!(s.max_width, Dp(200.0));
505 assert_eq!(s.min_height, Dp(20.0));
506 assert_eq!(s.max_height, Dp(300.0));
507 }
508
509 #[test]
510 fn box_with_constraints_scope_bounded_predicates() {
511 let bounded = BoxWithConstraintsScope {
512 min_width: Dp(0.0),
513 max_width: Dp(360.0),
514 min_height: Dp(0.0),
515 max_height: Dp(640.0),
516 };
517 assert!(bounded.has_bounded_width());
518 assert!(bounded.has_bounded_height());
519
520 let unbounded = BoxWithConstraintsScope {
521 min_width: Dp(0.0),
522 max_width: Dp(f32::INFINITY),
523 min_height: Dp(0.0),
524 max_height: Dp(f32::INFINITY),
525 };
526 assert!(!unbounded.has_bounded_width());
527 assert!(!unbounded.has_bounded_height());
528 }
529
530 #[test]
531 fn view_kind_subcompose_layout_holds_closure() {
532 let v: View = View {
533 id: 0,
534 kind: ViewKind::SubcomposeLayout {
535 content: std::sync::Arc::new(|scope| {
536 let _ = scope.max_width;
537 vec![(0, View::new(0, ViewKind::Box))]
538 }),
539 },
540 modifier: Modifier::default(),
541 children: vec![],
542 scope_key: None,
543 semantics: None,
544 };
545 match &v.kind {
546 ViewKind::SubcomposeLayout { .. } => {}
547 _ => panic!("expected SubcomposeLayout"),
548 }
549 }
550
551 #[test]
552 fn view_kind_subcompose_layout_supports_multiple_slots() {
553 let v: View = View {
554 id: 0,
555 kind: ViewKind::SubcomposeLayout {
556 content: std::sync::Arc::new(|_scope| {
557 vec![
558 (1, View::new(0, ViewKind::Box)),
559 (2, View::new(0, ViewKind::Box)),
560 (3, View::new(0, ViewKind::Box)),
561 ]
562 }),
563 },
564 modifier: Modifier::default(),
565 children: vec![],
566 scope_key: None,
567 semantics: None,
568 };
569 if let ViewKind::SubcomposeLayout { content } = &v.kind {
570 let slots = content(&SubcomposeScope::UNBOUNDED);
571 assert_eq!(slots.len(), 3);
572 assert_eq!(slots[0].0, 1);
573 assert_eq!(slots[1].0, 2);
574 assert_eq!(slots[2].0, 3);
575 } else {
576 panic!("expected SubcomposeLayout");
577 }
578 }
579}