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