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 Surface,
82 Box,
83 Row,
84 Column,
85 Stack,
86 ZStack,
87 OverlayHost,
88 ScrollV {
89 on_scroll: Option<ScrollCallback>,
90 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
91 set_content_height: Option<Rc<dyn Fn(f32)>>,
92 get_scroll_offset: Option<Rc<dyn Fn() -> f32>>,
93 set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
94 show_scrollbar: bool,
95 tick_scroll: Option<Rc<dyn Fn()>>,
96 },
97 ScrollXY {
98 on_scroll: Option<ScrollCallback>,
99 set_viewport_width: Option<Rc<dyn Fn(f32)>>,
100 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
101 set_content_width: Option<Rc<dyn Fn(f32)>>,
102 set_content_height: Option<Rc<dyn Fn(f32)>>,
103 get_scroll_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
104 set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
105 show_scrollbar: bool,
106 tick_scroll: Option<Rc<dyn Fn()>>,
107 },
108 Text {
109 text: String,
110 color: Color,
111 font_size: f32,
112 soft_wrap: bool,
113 max_lines: Option<usize>,
114 overflow: TextOverflow,
115 font_family: Option<&'static str>,
116 annotations: Option<Arc<[TextSpan]>>,
117 },
118
119 Image {
120 handle: ImageHandle,
121 tint: Color, fit: ImageFit,
123 },
124 Ellipse {
125 rect: Rect,
126 color: Color,
127 },
128 EllipseBorder {
129 rect: Rect,
130 color: Color,
131 width: f32, },
133 SubcomposeLayout {
143 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
144 },
145 Expander {
148 expanded: bool,
149 on_toggle: Option<Callback>,
150 },
151 TreeRow {
154 depth: usize,
155 has_children: bool,
156 is_expanded: bool,
157 is_selected: bool,
158 on_toggle: Option<Callback>,
159 on_select: Option<Callback>,
160 },
161}
162
163impl std::fmt::Debug for ViewKind {
164 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165 match self {
166 Self::Surface => f.write_str("Surface"),
167 Self::Box => f.write_str("Box"),
168 Self::Row => f.write_str("Row"),
169 Self::Column => f.write_str("Column"),
170 Self::Stack => f.write_str("Stack"),
171 Self::ZStack => f.write_str("ZStack"),
172 Self::OverlayHost => f.write_str("OverlayHost"),
173 Self::ScrollV { .. } => f.write_str("ScrollV"),
174 Self::ScrollXY { .. } => f.write_str("ScrollXY"),
175 Self::Image { .. } => f.write_str("Image"),
176 Self::Ellipse { .. } => f.write_str("Ellipse"),
177 Self::EllipseBorder { .. } => f.write_str("EllipseBorder"),
178 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
179 Self::Text { text, .. } => write!(f, "Text({:?})", text),
180
181 Self::Expander { expanded, .. } => {
182 if *expanded {
183 write!(f, "Expander(expanded)")
184 } else {
185 write!(f, "Expander(collapsed)")
186 }
187 }
188 Self::TreeRow {
189 depth,
190 has_children,
191 is_expanded,
192 is_selected,
193 ..
194 } => {
195 write!(
196 f,
197 "TreeRow(depth={}, children={}, expanded={}, selected={})",
198 depth, has_children, is_expanded, is_selected
199 )
200 }
201 }
202 }
203}
204
205#[derive(Clone, Debug)]
206pub struct View {
207 pub id: ViewId,
208 pub kind: ViewKind,
209 pub modifier: Modifier,
210 pub children: Vec<View>,
211 pub semantics: Option<crate::semantics::Semantics>,
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 }
223 }
224 pub fn modifier(mut self, m: Modifier) -> Self {
225 self.modifier = m;
226 self
227 }
228 pub fn disabled(mut self) -> Self {
230 self.modifier.disabled = true;
231 self
232 }
233 pub fn with_children(mut self, kids: Vec<View>) -> Self {
234 self.children = kids;
235 self
236 }
237 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
238 self.semantics = Some(s);
239 self
240 }
241}
242
243#[derive(Clone, Debug, Default)]
245pub struct Scene {
246 pub clear_color: Color,
247 pub nodes: Vec<SceneNode>,
248}
249
250#[derive(Clone, Debug)]
251#[non_exhaustive]
252pub enum SceneNode {
253 Rect {
254 rect: Rect,
255 brush: Brush,
256 radius: f32,
257 },
258 Border {
259 rect: Rect,
260 color: Color,
261 width: f32,
262 radius: f32,
263 },
264 Text {
265 rect: Rect,
266 text: Arc<str>,
267 color: Color,
268 size: f32,
269 font_family: Option<&'static str>,
270 },
271 Ellipse {
272 rect: Rect,
273 brush: Brush,
274 },
275 EllipseBorder {
276 rect: Rect,
277 color: Color,
278 width: f32, },
280 PushClip {
281 rect: Rect,
282 radius: f32,
283 },
284 PopClip,
285 PushTransform {
286 transform: Transform,
287 },
288 PopTransform,
289 Image {
290 rect: Rect,
291 handle: ImageHandle,
292 tint: Color,
293 fit: ImageFit,
294 },
295 Shadow {
298 rect: Rect,
299 radius: f32,
300 elevation: f32,
301 color: Color,
302 },
303 BeginLayer {
307 rect: Rect,
308 layer_id: u32,
309 alpha: f32,
310 },
311 EndLayer {
313 layer_id: u32,
314 },
315 CompositeShadow {
320 layer_id: u32,
321 blur_px: f32,
322 offset_px: (f32, f32),
323 color: Color,
324 },
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328#[non_exhaustive]
329pub enum TextOverflow {
330 Visible,
331 Clip,
332 Ellipsis,
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn subcompose_scope_unbounded_has_infinite_max() {
341 let s = SubcomposeScope::UNBOUNDED;
342 assert!(!s.max_width.is_finite());
343 assert!(!s.max_height.is_finite());
344 assert_eq!(s.min_width, 0.0);
345 assert_eq!(s.min_height, 0.0);
346 }
347
348 #[test]
349 fn subcompose_scope_new_round_trips() {
350 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
351 assert_eq!(s.min_width, 10.0);
352 assert_eq!(s.max_width, 200.0);
353 assert_eq!(s.min_height, 20.0);
354 assert_eq!(s.max_height, 300.0);
355 }
356
357 #[test]
358 fn box_with_constraints_scope_bounded_predicates() {
359 let bounded = BoxWithConstraintsScope {
360 min_width: 0.0,
361 max_width: 360.0,
362 min_height: 0.0,
363 max_height: 640.0,
364 };
365 assert!(bounded.has_bounded_width());
366 assert!(bounded.has_bounded_height());
367
368 let unbounded = BoxWithConstraintsScope {
369 min_width: 0.0,
370 max_width: f32::INFINITY,
371 min_height: 0.0,
372 max_height: f32::INFINITY,
373 };
374 assert!(!unbounded.has_bounded_width());
375 assert!(!unbounded.has_bounded_height());
376 }
377
378 #[test]
379 fn view_kind_subcompose_layout_holds_closure() {
380 let v: View = View {
381 id: 0,
382 kind: ViewKind::SubcomposeLayout {
383 content: std::sync::Arc::new(|scope| {
384 let _ = scope.max_width;
385 vec![(0, View::new(0, ViewKind::Box))]
386 }),
387 },
388 modifier: Modifier::default(),
389 children: vec![],
390 semantics: None,
391 };
392 match &v.kind {
393 ViewKind::SubcomposeLayout { .. } => {}
394 _ => panic!("expected SubcomposeLayout"),
395 }
396 }
397
398 #[test]
399 fn view_kind_subcompose_layout_supports_multiple_slots() {
400 let v: View = View {
401 id: 0,
402 kind: ViewKind::SubcomposeLayout {
403 content: std::sync::Arc::new(|_scope| {
404 vec![
405 (1, View::new(0, ViewKind::Box)),
406 (2, View::new(0, ViewKind::Box)),
407 (3, View::new(0, ViewKind::Box)),
408 ]
409 }),
410 },
411 modifier: Modifier::default(),
412 children: vec![],
413 semantics: None,
414 };
415 if let ViewKind::SubcomposeLayout { content } = &v.kind {
416 let slots = content(&SubcomposeScope::UNBOUNDED);
417 assert_eq!(slots.len(), 3);
418 assert_eq!(slots[0].0, 1);
419 assert_eq!(slots[1].0, 2);
420 assert_eq!(slots[2].0, 3);
421 } else {
422 panic!("expected SubcomposeLayout");
423 }
424 }
425}