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 visual_transformation: Option<Rc<dyn crate::text::VisualTransformation>>,
130 keyboard_type: Option<crate::text::KeyboardType>,
132 ime_action: Option<crate::text::ImeAction>,
134 },
135 Slider {
136 value: f32,
137 min: f32,
138 max: f32,
139 step: Option<f32>,
140 on_change: Option<CallbackF32>,
141 },
142 RangeSlider {
143 start: f32,
144 end: f32,
145 min: f32,
146 max: f32,
147 step: Option<f32>,
148 on_change: Option<CallbackRange>,
149 },
150 ProgressBar {
151 value: f32,
152 min: f32,
153 max: f32,
154 circular: bool,
155 },
156 Image {
157 handle: ImageHandle,
158 tint: Color, fit: ImageFit,
160 },
161 Ellipse {
162 rect: Rect,
163 color: Color,
164 },
165 EllipseBorder {
166 rect: Rect,
167 color: Color,
168 width: f32, },
170 SubcomposeLayout {
180 content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
181 },
182}
183
184impl std::fmt::Debug for ViewKind {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 match self {
187 Self::Surface => f.write_str("Surface"),
188 Self::Box => f.write_str("Box"),
189 Self::Row => f.write_str("Row"),
190 Self::Column => f.write_str("Column"),
191 Self::Stack => f.write_str("Stack"),
192 Self::ZStack => f.write_str("ZStack"),
193 Self::OverlayHost => f.write_str("OverlayHost"),
194 Self::ScrollV { .. } => f.write_str("ScrollV"),
195 Self::ScrollXY { .. } => f.write_str("ScrollXY"),
196 Self::Button { .. } => f.write_str("Button"),
197 Self::Image { .. } => f.write_str("Image"),
198 Self::Ellipse { .. } => f.write_str("Ellipse"),
199 Self::EllipseBorder { .. } => f.write_str("EllipseBorder"),
200 Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
201 Self::Text { text, .. } => write!(f, "Text({:?})", text),
202 Self::TextField {
203 hint,
204 visual_transformation,
205 keyboard_type,
206 ime_action,
207 ..
208 } => {
209 let mut s = f.debug_struct("TextField");
210 s.field("hint", hint);
211 if visual_transformation.is_some() {
212 s.field("visual_transformation", &"…");
213 }
214 if let Some(kt) = keyboard_type {
215 s.field("keyboard_type", kt);
216 }
217 if let Some(ia) = ime_action {
218 s.field("ime_action", ia);
219 }
220 s.finish()
221 }
222 Self::Slider { value, .. } => write!(f, "Slider({})", value),
223 Self::RangeSlider { start, end, .. } => write!(f, "Range({}..{})", start, end),
224 Self::ProgressBar { value, .. } => write!(f, "Progress({})", value),
225 }
226 }
227}
228
229#[derive(Clone, Debug)]
230pub struct View {
231 pub id: ViewId,
232 pub kind: ViewKind,
233 pub modifier: Modifier,
234 pub children: Vec<View>,
235 pub semantics: Option<crate::semantics::Semantics>,
236}
237
238impl View {
239 pub fn new(id: ViewId, kind: ViewKind) -> Self {
240 View {
241 id,
242 kind,
243 modifier: Modifier::default(),
244 children: vec![],
245 semantics: None,
246 }
247 }
248 pub fn modifier(mut self, m: Modifier) -> Self {
249 self.modifier = m;
250 self
251 }
252 pub fn disabled(mut self) -> Self {
254 self.modifier.disabled = true;
255 self
256 }
257 pub fn with_children(mut self, kids: Vec<View>) -> Self {
258 self.children = kids;
259 self
260 }
261 pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
262 self.semantics = Some(s);
263 self
264 }
265}
266
267#[derive(Clone, Debug, Default)]
269pub struct Scene {
270 pub clear_color: Color,
271 pub nodes: Vec<SceneNode>,
272}
273
274#[derive(Clone, Debug)]
275pub enum SceneNode {
276 Rect {
277 rect: Rect,
278 brush: Brush,
279 radius: f32,
280 },
281 Border {
282 rect: Rect,
283 color: Color,
284 width: f32,
285 radius: f32,
286 },
287 Text {
288 rect: Rect,
289 text: Arc<str>,
290 color: Color,
291 size: f32,
292 font_family: Option<&'static str>,
293 },
294 Ellipse {
295 rect: Rect,
296 brush: Brush,
297 },
298 EllipseBorder {
299 rect: Rect,
300 color: Color,
301 width: f32, },
303 PushClip {
304 rect: Rect,
305 radius: f32,
306 },
307 PopClip,
308 PushTransform {
309 transform: Transform,
310 },
311 PopTransform,
312 Image {
313 rect: Rect,
314 handle: ImageHandle,
315 tint: Color,
316 fit: ImageFit,
317 },
318 Shadow {
321 rect: Rect,
322 radius: f32,
323 elevation: f32,
324 color: Color,
325 },
326 BeginLayer {
330 rect: Rect,
331 layer_id: u32,
332 alpha: f32,
333 },
334 EndLayer {
336 layer_id: u32,
337 },
338 CompositeShadow {
343 layer_id: u32,
344 blur_px: f32,
345 offset_px: (f32, f32),
346 color: Color,
347 },
348}
349
350pub type CallbackF32 = Rc<dyn Fn(f32)>;
351pub type CallbackRange = Rc<dyn Fn(f32, f32)>;
352
353#[derive(Clone, Copy, Debug, PartialEq, Eq)]
354pub enum TextOverflow {
355 Visible,
356 Clip,
357 Ellipsis,
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn subcompose_scope_unbounded_has_infinite_max() {
366 let s = SubcomposeScope::UNBOUNDED;
367 assert!(!s.max_width.is_finite());
368 assert!(!s.max_height.is_finite());
369 assert_eq!(s.min_width, 0.0);
370 assert_eq!(s.min_height, 0.0);
371 }
372
373 #[test]
374 fn subcompose_scope_new_round_trips() {
375 let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
376 assert_eq!(s.min_width, 10.0);
377 assert_eq!(s.max_width, 200.0);
378 assert_eq!(s.min_height, 20.0);
379 assert_eq!(s.max_height, 300.0);
380 }
381
382 #[test]
383 fn box_with_constraints_scope_bounded_predicates() {
384 let bounded = BoxWithConstraintsScope {
385 min_width: 0.0,
386 max_width: 360.0,
387 min_height: 0.0,
388 max_height: 640.0,
389 };
390 assert!(bounded.has_bounded_width());
391 assert!(bounded.has_bounded_height());
392
393 let unbounded = BoxWithConstraintsScope {
394 min_width: 0.0,
395 max_width: f32::INFINITY,
396 min_height: 0.0,
397 max_height: f32::INFINITY,
398 };
399 assert!(!unbounded.has_bounded_width());
400 assert!(!unbounded.has_bounded_height());
401 }
402
403 #[test]
404 fn view_kind_subcompose_layout_holds_closure() {
405 let v: View = View {
406 id: 0,
407 kind: ViewKind::SubcomposeLayout {
408 content: std::sync::Arc::new(|scope| {
409 let _ = scope.max_width;
410 vec![(0, View::new(0, ViewKind::Box))]
411 }),
412 },
413 modifier: Modifier::default(),
414 children: vec![],
415 semantics: None,
416 };
417 match &v.kind {
418 ViewKind::SubcomposeLayout { .. } => {}
419 _ => panic!("expected SubcomposeLayout"),
420 }
421 }
422
423 #[test]
424 fn view_kind_subcompose_layout_supports_multiple_slots() {
425 let v: View = View {
426 id: 0,
427 kind: ViewKind::SubcomposeLayout {
428 content: std::sync::Arc::new(|_scope| {
429 vec![
430 (1, View::new(0, ViewKind::Box)),
431 (2, View::new(0, ViewKind::Box)),
432 (3, View::new(0, ViewKind::Box)),
433 ]
434 }),
435 },
436 modifier: Modifier::default(),
437 children: vec![],
438 semantics: None,
439 };
440 if let ViewKind::SubcomposeLayout { content } = &v.kind {
441 let slots = content(&SubcomposeScope::UNBOUNDED);
442 assert_eq!(slots.len(), 3);
443 assert_eq!(slots[0].0, 1);
444 assert_eq!(slots[1].0, 2);
445 assert_eq!(slots[2].0, 3);
446 } else {
447 panic!("expected SubcomposeLayout");
448 }
449 }
450}