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
83pub type Callback = Rc<dyn Fn()>;
84
85#[derive(Clone)]
86pub struct OverlayEntry {
87 pub id: u64,
88 pub view: Box<View>,
89}
90
91#[derive(Clone)]
92#[non_exhaustive]
93pub enum ViewKind {
94 Box,
95 Row,
96 Column,
97 ZStack,
98 OverlayHost,
99 Text {
100 text: String,
101 color: Color,
102 font_size: Sp,
103 soft_wrap: bool,
104 max_lines: Option<usize>,
105 overflow: TextOverflow,
106 font_family: Option<&'static str>,
107 annotations: Option<Arc<[TextSpan]>>,
108 text_align: TextAlign,
109 font_weight: FontWeight,
110 font_style: FontStyle,
111 text_decoration: TextDecoration,
112 letter_spacing: Sp,
113 line_height: Sp,
114 url: Option<Arc<str>>,
116 font_variation_settings: Option<Arc<str>>,
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 Expander {
140 expanded: bool,
141 on_toggle: Option<Callback>,
142 },
143 TreeRow {
146 depth: usize,
147 has_children: bool,
148 is_expanded: bool,
149 is_selected: bool,
150 on_toggle: Option<Callback>,
151 on_select: Option<Callback>,
152 },
153}
154
155impl std::fmt::Debug for ViewKind {
156 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::Box => f.write_str("Box"),
159 Self::Row => f.write_str("Row"),
160 Self::Column => f.write_str("Column"),
161 Self::ZStack => f.write_str("ZStack"),
162 Self::OverlayHost => f.write_str("OverlayHost"),
163
164 Self::Image { .. } => f.write_str("Image"),
165 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
166 Self::Text { text, .. } => write!(f, "Text({:?})", text),
167
168 Self::Expander { expanded, .. } => {
169 if *expanded {
170 write!(f, "Expander(expanded)")
171 } else {
172 write!(f, "Expander(collapsed)")
173 }
174 }
175 Self::TreeRow {
176 depth,
177 has_children,
178 is_expanded,
179 is_selected,
180 ..
181 } => {
182 write!(
183 f,
184 "TreeRow(depth={}, children={}, expanded={}, selected={})",
185 depth, has_children, is_expanded, is_selected
186 )
187 }
188 }
189 }
190}
191
192#[derive(Clone, Debug)]
193pub struct View {
194 pub id: ViewId,
195 pub kind: ViewKind,
196 pub modifier: Modifier,
197 pub children: Vec<View>,
198 pub semantics: Option<crate::semantics::Semantics>,
199 pub scope_key: Option<String>,
203}
204
205impl View {
206 pub fn new(id: ViewId, kind: ViewKind) -> Self {
207 View {
208 id,
209 kind,
210 modifier: Modifier::default(),
211 children: vec![],
212 semantics: None,
213 scope_key: None,
214 }
215 }
216 pub fn modifier(mut self, m: Modifier) -> Self {
217 self.modifier = m;
218 self
219 }
220 pub fn disabled(mut self) -> Self {
222 self.modifier.disabled = true;
223 self
224 }
225 pub fn with_children(mut self, kids: Vec<View>) -> Self {
226 self.children = kids;
227 self
228 }
229 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
230 self.children = kids.into();
231 self
232 }
233 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
234 self.semantics = Some(s);
235 self
236 }
237}
238
239#[derive(Clone, Debug, Default)]
241pub struct Scene {
242 pub clear_color: Color,
243 pub nodes: Vec<SceneNode>,
244}
245
246#[derive(Clone, Debug, PartialEq)]
248pub struct TextExtraStyle {
249 pub text_direction: TextDirection,
250 pub font_synthesis: FontSynthesis,
251 pub baseline_shift: BaselineShift,
252 pub draw_style: DrawStyle,
253}
254
255impl Default for TextExtraStyle {
256 fn default() -> Self {
257 Self {
258 text_direction: TextDirection::Ltr,
259 font_synthesis: FontSynthesis::Unspecified,
260 baseline_shift: BaselineShift::Unspecified,
261 draw_style: DrawStyle::Fill,
262 }
263 }
264}
265
266#[derive(Clone, Copy, Debug)]
267pub struct PaintCallbackInfo {
268 pub viewport: Rect,
270 pub clip_rect: Rect,
272 pub pixels_per_point: f32,
274 pub screen_size_px: [u32; 2],
276}
277
278pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
279
280#[derive(Clone, Debug)]
284#[non_exhaustive]
285pub enum SceneNode {
286 Rect {
287 rect: Rect,
288 brush: Brush,
289 radius: [Px; 4],
290 },
291 Border {
292 rect: Rect,
293 color: Color,
294 width: Px,
295 radius: [Px; 4],
296 },
297 Text {
298 rect: Rect,
299 text: Arc<str>,
300 color: Color,
301 size: Px,
302 font_family: Option<&'static str>,
303 text_align: TextAlign,
304 font_weight: FontWeight,
305 font_style: FontStyle,
306 text_decoration: TextDecoration,
307 letter_spacing: Px,
308 line_height: Px,
309 extra_style: TextExtraStyle,
311 url: Option<Arc<str>>,
313 font_variation_settings: Option<Arc<str>>,
315 },
316 Ellipse {
317 rect: Rect,
318 brush: Brush,
319 },
320 EllipseBorder {
321 rect: Rect,
322 color: Color,
323 width: Px,
324 },
325 PushClip {
326 rect: Rect,
327 radius: [Px; 4],
328 op: ClipOp,
329 },
330 PopClip,
331 PushTransform {
332 transform: Transform,
333 },
334 PopTransform,
335 Image {
336 rect: Rect,
337 handle: ImageHandle,
338 tint: Color,
339 fit: ImageFit,
340 },
341 Shadow {
344 rect: Rect,
345 radius: [Px; 4],
346 elevation: Px,
347 color: Color,
348 },
349 BeginLayer {
356 rect: Rect,
357 layer_id: u32,
358 alpha: f32,
359 blur_radius_x: Px,
360 blur_radius_y: Px,
361 rectangle_edge: bool,
362 },
363 EndLayer {
365 layer_id: u32,
366 },
367 CompositeShadow {
372 layer_id: u32,
373 blur_px: Px,
374 offset_px: (Px, Px),
375 color: Color,
376 },
377 Arc {
379 rect: Rect,
380 start_angle: f32,
381 sweep_angle: f32,
382 stroke_width: Px,
383 color: Color,
384 cap: StrokeCap,
385 },
386 VectorMesh {
392 mesh: Arc<VectorMeshData>,
393 transform: [f32; 6],
394 paint: PaintDesc,
395 clip: Option<u32>,
398 blend: BlendMode,
399 },
400 VectorOverlay {
404 meshes: Arc<[VectorMeshData]>,
405 },
406 PushVectorClip {
410 mesh: Arc<VectorMeshData>,
411 },
412 PopVectorClip,
414 Callback {
416 rect: Rect,
417 payload: PaintCallbackPayload,
418 },
419}
420
421#[derive(Clone, Debug, Default)]
423pub struct VectorMeshData {
424 pub vertices: Arc<[VectorVertex]>,
425 pub indices: Arc<[u32]>,
426}
427
428#[derive(Clone, Copy, Debug, PartialEq)]
432#[repr(C)]
433pub struct VectorVertex {
434 pub pos: [f32; 2],
435 pub color: [f32; 4],
436 pub uv: [f32; 2],
437}
438
439#[derive(Clone, Copy, Debug, PartialEq)]
441#[non_exhaustive]
442pub enum PaintDesc {
443 Solid,
445 Linear {
447 start: Vec2,
448 end: Vec2,
449 start_color: Color,
450 end_color: Color,
451 },
452}
453
454#[derive(Clone, Copy, Debug, PartialEq, Eq)]
457#[non_exhaustive]
458#[derive(Default)]
459pub enum BlendMode {
460 #[default]
462 Alpha,
463 Add,
465 Multiply,
467 Overlay,
469}
470
471#[derive(Clone, Copy, Debug, PartialEq, Eq)]
472#[non_exhaustive]
473pub enum TextOverflow {
474 Visible,
475 Clip,
476 Ellipsis,
477}
478
479#[derive(Clone, Copy, Debug, PartialEq, Default)]
481pub enum StrokeJoin {
482 #[default]
483 Miter,
485 Round,
487 Bevel,
489}
490
491#[derive(Clone, Copy, Debug, PartialEq, Default)]
493pub enum StrokeCap {
494 #[default]
495 Butt,
497 Round,
500 Square,
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn subcompose_scope_unbounded_has_infinite_max() {
511 let s = SubcomposeScope::UNBOUNDED;
512 assert!(!s.max_width.0.is_finite());
513 assert!(!s.max_height.0.is_finite());
514 assert_eq!(s.min_width, Dp(0.0));
515 assert_eq!(s.min_height, Dp(0.0));
516 }
517
518 #[test]
519 fn subcompose_scope_new_round_trips() {
520 let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
521 assert_eq!(s.min_width, Dp(10.0));
522 assert_eq!(s.max_width, Dp(200.0));
523 assert_eq!(s.min_height, Dp(20.0));
524 assert_eq!(s.max_height, Dp(300.0));
525 }
526
527 #[test]
528 fn box_with_constraints_scope_bounded_predicates() {
529 let bounded = BoxWithConstraintsScope {
530 min_width: Dp(0.0),
531 max_width: Dp(360.0),
532 min_height: Dp(0.0),
533 max_height: Dp(640.0),
534 };
535 assert!(bounded.has_bounded_width());
536 assert!(bounded.has_bounded_height());
537
538 let unbounded = BoxWithConstraintsScope {
539 min_width: Dp(0.0),
540 max_width: Dp(f32::INFINITY),
541 min_height: Dp(0.0),
542 max_height: Dp(f32::INFINITY),
543 };
544 assert!(!unbounded.has_bounded_width());
545 assert!(!unbounded.has_bounded_height());
546 }
547
548 #[test]
549 fn view_kind_subcompose_layout_holds_closure() {
550 let v: View = View {
551 id: 0,
552 kind: ViewKind::SubcomposeLayout {
553 content: std::sync::Arc::new(|scope| {
554 let _ = scope.max_width;
555 vec![(0, View::new(0, ViewKind::Box))]
556 }),
557 },
558 modifier: Modifier::default(),
559 children: vec![],
560 scope_key: None,
561 semantics: None,
562 };
563 match &v.kind {
564 ViewKind::SubcomposeLayout { .. } => {}
565 _ => panic!("expected SubcomposeLayout"),
566 }
567 }
568
569 #[test]
570 fn view_kind_subcompose_layout_supports_multiple_slots() {
571 let v: View = View {
572 id: 0,
573 kind: ViewKind::SubcomposeLayout {
574 content: std::sync::Arc::new(|_scope| {
575 vec![
576 (1, View::new(0, ViewKind::Box)),
577 (2, View::new(0, ViewKind::Box)),
578 (3, View::new(0, ViewKind::Box)),
579 ]
580 }),
581 },
582 modifier: Modifier::default(),
583 children: vec![],
584 scope_key: None,
585 semantics: None,
586 };
587 if let ViewKind::SubcomposeLayout { content } = &v.kind {
588 let slots = content(&SubcomposeScope::UNBOUNDED);
589 assert_eq!(slots.len(), 3);
590 assert_eq!(slots[0].0, 1);
591 assert_eq!(slots[1].0, 2);
592 assert_eq!(slots[2].0, 3);
593 } else {
594 panic!("expected SubcomposeLayout");
595 }
596 }
597}