1use crate::{Brush, Color, Modifier, Rect, TextSpan, Transform};
2use std::{fmt::Formatter, rc::Rc, sync::Arc};
3
4#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct SubcomposeScope {
8 pub min_width: f32,
9 pub max_width: f32,
10 pub min_height: f32,
11 pub max_height: f32,
12}
13
14impl SubcomposeScope {
15 pub const UNBOUNDED: Self = Self {
18 min_width: 0.0,
19 max_width: f32::INFINITY,
20 min_height: 0.0,
21 max_height: f32::INFINITY,
22 };
23
24 pub fn new(min_width: f32, max_width: f32, min_height: f32, max_height: f32) -> Self {
26 Self {
27 min_width,
28 max_width,
29 min_height,
30 max_height,
31 }
32 }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct BoxWithConstraintsScope {
39 pub min_width: f32,
40 pub max_width: f32,
41 pub min_height: f32,
42 pub max_height: f32,
43}
44
45impl BoxWithConstraintsScope {
46 pub fn has_bounded_width(&self) -> bool {
48 self.max_width.is_finite()
49 }
50
51 pub fn has_bounded_height(&self) -> bool {
53 self.max_height.is_finite()
54 }
55}
56
57pub type ViewId = u64;
58
59pub type ImageHandle = u64;
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum ImageFit {
63 Contain,
64 Cover,
65 FitWidth,
66 FitHeight,
67}
68
69pub type Callback = Rc<dyn Fn()>;
70pub type ScrollCallback = Rc<dyn Fn(crate::Vec2) -> crate::Vec2>;
71
72#[derive(Clone)]
73pub struct OverlayEntry {
74 pub id: u64,
75 pub view: Box<View>,
76}
77
78#[derive(Clone)]
79#[non_exhaustive]
80pub enum ViewKind {
81 Box,
82 Row,
83 Column,
84 Stack,
85 ZStack,
86 OverlayHost,
87 ScrollV {
88 on_scroll: Option<ScrollCallback>,
89 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
90 set_content_height: Option<Rc<dyn Fn(f32)>>,
91 get_scroll_offset: Option<Rc<dyn Fn() -> f32>>,
92 set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
93 show_scrollbar: bool,
94 tick_scroll: Option<Rc<dyn Fn()>>,
95 },
96 ScrollXY {
97 on_scroll: Option<ScrollCallback>,
98 set_viewport_width: Option<Rc<dyn Fn(f32)>>,
99 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
100 set_content_width: Option<Rc<dyn Fn(f32)>>,
101 set_content_height: Option<Rc<dyn Fn(f32)>>,
102 get_scroll_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
103 set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
104 show_scrollbar: bool,
105 tick_scroll: Option<Rc<dyn Fn()>>,
106 },
107 Text {
108 text: String,
109 color: Color,
110 font_size: f32,
111 soft_wrap: bool,
112 max_lines: Option<usize>,
113 overflow: TextOverflow,
114 font_family: Option<&'static str>,
115 annotations: Option<Arc<[TextSpan]>>,
116 },
117
118 Image {
119 handle: ImageHandle,
120 tint: Color, fit: ImageFit,
122 },
123 SubcomposeLayout {
133 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
134 },
135 Expander {
138 expanded: bool,
139 on_toggle: Option<Callback>,
140 },
141 TreeRow {
144 depth: usize,
145 has_children: bool,
146 is_expanded: bool,
147 is_selected: bool,
148 on_toggle: Option<Callback>,
149 on_select: Option<Callback>,
150 },
151}
152
153impl std::fmt::Debug for ViewKind {
154 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
155 match self {
156 Self::Box => f.write_str("Box"),
157 Self::Row => f.write_str("Row"),
158 Self::Column => f.write_str("Column"),
159 Self::Stack => f.write_str("Stack"),
160 Self::ZStack => f.write_str("ZStack"),
161 Self::OverlayHost => f.write_str("OverlayHost"),
162 Self::ScrollV { .. } => f.write_str("ScrollV"),
163 Self::ScrollXY { .. } => f.write_str("ScrollXY"),
164 Self::Image { .. } => f.write_str("Image"),
165 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
166 Self::Text { text, .. } => write!(f, "Text({:?})", text),
167
168 Self::Expander { expanded, .. } => {
169 if *expanded {
170 write!(f, "Expander(expanded)")
171 } else {
172 write!(f, "Expander(collapsed)")
173 }
174 }
175 Self::TreeRow {
176 depth,
177 has_children,
178 is_expanded,
179 is_selected,
180 ..
181 } => {
182 write!(
183 f,
184 "TreeRow(depth={}, children={}, expanded={}, selected={})",
185 depth, has_children, is_expanded, is_selected
186 )
187 }
188 }
189 }
190}
191
192#[derive(Clone, Debug)]
193pub struct View {
194 pub id: ViewId,
195 pub kind: ViewKind,
196 pub modifier: Modifier,
197 pub children: Vec<View>,
198 pub semantics: Option<crate::semantics::Semantics>,
199}
200
201impl View {
202 pub fn new(id: ViewId, kind: ViewKind) -> Self {
203 View {
204 id,
205 kind,
206 modifier: Modifier::default(),
207 children: vec![],
208 semantics: None,
209 }
210 }
211 pub fn modifier(mut self, m: Modifier) -> Self {
212 self.modifier = m;
213 self
214 }
215 pub fn disabled(mut self) -> Self {
217 self.modifier.disabled = true;
218 self
219 }
220 pub fn with_children(mut self, kids: Vec<View>) -> Self {
221 self.children = kids;
222 self
223 }
224 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
225 self.semantics = Some(s);
226 self
227 }
228}
229
230#[derive(Clone, Debug, Default)]
232pub struct Scene {
233 pub clear_color: Color,
234 pub nodes: Vec<SceneNode>,
235}
236
237#[derive(Clone, Debug)]
238#[non_exhaustive]
239pub enum SceneNode {
240 Rect {
241 rect: Rect,
242 brush: Brush,
243 radius: f32,
244 },
245 Border {
246 rect: Rect,
247 color: Color,
248 width: f32,
249 radius: f32,
250 },
251 Text {
252 rect: Rect,
253 text: Arc<str>,
254 color: Color,
255 size: f32,
256 font_family: Option<&'static str>,
257 },
258 Ellipse {
259 rect: Rect,
260 brush: Brush,
261 },
262 EllipseBorder {
263 rect: Rect,
264 color: Color,
265 width: f32, },
267 PushClip {
268 rect: Rect,
269 radius: f32,
270 },
271 PopClip,
272 PushTransform {
273 transform: Transform,
274 },
275 PopTransform,
276 Image {
277 rect: Rect,
278 handle: ImageHandle,
279 tint: Color,
280 fit: ImageFit,
281 },
282 Shadow {
285 rect: Rect,
286 radius: f32,
287 elevation: f32,
288 color: Color,
289 },
290 BeginLayer {
294 rect: Rect,
295 layer_id: u32,
296 alpha: f32,
297 },
298 EndLayer {
300 layer_id: u32,
301 },
302 CompositeShadow {
307 layer_id: u32,
308 blur_px: f32,
309 offset_px: (f32, f32),
310 color: Color,
311 },
312 Arc {
314 rect: Rect,
315 start_angle: f32,
316 sweep_angle: f32,
317 stroke_width: f32,
318 color: Color,
319 cap: StrokeCap,
320 },
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
324#[non_exhaustive]
325pub enum TextOverflow {
326 Visible,
327 Clip,
328 Ellipsis,
329}
330
331#[derive(Clone, Copy, Debug, PartialEq)]
335pub enum StrokeCap {
336 Butt,
338 Round,
341 Square,
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn subcompose_scope_unbounded_has_infinite_max() {
352 let s = SubcomposeScope::UNBOUNDED;
353 assert!(!s.max_width.is_finite());
354 assert!(!s.max_height.is_finite());
355 assert_eq!(s.min_width, 0.0);
356 assert_eq!(s.min_height, 0.0);
357 }
358
359 #[test]
360 fn subcompose_scope_new_round_trips() {
361 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
362 assert_eq!(s.min_width, 10.0);
363 assert_eq!(s.max_width, 200.0);
364 assert_eq!(s.min_height, 20.0);
365 assert_eq!(s.max_height, 300.0);
366 }
367
368 #[test]
369 fn box_with_constraints_scope_bounded_predicates() {
370 let bounded = BoxWithConstraintsScope {
371 min_width: 0.0,
372 max_width: 360.0,
373 min_height: 0.0,
374 max_height: 640.0,
375 };
376 assert!(bounded.has_bounded_width());
377 assert!(bounded.has_bounded_height());
378
379 let unbounded = BoxWithConstraintsScope {
380 min_width: 0.0,
381 max_width: f32::INFINITY,
382 min_height: 0.0,
383 max_height: f32::INFINITY,
384 };
385 assert!(!unbounded.has_bounded_width());
386 assert!(!unbounded.has_bounded_height());
387 }
388
389 #[test]
390 fn view_kind_subcompose_layout_holds_closure() {
391 let v: View = View {
392 id: 0,
393 kind: ViewKind::SubcomposeLayout {
394 content: std::sync::Arc::new(|scope| {
395 let _ = scope.max_width;
396 vec![(0, View::new(0, ViewKind::Box))]
397 }),
398 },
399 modifier: Modifier::default(),
400 children: vec![],
401 semantics: None,
402 };
403 match &v.kind {
404 ViewKind::SubcomposeLayout { .. } => {}
405 _ => panic!("expected SubcomposeLayout"),
406 }
407 }
408
409 #[test]
410 fn view_kind_subcompose_layout_supports_multiple_slots() {
411 let v: View = View {
412 id: 0,
413 kind: ViewKind::SubcomposeLayout {
414 content: std::sync::Arc::new(|_scope| {
415 vec![
416 (1, View::new(0, ViewKind::Box)),
417 (2, View::new(0, ViewKind::Box)),
418 (3, View::new(0, ViewKind::Box)),
419 ]
420 }),
421 },
422 modifier: Modifier::default(),
423 children: vec![],
424 semantics: None,
425 };
426 if let ViewKind::SubcomposeLayout { content } = &v.kind {
427 let slots = content(&SubcomposeScope::UNBOUNDED);
428 assert_eq!(slots.len(), 3);
429 assert_eq!(slots[0].0, 1);
430 assert_eq!(slots[1].0, 2);
431 assert_eq!(slots[2].0, 3);
432 } else {
433 panic!("expected SubcomposeLayout");
434 }
435 }
436}