1use crate::{
2 Brush, Color, FontStyle, FontWeight, Modifier, Rect, TextAlign, TextDecoration, TextSpan,
3 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()>;
73pub type ScrollCallback = Rc<dyn Fn(crate::Vec2) -> crate::Vec2>;
74
75#[derive(Clone)]
76pub struct OverlayEntry {
77 pub id: u64,
78 pub view: Box<View>,
79}
80
81#[derive(Clone)]
82#[non_exhaustive]
83pub enum ViewKind {
84 Box,
85 Row,
86 Column,
87 Stack,
88 ZStack,
89 OverlayHost,
90 ScrollV {
91 on_scroll: Option<ScrollCallback>,
92 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
93 set_content_height: Option<Rc<dyn Fn(f32)>>,
94 get_scroll_offset: Option<Rc<dyn Fn() -> f32>>,
95 set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
96 show_scrollbar: bool,
97 tick_scroll: Option<Rc<dyn Fn()>>,
98 },
99 ScrollXY {
100 on_scroll: Option<ScrollCallback>,
101 set_viewport_width: Option<Rc<dyn Fn(f32)>>,
102 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
103 set_content_width: Option<Rc<dyn Fn(f32)>>,
104 set_content_height: Option<Rc<dyn Fn(f32)>>,
105 get_scroll_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
106 set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
107 show_scrollbar: bool,
108 tick_scroll: Option<Rc<dyn Fn()>>,
109 },
110 Text {
111 text: String,
112 color: Color,
113 font_size: f32,
114 soft_wrap: bool,
115 max_lines: Option<usize>,
116 overflow: TextOverflow,
117 font_family: Option<&'static str>,
118 annotations: Option<Arc<[TextSpan]>>,
119 text_align: TextAlign,
120 font_weight: FontWeight,
121 font_style: FontStyle,
122 text_decoration: TextDecoration,
123 letter_spacing: f32,
124 line_height: f32,
125 },
126
127 Image {
128 handle: ImageHandle,
129 tint: Color, fit: ImageFit,
131 },
132 SubcomposeLayout {
142 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
143 },
144 Expander {
147 expanded: bool,
148 on_toggle: Option<Callback>,
149 },
150 TreeRow {
153 depth: usize,
154 has_children: bool,
155 is_expanded: bool,
156 is_selected: bool,
157 on_toggle: Option<Callback>,
158 on_select: Option<Callback>,
159 },
160}
161
162impl std::fmt::Debug for ViewKind {
163 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
164 match self {
165 Self::Box => f.write_str("Box"),
166 Self::Row => f.write_str("Row"),
167 Self::Column => f.write_str("Column"),
168 Self::Stack => f.write_str("Stack"),
169 Self::ZStack => f.write_str("ZStack"),
170 Self::OverlayHost => f.write_str("OverlayHost"),
171 Self::ScrollV { .. } => f.write_str("ScrollV"),
172 Self::ScrollXY { .. } => f.write_str("ScrollXY"),
173 Self::Image { .. } => f.write_str("Image"),
174 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
175 Self::Text { text, .. } => write!(f, "Text({:?})", text),
176
177 Self::Expander { expanded, .. } => {
178 if *expanded {
179 write!(f, "Expander(expanded)")
180 } else {
181 write!(f, "Expander(collapsed)")
182 }
183 }
184 Self::TreeRow {
185 depth,
186 has_children,
187 is_expanded,
188 is_selected,
189 ..
190 } => {
191 write!(
192 f,
193 "TreeRow(depth={}, children={}, expanded={}, selected={})",
194 depth, has_children, is_expanded, is_selected
195 )
196 }
197 }
198 }
199}
200
201#[derive(Clone, Debug)]
202pub struct View {
203 pub id: ViewId,
204 pub kind: ViewKind,
205 pub modifier: Modifier,
206 pub children: Vec<View>,
207 pub semantics: Option<crate::semantics::Semantics>,
208 pub scope_key: Option<String>,
212}
213
214impl View {
215 pub fn new(id: ViewId, kind: ViewKind) -> Self {
216 View {
217 id,
218 kind,
219 modifier: Modifier::default(),
220 children: vec![],
221 semantics: None,
222 scope_key: None,
223 }
224 }
225 pub fn modifier(mut self, m: Modifier) -> Self {
226 self.modifier = m;
227 self
228 }
229 pub fn disabled(mut self) -> Self {
231 self.modifier.disabled = true;
232 self
233 }
234 pub fn with_children(mut self, kids: Vec<View>) -> Self {
235 self.children = kids;
236 self
237 }
238 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
239 self.semantics = Some(s);
240 self
241 }
242}
243
244#[derive(Clone, Debug, Default)]
246pub struct Scene {
247 pub clear_color: Color,
248 pub nodes: Vec<SceneNode>,
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 },
278 Ellipse {
279 rect: Rect,
280 brush: Brush,
281 },
282 EllipseBorder {
283 rect: Rect,
284 color: Color,
285 width: f32, },
287 PushClip {
288 rect: Rect,
289 radius: [f32; 4],
290 },
291 PopClip,
292 PushTransform {
293 transform: Transform,
294 },
295 PopTransform,
296 Image {
297 rect: Rect,
298 handle: ImageHandle,
299 tint: Color,
300 fit: ImageFit,
301 },
302 Shadow {
305 rect: Rect,
306 radius: [f32; 4],
307 elevation: f32,
308 color: Color,
309 },
310 BeginLayer {
318 rect: Rect,
319 layer_id: u32,
320 alpha: f32,
321 blur_radius_x: f32,
322 blur_radius_y: f32,
323 rectangle_edge: bool,
324 },
325 EndLayer {
327 layer_id: u32,
328 },
329 CompositeShadow {
334 layer_id: u32,
335 blur_px: f32,
336 offset_px: (f32, f32),
337 color: Color,
338 },
339 Arc {
341 rect: Rect,
342 start_angle: f32,
343 sweep_angle: f32,
344 stroke_width: f32,
345 color: Color,
346 cap: StrokeCap,
347 },
348}
349
350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
351#[non_exhaustive]
352pub enum TextOverflow {
353 Visible,
354 Clip,
355 Ellipsis,
356}
357
358#[derive(Clone, Copy, Debug, PartialEq)]
362pub enum StrokeCap {
363 Butt,
365 Round,
368 Square,
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn subcompose_scope_unbounded_has_infinite_max() {
379 let s = SubcomposeScope::UNBOUNDED;
380 assert!(!s.max_width.is_finite());
381 assert!(!s.max_height.is_finite());
382 assert_eq!(s.min_width, 0.0);
383 assert_eq!(s.min_height, 0.0);
384 }
385
386 #[test]
387 fn subcompose_scope_new_round_trips() {
388 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
389 assert_eq!(s.min_width, 10.0);
390 assert_eq!(s.max_width, 200.0);
391 assert_eq!(s.min_height, 20.0);
392 assert_eq!(s.max_height, 300.0);
393 }
394
395 #[test]
396 fn box_with_constraints_scope_bounded_predicates() {
397 let bounded = BoxWithConstraintsScope {
398 min_width: 0.0,
399 max_width: 360.0,
400 min_height: 0.0,
401 max_height: 640.0,
402 };
403 assert!(bounded.has_bounded_width());
404 assert!(bounded.has_bounded_height());
405
406 let unbounded = BoxWithConstraintsScope {
407 min_width: 0.0,
408 max_width: f32::INFINITY,
409 min_height: 0.0,
410 max_height: f32::INFINITY,
411 };
412 assert!(!unbounded.has_bounded_width());
413 assert!(!unbounded.has_bounded_height());
414 }
415
416 #[test]
417 fn view_kind_subcompose_layout_holds_closure() {
418 let v: View = View {
419 id: 0,
420 kind: ViewKind::SubcomposeLayout {
421 content: std::sync::Arc::new(|scope| {
422 let _ = scope.max_width;
423 vec![(0, View::new(0, ViewKind::Box))]
424 }),
425 },
426 modifier: Modifier::default(),
427 children: vec![],
428 scope_key: None,
429 semantics: None,
430 };
431 match &v.kind {
432 ViewKind::SubcomposeLayout { .. } => {}
433 _ => panic!("expected SubcomposeLayout"),
434 }
435 }
436
437 #[test]
438 fn view_kind_subcompose_layout_supports_multiple_slots() {
439 let v: View = View {
440 id: 0,
441 kind: ViewKind::SubcomposeLayout {
442 content: std::sync::Arc::new(|_scope| {
443 vec![
444 (1, View::new(0, ViewKind::Box)),
445 (2, View::new(0, ViewKind::Box)),
446 (3, View::new(0, ViewKind::Box)),
447 ]
448 }),
449 },
450 modifier: Modifier::default(),
451 children: vec![],
452 scope_key: None,
453 semantics: None,
454 };
455 if let ViewKind::SubcomposeLayout { content } = &v.kind {
456 let slots = content(&SubcomposeScope::UNBOUNDED);
457 assert_eq!(slots.len(), 3);
458 assert_eq!(slots[0].0, 1);
459 assert_eq!(slots[1].0, 2);
460 assert_eq!(slots[2].0, 3);
461 } else {
462 panic!("expected SubcomposeLayout");
463 }
464 }
465}