Skip to main content

repose_core/
view.rs

1use crate::{Brush, Color, Modifier, Rect, TextSpan, Transform};
2use std::{cell::Cell, rc::Rc, sync::Arc};
3
4/// The constraints that will be passed to a subcomposed child. Values are in
5/// device-independent pixels (dp), matching the units used by `Modifier`.
6#[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    /// A scope with no constraints: unbounded in both dimensions. Use this as
16    /// a default when the parent constraints are not yet known.
17    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    /// Construct a scope from raw min/max dp values.
25    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/// Scope passed to [`BoxWithConstraints`](crate::prelude::BoxWithConstraints)
36/// content. All values are in dp.
37#[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    /// `true` if the width is bounded by the parent (i.e. not infinite).
47    pub fn has_bounded_width(&self) -> bool {
48        self.max_width.is_finite()
49    }
50
51    /// `true` if the height is bounded by the parent (i.e. not infinite).
52    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        /// Set by the component (e.g. OutlinedTextField) to receive focus-change
126        /// signals from the layout/paint phase.
127        focus_tracker: Option<Rc<Cell<bool>>>,
128        /// Current text content, supplied by the caller. The platform syncs
129        value: String,
130        /// Optional text display transformation (e.g., password masking).
131        visual_transformation: Option<Rc<dyn crate::text::VisualTransformation>>,
132        /// Keyboard type hint for the platform IME.
133        keyboard_type: Option<crate::text::KeyboardType>,
134        /// IME action button configuration.
135        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, // multiplicative (WHITE = no tint)
161        fit: ImageFit,
162    },
163    Ellipse {
164        rect: Rect,
165        color: Color,
166    },
167    EllipseBorder {
168        rect: Rect,
169        color: Color,
170        width: f32, // screen-space width (px)
171    },
172    /// A layout whose children are produced by calling `content` with the
173    /// current `SubcomposeScope`. The closure is invoked during reconciliation
174    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
175    /// identity used to reconcile the returned view across frames. This is
176    /// the building block for `BoxWithConstraints` and other
177    /// constraints-driven layouts.
178    ///
179    /// Note: any `Modifier::key` set on a returned view is overwritten by its
180    /// slot id so the slot's identity is stable across frames.
181    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    /// Mark this view as disabled - ignores pointer events.
255    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/// Renderable scene
270#[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, // screen-space width (px)
305    },
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 behind a rounded rect, typically driven by `StateElevation`.
322    /// The `elevation` field controls offset and alpha.
323    Shadow {
324        rect: Rect,
325        radius: f32,
326        elevation: f32,
327        color: Color,
328    },
329    /// Mark the start of a graphics layer: the contained subtree is rendered
330    /// into an offscreen texture and then composited back into the parent.
331    /// `alpha` is the group-compositing alpha applied at composite time.
332    BeginLayer {
333        rect: Rect,
334        layer_id: u32,
335        alpha: f32,
336    },
337    /// Closes the graphics layer opened by the matching `BeginLayer`.
338    EndLayer {
339        layer_id: u32,
340    },
341    /// Draws a blurred drop shadow underneath a previously-rendered layer.
342    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
343    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
344    /// optional vertical offset.
345    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}