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