1use crate::{Brush, Color, Modifier, Rect, TextSpan, Transform};
2use std::{cell::Cell, 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)]
61pub enum ImageFit {
62 Contain,
63 Cover,
64 FitWidth,
65 FitHeight,
66}
67
68pub type Callback = Rc<dyn Fn()>;
69pub type ScrollCallback = Rc<dyn Fn(crate::Vec2) -> crate::Vec2>;
70
71#[derive(Clone)]
72pub struct OverlayEntry {
73 pub id: u64,
74 pub view: Box<View>,
75}
76
77#[derive(Clone)]
78pub enum ViewKind {
79 Surface,
80 Box,
81 Row,
82 Column,
83 Stack,
84 ZStack,
85 OverlayHost,
86 ScrollV {
87 on_scroll: Option<ScrollCallback>,
88 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
89 set_content_height: Option<Rc<dyn Fn(f32)>>,
90 get_scroll_offset: Option<Rc<dyn Fn() -> f32>>,
91 set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
92 show_scrollbar: bool,
93 },
94 ScrollXY {
95 on_scroll: Option<ScrollCallback>,
96 set_viewport_width: Option<Rc<dyn Fn(f32)>>,
97 set_viewport_height: Option<Rc<dyn Fn(f32)>>,
98 set_content_width: Option<Rc<dyn Fn(f32)>>,
99 set_content_height: Option<Rc<dyn Fn(f32)>>,
100 get_scroll_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
101 set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
102 show_scrollbar: bool,
103 },
104 Text {
105 text: String,
106 color: Color,
107 font_size: f32,
108 soft_wrap: bool,
109 max_lines: Option<usize>,
110 overflow: TextOverflow,
111 font_family: Option<&'static str>,
112 annotations: Option<Arc<[TextSpan]>>,
113 },
114 Button {
115 on_click: Option<Callback>,
116 },
117 TextField {
118 state_key: ViewId,
119 hint: String,
120 multiline: bool,
121 on_change: Option<Rc<dyn Fn(String)>>,
122 on_submit: Option<Rc<dyn Fn(String)>>,
123 focus_tracker: Option<Rc<Cell<bool>>>,
126 value: String,
128 },
129 Slider {
130 value: f32,
131 min: f32,
132 max: f32,
133 step: Option<f32>,
134 on_change: Option<CallbackF32>,
135 },
136 RangeSlider {
137 start: f32,
138 end: f32,
139 min: f32,
140 max: f32,
141 step: Option<f32>,
142 on_change: Option<CallbackRange>,
143 },
144 ProgressBar {
145 value: f32,
146 min: f32,
147 max: f32,
148 circular: bool,
149 },
150 Image {
151 handle: ImageHandle,
152 tint: Color, fit: ImageFit,
154 },
155 Ellipse {
156 rect: Rect,
157 color: Color,
158 },
159 EllipseBorder {
160 rect: Rect,
161 color: Color,
162 width: f32, },
164 SubcomposeLayout {
174 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
175 },
176}
177
178impl std::fmt::Debug for ViewKind {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
181 Self::Surface => f.write_str("Surface"),
182 Self::Box => f.write_str("Box"),
183 Self::Row => f.write_str("Row"),
184 Self::Column => f.write_str("Column"),
185 Self::Stack => f.write_str("Stack"),
186 Self::ZStack => f.write_str("ZStack"),
187 Self::OverlayHost => f.write_str("OverlayHost"),
188 Self::ScrollV { .. } => f.write_str("ScrollV"),
189 Self::ScrollXY { .. } => f.write_str("ScrollXY"),
190 Self::Button { .. } => f.write_str("Button"),
191 Self::Image { .. } => f.write_str("Image"),
192 Self::Ellipse { .. } => f.write_str("Ellipse"),
193 Self::EllipseBorder { .. } => f.write_str("EllipseBorder"),
194 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
195 Self::Text { text, .. } => write!(f, "Text({:?})", text),
196 Self::TextField { hint, .. } => write!(f, "TextField({:?})", hint),
197 Self::Slider { value, .. } => write!(f, "Slider({})", value),
198 Self::RangeSlider { start, end, .. } => write!(f, "Range({}..{})", start, end),
199 Self::ProgressBar { value, .. } => write!(f, "Progress({})", value),
200 }
201 }
202}
203
204#[derive(Clone, Debug)]
205pub struct View {
206 pub id: ViewId,
207 pub kind: ViewKind,
208 pub modifier: Modifier,
209 pub children: Vec<View>,
210 pub semantics: Option<crate::semantics::Semantics>,
211}
212
213impl View {
214 pub fn new(id: ViewId, kind: ViewKind) -> Self {
215 View {
216 id,
217 kind,
218 modifier: Modifier::default(),
219 children: vec![],
220 semantics: None,
221 }
222 }
223 pub fn modifier(mut self, m: Modifier) -> Self {
224 self.modifier = m;
225 self
226 }
227 pub fn disabled(mut self) -> Self {
229 self.modifier.disabled = true;
230 self
231 }
232 pub fn with_children(mut self, kids: Vec<View>) -> Self {
233 self.children = kids;
234 self
235 }
236 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
237 self.semantics = Some(s);
238 self
239 }
240}
241
242#[derive(Clone, Debug, Default)]
244pub struct Scene {
245 pub clear_color: Color,
246 pub nodes: Vec<SceneNode>,
247}
248
249#[derive(Clone, Debug)]
250pub enum SceneNode {
251 Rect {
252 rect: Rect,
253 brush: Brush,
254 radius: f32,
255 },
256 Border {
257 rect: Rect,
258 color: Color,
259 width: f32,
260 radius: f32,
261 },
262 Text {
263 rect: Rect,
264 text: Arc<str>,
265 color: Color,
266 size: f32,
267 font_family: Option<&'static str>,
268 },
269 Ellipse {
270 rect: Rect,
271 brush: Brush,
272 },
273 EllipseBorder {
274 rect: Rect,
275 color: Color,
276 width: f32, },
278 PushClip {
279 rect: Rect,
280 radius: f32,
281 },
282 PopClip,
283 PushTransform {
284 transform: Transform,
285 },
286 PopTransform,
287 Image {
288 rect: Rect,
289 handle: ImageHandle,
290 tint: Color,
291 fit: ImageFit,
292 },
293 Shadow {
296 rect: Rect,
297 radius: f32,
298 elevation: f32,
299 color: Color,
300 },
301 BeginLayer {
305 rect: Rect,
306 layer_id: u32,
307 alpha: f32,
308 },
309 EndLayer {
311 layer_id: u32,
312 },
313 CompositeShadow {
318 layer_id: u32,
319 blur_px: f32,
320 offset_px: (f32, f32),
321 color: Color,
322 },
323}
324
325pub type CallbackF32 = Rc<dyn Fn(f32)>;
326pub type CallbackRange = Rc<dyn Fn(f32, f32)>;
327
328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
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}