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)]
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        /// Set by the component (e.g. OutlinedTextField) to receive focus-change
124        /// signals from the layout/paint phase.
125        focus_tracker: Option<Rc<Cell<bool>>>,
126        /// Current text content, supplied by the caller. The platform syncs
127        value: String,
128        /// Optional text display transformation (e.g., password masking).
129        visual_transformation: Option<Rc<dyn crate::text::VisualTransformation>>,
130        /// Keyboard type hint for the platform IME.
131        keyboard_type: Option<crate::text::KeyboardType>,
132        /// IME action button configuration.
133        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, // multiplicative (WHITE = no tint)
159        fit: ImageFit,
160    },
161    Ellipse {
162        rect: Rect,
163        color: Color,
164    },
165    EllipseBorder {
166        rect: Rect,
167        color: Color,
168        width: f32, // screen-space width (px)
169    },
170    /// A layout whose children are produced by calling `content` with the
171    /// current `SubcomposeScope`. The closure is invoked during reconciliation
172    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
173    /// identity used to reconcile the returned view across frames. This is
174    /// the building block for `BoxWithConstraints` and other
175    /// constraints-driven layouts.
176    ///
177    /// Note: any `Modifier::key` set on a returned view is overwritten by its
178    /// slot id so the slot's identity is stable across frames.
179    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    /// Mark this view as disabled - ignores pointer events.
253    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/// Renderable scene
268#[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, // screen-space width (px)
302    },
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 behind a rounded rect, typically driven by `StateElevation`.
319    /// The `elevation` field controls offset and alpha.
320    Shadow {
321        rect: Rect,
322        radius: f32,
323        elevation: f32,
324        color: Color,
325    },
326    /// Mark the start of a graphics layer: the contained subtree is rendered
327    /// into an offscreen texture and then composited back into the parent.
328    /// `alpha` is the group-compositing alpha applied at composite time.
329    BeginLayer {
330        rect: Rect,
331        layer_id: u32,
332        alpha: f32,
333    },
334    /// Closes the graphics layer opened by the matching `BeginLayer`.
335    EndLayer {
336        layer_id: u32,
337    },
338    /// Draws a blurred drop shadow underneath a previously-rendered layer.
339    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
340    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
341    /// optional vertical offset.
342    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}