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 Stack,
87 ZStack,
88 OverlayHost,
89 Text {
90 text: String,
91 color: Color,
92 font_size: f32,
93 soft_wrap: bool,
94 max_lines: Option<usize>,
95 overflow: TextOverflow,
96 font_family: Option<&'static str>,
97 annotations: Option<Arc<[TextSpan]>>,
98 text_align: TextAlign,
99 font_weight: FontWeight,
100 font_style: FontStyle,
101 text_decoration: TextDecoration,
102 letter_spacing: f32,
103 line_height: f32,
104 url: Option<Arc<str>>,
106 },
107
108 Image {
109 handle: ImageHandle,
110 tint: Color, fit: ImageFit,
112 },
113 SubcomposeLayout {
123 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
124 },
125 Expander {
128 expanded: bool,
129 on_toggle: Option<Callback>,
130 },
131 TreeRow {
134 depth: usize,
135 has_children: bool,
136 is_expanded: bool,
137 is_selected: bool,
138 on_toggle: Option<Callback>,
139 on_select: Option<Callback>,
140 },
141}
142
143impl std::fmt::Debug for ViewKind {
144 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
145 match self {
146 Self::Box => f.write_str("Box"),
147 Self::Row => f.write_str("Row"),
148 Self::Column => f.write_str("Column"),
149 Self::Stack => f.write_str("Stack"),
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 semantics(mut self, s: crate::semantics::Semantics) -> Self {
219 self.semantics = Some(s);
220 self
221 }
222}
223
224#[derive(Clone, Debug, Default)]
226pub struct Scene {
227 pub clear_color: Color,
228 pub nodes: Vec<SceneNode>,
229}
230
231#[derive(Clone, Debug, PartialEq)]
233pub struct TextExtraStyle {
234 pub text_direction: TextDirection,
235 pub font_synthesis: FontSynthesis,
236 pub baseline_shift: BaselineShift,
237 pub draw_style: DrawStyle,
238}
239
240impl Default for TextExtraStyle {
241 fn default() -> Self {
242 Self {
243 text_direction: TextDirection::Ltr,
244 font_synthesis: FontSynthesis::Unspecified,
245 baseline_shift: BaselineShift::Unspecified,
246 draw_style: DrawStyle::Fill,
247 }
248 }
249}
250
251#[derive(Clone, Debug)]
252#[non_exhaustive]
253pub enum SceneNode {
254 Rect {
255 rect: Rect,
256 brush: Brush,
257 radius: [f32; 4],
258 },
259 Border {
260 rect: Rect,
261 color: Color,
262 width: f32,
263 radius: [f32; 4],
264 },
265 Text {
266 rect: Rect,
267 text: Arc<str>,
268 color: Color,
269 size: f32,
270 font_family: Option<&'static str>,
271 text_align: TextAlign,
272 font_weight: FontWeight,
273 font_style: FontStyle,
274 text_decoration: TextDecoration,
275 letter_spacing: f32,
276 line_height: f32,
277 extra_style: TextExtraStyle,
279 url: Option<Arc<str>>,
281 },
282 Ellipse {
283 rect: Rect,
284 brush: Brush,
285 },
286 EllipseBorder {
287 rect: Rect,
288 color: Color,
289 width: f32, },
291 PushClip {
292 rect: Rect,
293 radius: [f32; 4],
294 op: ClipOp,
295 },
296 PopClip,
297 PushTransform {
298 transform: Transform,
299 },
300 PopTransform,
301 Image {
302 rect: Rect,
303 handle: ImageHandle,
304 tint: Color,
305 fit: ImageFit,
306 },
307 Shadow {
310 rect: Rect,
311 radius: [f32; 4],
312 elevation: f32,
313 color: Color,
314 },
315 BeginLayer {
323 rect: Rect,
324 layer_id: u32,
325 alpha: f32,
326 blur_radius_x: f32,
327 blur_radius_y: f32,
328 rectangle_edge: bool,
329 },
330 EndLayer {
332 layer_id: u32,
333 },
334 CompositeShadow {
339 layer_id: u32,
340 blur_px: f32,
341 offset_px: (f32, f32),
342 color: Color,
343 },
344 Arc {
346 rect: Rect,
347 start_angle: f32,
348 sweep_angle: f32,
349 stroke_width: f32,
350 color: Color,
351 cap: StrokeCap,
352 },
353}
354
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356#[non_exhaustive]
357pub enum TextOverflow {
358 Visible,
359 Clip,
360 Ellipsis,
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Default)]
365pub enum StrokeJoin {
366 #[default]
367 Miter,
369 Round,
371 Bevel,
373}
374
375#[derive(Clone, Copy, Debug, PartialEq, Default)]
377pub enum StrokeCap {
378 #[default]
379 Butt,
381 Round,
384 Square,
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn subcompose_scope_unbounded_has_infinite_max() {
395 let s = SubcomposeScope::UNBOUNDED;
396 assert!(!s.max_width.is_finite());
397 assert!(!s.max_height.is_finite());
398 assert_eq!(s.min_width, 0.0);
399 assert_eq!(s.min_height, 0.0);
400 }
401
402 #[test]
403 fn subcompose_scope_new_round_trips() {
404 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
405 assert_eq!(s.min_width, 10.0);
406 assert_eq!(s.max_width, 200.0);
407 assert_eq!(s.min_height, 20.0);
408 assert_eq!(s.max_height, 300.0);
409 }
410
411 #[test]
412 fn box_with_constraints_scope_bounded_predicates() {
413 let bounded = BoxWithConstraintsScope {
414 min_width: 0.0,
415 max_width: 360.0,
416 min_height: 0.0,
417 max_height: 640.0,
418 };
419 assert!(bounded.has_bounded_width());
420 assert!(bounded.has_bounded_height());
421
422 let unbounded = BoxWithConstraintsScope {
423 min_width: 0.0,
424 max_width: f32::INFINITY,
425 min_height: 0.0,
426 max_height: f32::INFINITY,
427 };
428 assert!(!unbounded.has_bounded_width());
429 assert!(!unbounded.has_bounded_height());
430 }
431
432 #[test]
433 fn view_kind_subcompose_layout_holds_closure() {
434 let v: View = View {
435 id: 0,
436 kind: ViewKind::SubcomposeLayout {
437 content: std::sync::Arc::new(|scope| {
438 let _ = scope.max_width;
439 vec![(0, View::new(0, ViewKind::Box))]
440 }),
441 },
442 modifier: Modifier::default(),
443 children: vec![],
444 scope_key: None,
445 semantics: None,
446 };
447 match &v.kind {
448 ViewKind::SubcomposeLayout { .. } => {}
449 _ => panic!("expected SubcomposeLayout"),
450 }
451 }
452
453 #[test]
454 fn view_kind_subcompose_layout_supports_multiple_slots() {
455 let v: View = View {
456 id: 0,
457 kind: ViewKind::SubcomposeLayout {
458 content: std::sync::Arc::new(|_scope| {
459 vec![
460 (1, View::new(0, ViewKind::Box)),
461 (2, View::new(0, ViewKind::Box)),
462 (3, View::new(0, ViewKind::Box)),
463 ]
464 }),
465 },
466 modifier: Modifier::default(),
467 children: vec![],
468 scope_key: None,
469 semantics: None,
470 };
471 if let ViewKind::SubcomposeLayout { content } = &v.kind {
472 let slots = content(&SubcomposeScope::UNBOUNDED);
473 assert_eq!(slots.len(), 3);
474 assert_eq!(slots[0].0, 1);
475 assert_eq!(slots[1].0, 2);
476 assert_eq!(slots[2].0, 3);
477 } else {
478 panic!("expected SubcomposeLayout");
479 }
480 }
481}