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