1use crate::{
2 Brush, ClipOp, Color, FontStyle, FontWeight, Modifier, Rect, TextAlign, TextDecoration,
3 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)]
232#[non_exhaustive]
233pub enum SceneNode {
234 Rect {
235 rect: Rect,
236 brush: Brush,
237 radius: [f32; 4],
238 },
239 Border {
240 rect: Rect,
241 color: Color,
242 width: f32,
243 radius: [f32; 4],
244 },
245 Text {
246 rect: Rect,
247 text: Arc<str>,
248 color: Color,
249 size: f32,
250 font_family: Option<&'static str>,
251 text_align: TextAlign,
252 font_weight: FontWeight,
253 font_style: FontStyle,
254 text_decoration: TextDecoration,
255 letter_spacing: f32,
256 line_height: f32,
257 url: Option<Arc<str>>,
259 },
260 Ellipse {
261 rect: Rect,
262 brush: Brush,
263 },
264 EllipseBorder {
265 rect: Rect,
266 color: Color,
267 width: f32, },
269 PushClip {
270 rect: Rect,
271 radius: [f32; 4],
272 op: ClipOp,
273 },
274 PopClip,
275 PushTransform {
276 transform: Transform,
277 },
278 PopTransform,
279 Image {
280 rect: Rect,
281 handle: ImageHandle,
282 tint: Color,
283 fit: ImageFit,
284 },
285 Shadow {
288 rect: Rect,
289 radius: [f32; 4],
290 elevation: f32,
291 color: Color,
292 },
293 BeginLayer {
301 rect: Rect,
302 layer_id: u32,
303 alpha: f32,
304 blur_radius_x: f32,
305 blur_radius_y: f32,
306 rectangle_edge: bool,
307 },
308 EndLayer {
310 layer_id: u32,
311 },
312 CompositeShadow {
317 layer_id: u32,
318 blur_px: f32,
319 offset_px: (f32, f32),
320 color: Color,
321 },
322 Arc {
324 rect: Rect,
325 start_angle: f32,
326 sweep_angle: f32,
327 stroke_width: f32,
328 color: Color,
329 cap: StrokeCap,
330 },
331}
332
333#[derive(Clone, Copy, Debug, PartialEq, Eq)]
334#[non_exhaustive]
335pub enum TextOverflow {
336 Visible,
337 Clip,
338 Ellipsis,
339}
340
341#[derive(Clone, Copy, Debug, PartialEq)]
345pub enum StrokeCap {
346 Butt,
348 Round,
351 Square,
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn subcompose_scope_unbounded_has_infinite_max() {
362 let s = SubcomposeScope::UNBOUNDED;
363 assert!(!s.max_width.is_finite());
364 assert!(!s.max_height.is_finite());
365 assert_eq!(s.min_width, 0.0);
366 assert_eq!(s.min_height, 0.0);
367 }
368
369 #[test]
370 fn subcompose_scope_new_round_trips() {
371 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
372 assert_eq!(s.min_width, 10.0);
373 assert_eq!(s.max_width, 200.0);
374 assert_eq!(s.min_height, 20.0);
375 assert_eq!(s.max_height, 300.0);
376 }
377
378 #[test]
379 fn box_with_constraints_scope_bounded_predicates() {
380 let bounded = BoxWithConstraintsScope {
381 min_width: 0.0,
382 max_width: 360.0,
383 min_height: 0.0,
384 max_height: 640.0,
385 };
386 assert!(bounded.has_bounded_width());
387 assert!(bounded.has_bounded_height());
388
389 let unbounded = BoxWithConstraintsScope {
390 min_width: 0.0,
391 max_width: f32::INFINITY,
392 min_height: 0.0,
393 max_height: f32::INFINITY,
394 };
395 assert!(!unbounded.has_bounded_width());
396 assert!(!unbounded.has_bounded_height());
397 }
398
399 #[test]
400 fn view_kind_subcompose_layout_holds_closure() {
401 let v: View = View {
402 id: 0,
403 kind: ViewKind::SubcomposeLayout {
404 content: std::sync::Arc::new(|scope| {
405 let _ = scope.max_width;
406 vec![(0, View::new(0, ViewKind::Box))]
407 }),
408 },
409 modifier: Modifier::default(),
410 children: vec![],
411 scope_key: None,
412 semantics: None,
413 };
414 match &v.kind {
415 ViewKind::SubcomposeLayout { .. } => {}
416 _ => panic!("expected SubcomposeLayout"),
417 }
418 }
419
420 #[test]
421 fn view_kind_subcompose_layout_supports_multiple_slots() {
422 let v: View = View {
423 id: 0,
424 kind: ViewKind::SubcomposeLayout {
425 content: std::sync::Arc::new(|_scope| {
426 vec![
427 (1, View::new(0, ViewKind::Box)),
428 (2, View::new(0, ViewKind::Box)),
429 (3, View::new(0, ViewKind::Box)),
430 ]
431 }),
432 },
433 modifier: Modifier::default(),
434 children: vec![],
435 scope_key: None,
436 semantics: None,
437 };
438 if let ViewKind::SubcomposeLayout { content } = &v.kind {
439 let slots = content(&SubcomposeScope::UNBOUNDED);
440 assert_eq!(slots.len(), 3);
441 assert_eq!(slots[0].0, 1);
442 assert_eq!(slots[1].0, 2);
443 assert_eq!(slots[2].0, 3);
444 } else {
445 panic!("expected SubcomposeLayout");
446 }
447 }
448}