1use crate::{
2 BaselineShift, Brush, ClipOp, Color, DrawStyle, FontStyle, FontSynthesis, FontWeight, Modifier,
3 Rect, TextAlign, TextDecoration, TextDirection, TextSpan, Transform,
4};
5use std::{fmt::Formatter, rc::Rc, sync::Arc};
6
7#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct SubcomposeScope {
11 pub min_width: f32,
12 pub max_width: f32,
13 pub min_height: f32,
14 pub max_height: f32,
15}
16
17impl SubcomposeScope {
18 pub const UNBOUNDED: Self = Self {
21 min_width: 0.0,
22 max_width: f32::INFINITY,
23 min_height: 0.0,
24 max_height: f32::INFINITY,
25 };
26
27 pub fn new(min_width: f32, max_width: f32, min_height: f32, max_height: f32) -> Self {
29 Self {
30 min_width,
31 max_width,
32 min_height,
33 max_height,
34 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
41pub struct BoxWithConstraintsScope {
42 pub min_width: f32,
43 pub max_width: f32,
44 pub min_height: f32,
45 pub max_height: f32,
46}
47
48impl BoxWithConstraintsScope {
49 pub fn has_bounded_width(&self) -> bool {
51 self.max_width.is_finite()
52 }
53
54 pub fn has_bounded_height(&self) -> bool {
56 self.max_height.is_finite()
57 }
58}
59
60pub type ViewId = u64;
61
62pub type ImageHandle = u64;
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[non_exhaustive]
65pub enum ImageFit {
66 Contain,
67 Cover,
68 FitWidth,
69 FitHeight,
70}
71
72pub type Callback = Rc<dyn Fn()>;
73
74#[derive(Clone)]
75pub struct OverlayEntry {
76 pub id: u64,
77 pub view: Box<View>,
78}
79
80#[derive(Clone)]
81#[non_exhaustive]
82pub enum ViewKind {
83 Box,
84 Row,
85 Column,
86 ZStack,
87 OverlayHost,
88 Text {
89 text: String,
90 color: Color,
91 font_size: f32,
92 soft_wrap: bool,
93 max_lines: Option<usize>,
94 overflow: TextOverflow,
95 font_family: Option<&'static str>,
96 annotations: Option<Arc<[TextSpan]>>,
97 text_align: TextAlign,
98 font_weight: FontWeight,
99 font_style: FontStyle,
100 text_decoration: TextDecoration,
101 letter_spacing: f32,
102 line_height: f32,
103 url: Option<Arc<str>>,
105 font_variation_settings: Option<Arc<str>>,
107 },
108
109 Image {
110 handle: ImageHandle,
111 tint: Color, fit: ImageFit,
113 },
114 SubcomposeLayout {
124 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
125 },
126 Expander {
129 expanded: bool,
130 on_toggle: Option<Callback>,
131 },
132 TreeRow {
135 depth: usize,
136 has_children: bool,
137 is_expanded: bool,
138 is_selected: bool,
139 on_toggle: Option<Callback>,
140 on_select: Option<Callback>,
141 },
142}
143
144impl std::fmt::Debug for ViewKind {
145 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 match self {
147 Self::Box => f.write_str("Box"),
148 Self::Row => f.write_str("Row"),
149 Self::Column => f.write_str("Column"),
150 Self::ZStack => f.write_str("ZStack"),
151 Self::OverlayHost => f.write_str("OverlayHost"),
152
153 Self::Image { .. } => f.write_str("Image"),
154 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
155 Self::Text { text, .. } => write!(f, "Text({:?})", text),
156
157 Self::Expander { expanded, .. } => {
158 if *expanded {
159 write!(f, "Expander(expanded)")
160 } else {
161 write!(f, "Expander(collapsed)")
162 }
163 }
164 Self::TreeRow {
165 depth,
166 has_children,
167 is_expanded,
168 is_selected,
169 ..
170 } => {
171 write!(
172 f,
173 "TreeRow(depth={}, children={}, expanded={}, selected={})",
174 depth, has_children, is_expanded, is_selected
175 )
176 }
177 }
178 }
179}
180
181#[derive(Clone, Debug)]
182pub struct View {
183 pub id: ViewId,
184 pub kind: ViewKind,
185 pub modifier: Modifier,
186 pub children: Vec<View>,
187 pub semantics: Option<crate::semantics::Semantics>,
188 pub scope_key: Option<String>,
192}
193
194impl View {
195 pub fn new(id: ViewId, kind: ViewKind) -> Self {
196 View {
197 id,
198 kind,
199 modifier: Modifier::default(),
200 children: vec![],
201 semantics: None,
202 scope_key: None,
203 }
204 }
205 pub fn modifier(mut self, m: Modifier) -> Self {
206 self.modifier = m;
207 self
208 }
209 pub fn disabled(mut self) -> Self {
211 self.modifier.disabled = true;
212 self
213 }
214 pub fn with_children(mut self, kids: Vec<View>) -> Self {
215 self.children = kids;
216 self
217 }
218 pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
219 self.children = kids.into();
220 self
221 }
222 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
223 self.semantics = Some(s);
224 self
225 }
226}
227
228#[derive(Clone, Debug, Default)]
230pub struct Scene {
231 pub clear_color: Color,
232 pub nodes: Vec<SceneNode>,
233}
234
235#[derive(Clone, Debug, PartialEq)]
237pub struct TextExtraStyle {
238 pub text_direction: TextDirection,
239 pub font_synthesis: FontSynthesis,
240 pub baseline_shift: BaselineShift,
241 pub draw_style: DrawStyle,
242}
243
244impl Default for TextExtraStyle {
245 fn default() -> Self {
246 Self {
247 text_direction: TextDirection::Ltr,
248 font_synthesis: FontSynthesis::Unspecified,
249 baseline_shift: BaselineShift::Unspecified,
250 draw_style: DrawStyle::Fill,
251 }
252 }
253}
254
255#[derive(Clone, Debug)]
256#[non_exhaustive]
257pub enum SceneNode {
258 Rect {
259 rect: Rect,
260 brush: Brush,
261 radius: [f32; 4],
262 },
263 Border {
264 rect: Rect,
265 color: Color,
266 width: f32,
267 radius: [f32; 4],
268 },
269 Text {
270 rect: Rect,
271 text: Arc<str>,
272 color: Color,
273 size: f32,
274 font_family: Option<&'static str>,
275 text_align: TextAlign,
276 font_weight: FontWeight,
277 font_style: FontStyle,
278 text_decoration: TextDecoration,
279 letter_spacing: f32,
280 line_height: f32,
281 extra_style: TextExtraStyle,
283 url: Option<Arc<str>>,
285 font_variation_settings: Option<Arc<str>>,
287 },
288 Ellipse {
289 rect: Rect,
290 brush: Brush,
291 },
292 EllipseBorder {
293 rect: Rect,
294 color: Color,
295 width: f32, },
297 PushClip {
298 rect: Rect,
299 radius: [f32; 4],
300 op: ClipOp,
301 },
302 PopClip,
303 PushTransform {
304 transform: Transform,
305 },
306 PopTransform,
307 Image {
308 rect: Rect,
309 handle: ImageHandle,
310 tint: Color,
311 fit: ImageFit,
312 },
313 Shadow {
316 rect: Rect,
317 radius: [f32; 4],
318 elevation: f32,
319 color: Color,
320 },
321 BeginLayer {
329 rect: Rect,
330 layer_id: u32,
331 alpha: f32,
332 blur_radius_x: f32,
333 blur_radius_y: f32,
334 rectangle_edge: bool,
335 },
336 EndLayer {
338 layer_id: u32,
339 },
340 CompositeShadow {
345 layer_id: u32,
346 blur_px: f32,
347 offset_px: (f32, f32),
348 color: Color,
349 },
350 Arc {
352 rect: Rect,
353 start_angle: f32,
354 sweep_angle: f32,
355 stroke_width: f32,
356 color: Color,
357 cap: StrokeCap,
358 },
359}
360
361#[derive(Clone, Copy, Debug, PartialEq, Eq)]
362#[non_exhaustive]
363pub enum TextOverflow {
364 Visible,
365 Clip,
366 Ellipsis,
367}
368
369#[derive(Clone, Copy, Debug, PartialEq, Default)]
371pub enum StrokeJoin {
372 #[default]
373 Miter,
375 Round,
377 Bevel,
379}
380
381#[derive(Clone, Copy, Debug, PartialEq, Default)]
383pub enum StrokeCap {
384 #[default]
385 Butt,
387 Round,
390 Square,
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn subcompose_scope_unbounded_has_infinite_max() {
401 let s = SubcomposeScope::UNBOUNDED;
402 assert!(!s.max_width.is_finite());
403 assert!(!s.max_height.is_finite());
404 assert_eq!(s.min_width, 0.0);
405 assert_eq!(s.min_height, 0.0);
406 }
407
408 #[test]
409 fn subcompose_scope_new_round_trips() {
410 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
411 assert_eq!(s.min_width, 10.0);
412 assert_eq!(s.max_width, 200.0);
413 assert_eq!(s.min_height, 20.0);
414 assert_eq!(s.max_height, 300.0);
415 }
416
417 #[test]
418 fn box_with_constraints_scope_bounded_predicates() {
419 let bounded = BoxWithConstraintsScope {
420 min_width: 0.0,
421 max_width: 360.0,
422 min_height: 0.0,
423 max_height: 640.0,
424 };
425 assert!(bounded.has_bounded_width());
426 assert!(bounded.has_bounded_height());
427
428 let unbounded = BoxWithConstraintsScope {
429 min_width: 0.0,
430 max_width: f32::INFINITY,
431 min_height: 0.0,
432 max_height: f32::INFINITY,
433 };
434 assert!(!unbounded.has_bounded_width());
435 assert!(!unbounded.has_bounded_height());
436 }
437
438 #[test]
439 fn view_kind_subcompose_layout_holds_closure() {
440 let v: View = View {
441 id: 0,
442 kind: ViewKind::SubcomposeLayout {
443 content: std::sync::Arc::new(|scope| {
444 let _ = scope.max_width;
445 vec![(0, View::new(0, ViewKind::Box))]
446 }),
447 },
448 modifier: Modifier::default(),
449 children: vec![],
450 scope_key: None,
451 semantics: None,
452 };
453 match &v.kind {
454 ViewKind::SubcomposeLayout { .. } => {}
455 _ => panic!("expected SubcomposeLayout"),
456 }
457 }
458
459 #[test]
460 fn view_kind_subcompose_layout_supports_multiple_slots() {
461 let v: View = View {
462 id: 0,
463 kind: ViewKind::SubcomposeLayout {
464 content: std::sync::Arc::new(|_scope| {
465 vec![
466 (1, View::new(0, ViewKind::Box)),
467 (2, View::new(0, ViewKind::Box)),
468 (3, View::new(0, ViewKind::Box)),
469 ]
470 }),
471 },
472 modifier: Modifier::default(),
473 children: vec![],
474 scope_key: None,
475 semantics: None,
476 };
477 if let ViewKind::SubcomposeLayout { content } = &v.kind {
478 let slots = content(&SubcomposeScope::UNBOUNDED);
479 assert_eq!(slots.len(), 3);
480 assert_eq!(slots[0].0, 1);
481 assert_eq!(slots[1].0, 2);
482 assert_eq!(slots[2].0, 3);
483 } else {
484 panic!("expected SubcomposeLayout");
485 }
486 }
487}