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 },
105
106 Image {
107 handle: ImageHandle,
108 tint: Color, fit: ImageFit,
110 },
111 SubcomposeLayout {
121 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
122 },
123 Expander {
126 expanded: bool,
127 on_toggle: Option<Callback>,
128 },
129 TreeRow {
132 depth: usize,
133 has_children: bool,
134 is_expanded: bool,
135 is_selected: bool,
136 on_toggle: Option<Callback>,
137 on_select: Option<Callback>,
138 },
139}
140
141impl std::fmt::Debug for ViewKind {
142 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::Box => f.write_str("Box"),
145 Self::Row => f.write_str("Row"),
146 Self::Column => f.write_str("Column"),
147 Self::Stack => f.write_str("Stack"),
148 Self::ZStack => f.write_str("ZStack"),
149 Self::OverlayHost => f.write_str("OverlayHost"),
150
151 Self::Image { .. } => f.write_str("Image"),
152 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
153 Self::Text { text, .. } => write!(f, "Text({:?})", text),
154
155 Self::Expander { expanded, .. } => {
156 if *expanded {
157 write!(f, "Expander(expanded)")
158 } else {
159 write!(f, "Expander(collapsed)")
160 }
161 }
162 Self::TreeRow {
163 depth,
164 has_children,
165 is_expanded,
166 is_selected,
167 ..
168 } => {
169 write!(
170 f,
171 "TreeRow(depth={}, children={}, expanded={}, selected={})",
172 depth, has_children, is_expanded, is_selected
173 )
174 }
175 }
176 }
177}
178
179#[derive(Clone, Debug)]
180pub struct View {
181 pub id: ViewId,
182 pub kind: ViewKind,
183 pub modifier: Modifier,
184 pub children: Vec<View>,
185 pub semantics: Option<crate::semantics::Semantics>,
186 pub scope_key: Option<String>,
190}
191
192impl View {
193 pub fn new(id: ViewId, kind: ViewKind) -> Self {
194 View {
195 id,
196 kind,
197 modifier: Modifier::default(),
198 children: vec![],
199 semantics: None,
200 scope_key: None,
201 }
202 }
203 pub fn modifier(mut self, m: Modifier) -> Self {
204 self.modifier = m;
205 self
206 }
207 pub fn disabled(mut self) -> Self {
209 self.modifier.disabled = true;
210 self
211 }
212 pub fn with_children(mut self, kids: Vec<View>) -> Self {
213 self.children = kids;
214 self
215 }
216 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
217 self.semantics = Some(s);
218 self
219 }
220}
221
222#[derive(Clone, Debug, Default)]
224pub struct Scene {
225 pub clear_color: Color,
226 pub nodes: Vec<SceneNode>,
227}
228
229#[derive(Clone, Debug)]
230#[non_exhaustive]
231pub enum SceneNode {
232 Rect {
233 rect: Rect,
234 brush: Brush,
235 radius: [f32; 4],
236 },
237 Border {
238 rect: Rect,
239 color: Color,
240 width: f32,
241 radius: [f32; 4],
242 },
243 Text {
244 rect: Rect,
245 text: Arc<str>,
246 color: Color,
247 size: f32,
248 font_family: Option<&'static str>,
249 text_align: TextAlign,
250 font_weight: FontWeight,
251 font_style: FontStyle,
252 text_decoration: TextDecoration,
253 letter_spacing: f32,
254 line_height: f32,
255 },
256 Ellipse {
257 rect: Rect,
258 brush: Brush,
259 },
260 EllipseBorder {
261 rect: Rect,
262 color: Color,
263 width: f32, },
265 PushClip {
266 rect: Rect,
267 radius: [f32; 4],
268 op: ClipOp,
269 },
270 PopClip,
271 PushTransform {
272 transform: Transform,
273 },
274 PopTransform,
275 Image {
276 rect: Rect,
277 handle: ImageHandle,
278 tint: Color,
279 fit: ImageFit,
280 },
281 Shadow {
284 rect: Rect,
285 radius: [f32; 4],
286 elevation: f32,
287 color: Color,
288 },
289 BeginLayer {
297 rect: Rect,
298 layer_id: u32,
299 alpha: f32,
300 blur_radius_x: f32,
301 blur_radius_y: f32,
302 rectangle_edge: bool,
303 },
304 EndLayer {
306 layer_id: u32,
307 },
308 CompositeShadow {
313 layer_id: u32,
314 blur_px: f32,
315 offset_px: (f32, f32),
316 color: Color,
317 },
318 Arc {
320 rect: Rect,
321 start_angle: f32,
322 sweep_angle: f32,
323 stroke_width: f32,
324 color: Color,
325 cap: StrokeCap,
326 },
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
330#[non_exhaustive]
331pub enum TextOverflow {
332 Visible,
333 Clip,
334 Ellipsis,
335}
336
337#[derive(Clone, Copy, Debug, PartialEq)]
341pub enum StrokeCap {
342 Butt,
344 Round,
347 Square,
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn subcompose_scope_unbounded_has_infinite_max() {
358 let s = SubcomposeScope::UNBOUNDED;
359 assert!(!s.max_width.is_finite());
360 assert!(!s.max_height.is_finite());
361 assert_eq!(s.min_width, 0.0);
362 assert_eq!(s.min_height, 0.0);
363 }
364
365 #[test]
366 fn subcompose_scope_new_round_trips() {
367 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
368 assert_eq!(s.min_width, 10.0);
369 assert_eq!(s.max_width, 200.0);
370 assert_eq!(s.min_height, 20.0);
371 assert_eq!(s.max_height, 300.0);
372 }
373
374 #[test]
375 fn box_with_constraints_scope_bounded_predicates() {
376 let bounded = BoxWithConstraintsScope {
377 min_width: 0.0,
378 max_width: 360.0,
379 min_height: 0.0,
380 max_height: 640.0,
381 };
382 assert!(bounded.has_bounded_width());
383 assert!(bounded.has_bounded_height());
384
385 let unbounded = BoxWithConstraintsScope {
386 min_width: 0.0,
387 max_width: f32::INFINITY,
388 min_height: 0.0,
389 max_height: f32::INFINITY,
390 };
391 assert!(!unbounded.has_bounded_width());
392 assert!(!unbounded.has_bounded_height());
393 }
394
395 #[test]
396 fn view_kind_subcompose_layout_holds_closure() {
397 let v: View = View {
398 id: 0,
399 kind: ViewKind::SubcomposeLayout {
400 content: std::sync::Arc::new(|scope| {
401 let _ = scope.max_width;
402 vec![(0, View::new(0, ViewKind::Box))]
403 }),
404 },
405 modifier: Modifier::default(),
406 children: vec![],
407 scope_key: None,
408 semantics: None,
409 };
410 match &v.kind {
411 ViewKind::SubcomposeLayout { .. } => {}
412 _ => panic!("expected SubcomposeLayout"),
413 }
414 }
415
416 #[test]
417 fn view_kind_subcompose_layout_supports_multiple_slots() {
418 let v: View = View {
419 id: 0,
420 kind: ViewKind::SubcomposeLayout {
421 content: std::sync::Arc::new(|_scope| {
422 vec![
423 (1, View::new(0, ViewKind::Box)),
424 (2, View::new(0, ViewKind::Box)),
425 (3, View::new(0, ViewKind::Box)),
426 ]
427 }),
428 },
429 modifier: Modifier::default(),
430 children: vec![],
431 scope_key: None,
432 semantics: None,
433 };
434 if let ViewKind::SubcomposeLayout { content } = &v.kind {
435 let slots = content(&SubcomposeScope::UNBOUNDED);
436 assert_eq!(slots.len(), 3);
437 assert_eq!(slots[0].0, 1);
438 assert_eq!(slots[1].0, 2);
439 assert_eq!(slots[2].0, 3);
440 } else {
441 panic!("expected SubcomposeLayout");
442 }
443 }
444}