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 {
314 rect: Rect,
315 layer_id: u32,
316 alpha: f32,
317 },
318 EndLayer {
320 layer_id: u32,
321 },
322 CompositeShadow {
327 layer_id: u32,
328 blur_px: f32,
329 offset_px: (f32, f32),
330 color: Color,
331 },
332 Arc {
334 rect: Rect,
335 start_angle: f32,
336 sweep_angle: f32,
337 stroke_width: f32,
338 color: Color,
339 cap: StrokeCap,
340 },
341}
342
343#[derive(Clone, Copy, Debug, PartialEq, Eq)]
344#[non_exhaustive]
345pub enum TextOverflow {
346 Visible,
347 Clip,
348 Ellipsis,
349}
350
351#[derive(Clone, Copy, Debug, PartialEq)]
355pub enum StrokeCap {
356 Butt,
358 Round,
361 Square,
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn subcompose_scope_unbounded_has_infinite_max() {
372 let s = SubcomposeScope::UNBOUNDED;
373 assert!(!s.max_width.is_finite());
374 assert!(!s.max_height.is_finite());
375 assert_eq!(s.min_width, 0.0);
376 assert_eq!(s.min_height, 0.0);
377 }
378
379 #[test]
380 fn subcompose_scope_new_round_trips() {
381 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
382 assert_eq!(s.min_width, 10.0);
383 assert_eq!(s.max_width, 200.0);
384 assert_eq!(s.min_height, 20.0);
385 assert_eq!(s.max_height, 300.0);
386 }
387
388 #[test]
389 fn box_with_constraints_scope_bounded_predicates() {
390 let bounded = BoxWithConstraintsScope {
391 min_width: 0.0,
392 max_width: 360.0,
393 min_height: 0.0,
394 max_height: 640.0,
395 };
396 assert!(bounded.has_bounded_width());
397 assert!(bounded.has_bounded_height());
398
399 let unbounded = BoxWithConstraintsScope {
400 min_width: 0.0,
401 max_width: f32::INFINITY,
402 min_height: 0.0,
403 max_height: f32::INFINITY,
404 };
405 assert!(!unbounded.has_bounded_width());
406 assert!(!unbounded.has_bounded_height());
407 }
408
409 #[test]
410 fn view_kind_subcompose_layout_holds_closure() {
411 let v: View = View {
412 id: 0,
413 kind: ViewKind::SubcomposeLayout {
414 content: std::sync::Arc::new(|scope| {
415 let _ = scope.max_width;
416 vec![(0, View::new(0, ViewKind::Box))]
417 }),
418 },
419 modifier: Modifier::default(),
420 children: vec![],
421 scope_key: None,
422 semantics: None,
423 };
424 match &v.kind {
425 ViewKind::SubcomposeLayout { .. } => {}
426 _ => panic!("expected SubcomposeLayout"),
427 }
428 }
429
430 #[test]
431 fn view_kind_subcompose_layout_supports_multiple_slots() {
432 let v: View = View {
433 id: 0,
434 kind: ViewKind::SubcomposeLayout {
435 content: std::sync::Arc::new(|_scope| {
436 vec![
437 (1, View::new(0, ViewKind::Box)),
438 (2, View::new(0, ViewKind::Box)),
439 (3, View::new(0, ViewKind::Box)),
440 ]
441 }),
442 },
443 modifier: Modifier::default(),
444 children: vec![],
445 scope_key: None,
446 semantics: None,
447 };
448 if let ViewKind::SubcomposeLayout { content } = &v.kind {
449 let slots = content(&SubcomposeScope::UNBOUNDED);
450 assert_eq!(slots.len(), 3);
451 assert_eq!(slots[0].0, 1);
452 assert_eq!(slots[1].0, 2);
453 assert_eq!(slots[2].0, 3);
454 } else {
455 panic!("expected SubcomposeLayout");
456 }
457 }
458}