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}
313
314#[derive(Clone, Copy, Debug, PartialEq, Eq)]
315#[non_exhaustive]
316pub enum TextOverflow {
317 Visible,
318 Clip,
319 Ellipsis,
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn subcompose_scope_unbounded_has_infinite_max() {
328 let s = SubcomposeScope::UNBOUNDED;
329 assert!(!s.max_width.is_finite());
330 assert!(!s.max_height.is_finite());
331 assert_eq!(s.min_width, 0.0);
332 assert_eq!(s.min_height, 0.0);
333 }
334
335 #[test]
336 fn subcompose_scope_new_round_trips() {
337 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
338 assert_eq!(s.min_width, 10.0);
339 assert_eq!(s.max_width, 200.0);
340 assert_eq!(s.min_height, 20.0);
341 assert_eq!(s.max_height, 300.0);
342 }
343
344 #[test]
345 fn box_with_constraints_scope_bounded_predicates() {
346 let bounded = BoxWithConstraintsScope {
347 min_width: 0.0,
348 max_width: 360.0,
349 min_height: 0.0,
350 max_height: 640.0,
351 };
352 assert!(bounded.has_bounded_width());
353 assert!(bounded.has_bounded_height());
354
355 let unbounded = BoxWithConstraintsScope {
356 min_width: 0.0,
357 max_width: f32::INFINITY,
358 min_height: 0.0,
359 max_height: f32::INFINITY,
360 };
361 assert!(!unbounded.has_bounded_width());
362 assert!(!unbounded.has_bounded_height());
363 }
364
365 #[test]
366 fn view_kind_subcompose_layout_holds_closure() {
367 let v: View = View {
368 id: 0,
369 kind: ViewKind::SubcomposeLayout {
370 content: std::sync::Arc::new(|scope| {
371 let _ = scope.max_width;
372 vec![(0, View::new(0, ViewKind::Box))]
373 }),
374 },
375 modifier: Modifier::default(),
376 children: vec![],
377 semantics: None,
378 };
379 match &v.kind {
380 ViewKind::SubcomposeLayout { .. } => {}
381 _ => panic!("expected SubcomposeLayout"),
382 }
383 }
384
385 #[test]
386 fn view_kind_subcompose_layout_supports_multiple_slots() {
387 let v: View = View {
388 id: 0,
389 kind: ViewKind::SubcomposeLayout {
390 content: std::sync::Arc::new(|_scope| {
391 vec![
392 (1, View::new(0, ViewKind::Box)),
393 (2, View::new(0, ViewKind::Box)),
394 (3, View::new(0, ViewKind::Box)),
395 ]
396 }),
397 },
398 modifier: Modifier::default(),
399 children: vec![],
400 semantics: None,
401 };
402 if let ViewKind::SubcomposeLayout { content } = &v.kind {
403 let slots = content(&SubcomposeScope::UNBOUNDED);
404 assert_eq!(slots.len(), 3);
405 assert_eq!(slots[0].0, 1);
406 assert_eq!(slots[1].0, 2);
407 assert_eq!(slots[2].0, 3);
408 } else {
409 panic!("expected SubcomposeLayout");
410 }
411 }
412}