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, rc::Rc, 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 },
117
118 Image {
119 handle: ImageHandle,
120 tint: Color, fit: ImageFit,
122 },
123 SubcomposeLayout {
133 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
134 },
135}
136
137impl std::fmt::Debug for ViewKind {
138 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
139 match self {
140 Self::Box => f.write_str("Box"),
141 Self::Row => f.write_str("Row"),
142 Self::Column => f.write_str("Column"),
143 Self::ZStack => f.write_str("ZStack"),
144 Self::OverlayHost => f.write_str("OverlayHost"),
145
146 Self::Image { .. } => f.write_str("Image"),
147 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
148 Self::Text { text, .. } => write!(f, "Text({:?})", text),
149 }
150 }
151}
152
153#[derive(Clone, Debug)]
154pub struct View {
155 pub id: ViewId,
156 pub kind: ViewKind,
157 pub modifier: Modifier,
158 pub children: Vec<View>,
159 pub semantics: Option<crate::semantics::Semantics>,
160 pub scope_key: Option<String>,
164}
165
166impl View {
167 pub fn new(id: ViewId, kind: ViewKind) -> Self {
168 View {
169 id,
170 kind,
171 modifier: Modifier::default(),
172 children: vec![],
173 semantics: None,
174 scope_key: None,
175 }
176 }
177 pub fn modifier(mut self, m: Modifier) -> Self {
178 self.modifier = m;
179 self
180 }
181 pub fn disabled(mut self) -> Self {
183 self.modifier.disabled = true;
184 self
185 }
186 pub fn with_children(mut self, kids: Vec<View>) -> Self {
187 self.children = kids;
188 self
189 }
190 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
191 self.children = kids.into();
192 self
193 }
194 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
195 self.semantics = Some(s);
196 self
197 }
198}
199
200#[derive(Clone, Debug, Default)]
202pub struct Scene {
203 pub clear_color: Color,
204 pub nodes: Vec<SceneNode>,
205}
206
207#[derive(Clone, Debug, PartialEq)]
209pub struct TextExtraStyle {
210 pub text_direction: TextDirection,
211 pub font_synthesis: FontSynthesis,
212 pub baseline_shift: BaselineShift,
213 pub draw_style: DrawStyle,
214}
215
216impl Default for TextExtraStyle {
217 fn default() -> Self {
218 Self {
219 text_direction: TextDirection::Ltr,
220 font_synthesis: FontSynthesis::Unspecified,
221 baseline_shift: BaselineShift::Unspecified,
222 draw_style: DrawStyle::Fill,
223 }
224 }
225}
226
227#[derive(Clone, Copy, Debug)]
228pub struct PaintCallbackInfo {
229 pub viewport: Rect,
231 pub clip_rect: Rect,
233 pub pixels_per_point: f32,
235 pub screen_size_px: [u32; 2],
237}
238
239pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
240
241#[derive(Clone, Debug)]
245#[non_exhaustive]
246pub enum SceneNode {
247 Rect {
248 rect: Rect,
249 brush: Brush,
250 radius: [Px; 4],
251 },
252 Border {
253 rect: Rect,
254 color: Color,
255 width: Px,
256 radius: [Px; 4],
257 },
258 Text {
259 rect: Rect,
260 text: Arc<str>,
261 color: Color,
262 size: Px,
263 font_family: Option<&'static str>,
264 text_align: TextAlign,
265 font_weight: FontWeight,
266 font_style: FontStyle,
267 text_decoration: TextDecoration,
268 letter_spacing: Px,
269 line_height: Px,
270 extra_style: TextExtraStyle,
272 url: Option<Arc<str>>,
274 font_variation_settings: Option<Arc<str>>,
276 },
277 Ellipse {
278 rect: Rect,
279 brush: Brush,
280 },
281 EllipseBorder {
282 rect: Rect,
283 color: Color,
284 width: Px,
285 },
286 PushClip {
287 rect: Rect,
288 radius: [Px; 4],
289 op: ClipOp,
290 },
291 PopClip,
292 PushTransform {
293 transform: Transform,
294 },
295 PopTransform,
296 Image {
297 rect: Rect,
298 handle: ImageHandle,
299 tint: Color,
300 fit: ImageFit,
301 },
302 Coverage {
309 rect: Rect,
310 handle: ImageHandle,
311 color: Color,
312 },
313 Shadow {
316 rect: Rect,
317 radius: [Px; 4],
318 elevation: Px,
319 color: Color,
320 },
321 BeginLayer {
328 rect: Rect,
329 layer_id: u32,
330 alpha: f32,
331 blur_radius_x: Px,
332 blur_radius_y: Px,
333 rectangle_edge: bool,
334 },
335 EndLayer {
337 layer_id: u32,
338 },
339 CompositeShadow {
344 layer_id: u32,
345 blur_px: Px,
346 offset_px: (Px, Px),
347 color: Color,
348 },
349 Arc {
351 rect: Rect,
352 start_angle: f32,
353 sweep_angle: f32,
354 stroke_width: Px,
355 color: Color,
356 cap: StrokeCap,
357 },
358 VectorMesh {
366 mesh: Arc<VectorMeshData>,
367 transform: [f32; 6],
368 paint: PaintDesc,
369 clip: Option<u32>,
372 blend: BlendMode,
373 },
374 VectorOverlay {
378 meshes: Arc<[VectorMeshData]>,
379 },
380 PushVectorClip {
389 mesh: Arc<VectorMeshData>,
390 op: ClipOp,
391 },
392 PopVectorClip,
394 Callback {
396 rect: Rect,
397 payload: PaintCallbackPayload,
398 },
399}
400
401#[derive(Clone, Debug, Default)]
403pub struct VectorMeshData {
404 pub vertices: Arc<[VectorVertex]>,
405 pub indices: Arc<[u32]>,
406}
407
408#[derive(Clone, Copy, Debug, PartialEq)]
412#[repr(C)]
413pub struct VectorVertex {
414 pub pos: [f32; 2],
415 pub color: [f32; 4],
416 pub uv: [f32; 2],
417}
418
419#[derive(Clone, Copy, Debug, PartialEq)]
421#[non_exhaustive]
422pub enum PaintDesc {
423 Solid,
425 Linear {
427 start: Vec2,
428 end: Vec2,
429 start_color: Color,
430 end_color: Color,
431 },
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Eq)]
437#[non_exhaustive]
438#[derive(Default)]
439pub enum BlendMode {
440 #[default]
442 Alpha,
443 Add,
445 Multiply,
447 Overlay,
449}
450
451#[derive(Clone, Copy, Debug, PartialEq, Eq)]
452#[non_exhaustive]
453pub enum TextOverflow {
454 Visible,
455 Clip,
456 Ellipsis,
457}
458
459#[derive(Clone, Copy, Debug, PartialEq, Default)]
461pub enum StrokeJoin {
462 #[default]
463 Miter,
465 Round,
467 Bevel,
469}
470
471#[derive(Clone, Copy, Debug, PartialEq, Default)]
473pub enum StrokeCap {
474 #[default]
475 Butt,
477 Round,
480 Square,
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn subcompose_scope_unbounded_has_infinite_max() {
491 let s = SubcomposeScope::UNBOUNDED;
492 assert!(!s.max_width.0.is_finite());
493 assert!(!s.max_height.0.is_finite());
494 assert_eq!(s.min_width, Dp(0.0));
495 assert_eq!(s.min_height, Dp(0.0));
496 }
497
498 #[test]
499 fn subcompose_scope_new_round_trips() {
500 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
501 assert_eq!(s.min_width, Dp(10.0));
502 assert_eq!(s.max_width, Dp(200.0));
503 assert_eq!(s.min_height, Dp(20.0));
504 assert_eq!(s.max_height, Dp(300.0));
505 }
506
507 #[test]
508 fn box_with_constraints_scope_bounded_predicates() {
509 let bounded = BoxWithConstraintsScope {
510 min_width: Dp(0.0),
511 max_width: Dp(360.0),
512 min_height: Dp(0.0),
513 max_height: Dp(640.0),
514 };
515 assert!(bounded.has_bounded_width());
516 assert!(bounded.has_bounded_height());
517
518 let unbounded = BoxWithConstraintsScope {
519 min_width: Dp(0.0),
520 max_width: Dp(f32::INFINITY),
521 min_height: Dp(0.0),
522 max_height: Dp(f32::INFINITY),
523 };
524 assert!(!unbounded.has_bounded_width());
525 assert!(!unbounded.has_bounded_height());
526 }
527
528 #[test]
529 fn view_kind_subcompose_layout_holds_closure() {
530 let v: View = View {
531 id: 0,
532 kind: ViewKind::SubcomposeLayout {
533 content: std::sync::Arc::new(|scope| {
534 let _ = scope.max_width;
535 vec![(0, View::new(0, ViewKind::Box))]
536 }),
537 },
538 modifier: Modifier::default(),
539 children: vec![],
540 scope_key: None,
541 semantics: None,
542 };
543 match &v.kind {
544 ViewKind::SubcomposeLayout { .. } => {}
545 _ => panic!("expected SubcomposeLayout"),
546 }
547 }
548
549 #[test]
550 fn view_kind_subcompose_layout_supports_multiple_slots() {
551 let v: View = View {
552 id: 0,
553 kind: ViewKind::SubcomposeLayout {
554 content: std::sync::Arc::new(|_scope| {
555 vec![
556 (1, View::new(0, ViewKind::Box)),
557 (2, View::new(0, ViewKind::Box)),
558 (3, View::new(0, ViewKind::Box)),
559 ]
560 }),
561 },
562 modifier: Modifier::default(),
563 children: vec![],
564 scope_key: None,
565 semantics: None,
566 };
567 if let ViewKind::SubcomposeLayout { content } = &v.kind {
568 let slots = content(&SubcomposeScope::UNBOUNDED);
569 assert_eq!(slots.len(), 3);
570 assert_eq!(slots[0].0, 1);
571 assert_eq!(slots[1].0, 2);
572 assert_eq!(slots[2].0, 3);
573 } else {
574 panic!("expected SubcomposeLayout");
575 }
576 }
577}