Skip to main content

ravenrs_widgets/
view.rs

1use crate::context::{BuildCx, NodeId};
2
3/// Anything that can appear in the UI.
4///
5/// `build` runs whenever the UI rebuilds. It must be cheap and side-effect free:
6/// read state, create layout boxes and paint instructions through `cx`, and
7/// return the node that represents this view.
8pub trait View {
9    /// Creates this view's node for the current build.
10    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId;
11
12    /// The name the diagnostics panel shows this view's cost under.
13    fn type_name(&self) -> &'static str {
14        std::any::type_name::<Self>()
15    }
16}
17
18/// A component composed from other views, SwiftUI style.
19///
20/// Keep state in the struct's fields (e.g. `State<T>`, created once in a
21/// constructor); `body` is called on every rebuild and must not create state.
22///
23/// ```
24/// use ravenrs_reactive::State;
25/// use ravenrs_widgets::{Button, Component, Text, VStack, View};
26///
27/// struct Counter {
28///     count: State<i32>,
29/// }
30///
31/// impl Component for Counter {
32///     fn body(&self) -> impl View {
33///         let count = self.count.clone();
34///         VStack::new((
35///             Text::bound(&self.count, |c| format!("Count: {c}")),
36///             Button::new("Increment").on_click(move || count.update(|c| *c += 1)),
37///         ))
38///     }
39/// }
40/// ```
41pub trait Component {
42    /// The views this component is made of; called on every rebuild.
43    fn body(&self) -> impl View;
44}
45
46impl<C: Component> View for C {
47    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
48        let body = self.body();
49        cx.child(&body)
50    }
51}
52
53/// A type-erased view, for heterogeneous lists and dynamic UI.
54pub type AnyView = Box<dyn View>;
55
56impl View for Box<dyn View> {
57    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
58        (**self).build(cx)
59    }
60
61    fn type_name(&self) -> &'static str {
62        (**self).type_name()
63    }
64}
65
66/// Renders nothing when `None`, so `Option<V>` expresses conditional UI.
67impl<V: View> View for Option<V> {
68    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
69        match self {
70            Some(v) => v.build(cx),
71            None => cx.empty(),
72        }
73    }
74}
75
76/// Gives a view an explicit identity, like SwiftUI's `.id()`: when the key
77/// changes, the view is treated as new (its local state and scroll position
78/// start over).
79#[derive(Debug)]
80pub struct Keyed<V> {
81    key: u64,
82    view: V,
83}
84
85#[cfg_attr(feature = "live", ravenrs_live::expose)]
86impl<V: View> Keyed<V> {
87    /// Wraps `view` with identity `key`.
88    pub fn new(key: u64, view: V) -> Self {
89        Self { key, view }
90    }
91}
92
93impl<V: View> View for Keyed<V> {
94    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
95        cx.keyed_child(self.key, &self.view)
96    }
97}
98
99/// A view built from the viewport size; see [`responsive`].
100pub struct Responsive<F>(F);
101
102impl<F> std::fmt::Debug for Responsive<F> {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.write_str("Responsive")
105    }
106}
107
108/// Builds a view from the viewport size (in logical pixels), like SwiftUI's
109/// `GeometryReader`: use it to switch layouts at breakpoints.
110///
111/// ```
112/// use ravenrs_widgets::{HStack, Text, VStack, View, responsive, AnyView};
113///
114/// let layout = responsive(|size| -> AnyView {
115///     if size.width < 600.0 { Box::new(VStack::new((Text::new("A"), Text::new("B")))) }
116///     else { Box::new(HStack::new((Text::new("A"), Text::new("B")))) }
117/// });
118/// ```
119pub fn responsive<F, V>(f: F) -> Responsive<F>
120where
121    F: Fn(ravenrs_core::Size) -> V,
122    V: View,
123{
124    Responsive(f)
125}
126
127impl<F, V> View for Responsive<F>
128where
129    F: Fn(ravenrs_core::Size) -> V,
130    V: View,
131{
132    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
133        let view = (self.0)(cx.viewport());
134        cx.child(&view)
135    }
136}
137
138/// A view produced by a closure on every build; see [`view_fn`].
139pub struct FnView<F>(F);
140
141impl<F> std::fmt::Debug for FnView<F> {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.write_str("FnView")
144    }
145}
146
147/// Wraps a closure as a view. Whatever state the closure reads is tracked, so
148/// this is the simplest way to make a region of the UI depend on state:
149///
150/// ```
151/// use ravenrs_reactive::State;
152/// use ravenrs_widgets::{Text, view_fn};
153///
154/// let logged_in = State::new(false);
155/// let l = logged_in.clone();
156/// let greeting = view_fn(move || Text::new(if l.get() { "Welcome back" } else { "Sign in" }));
157/// ```
158pub fn view_fn<F, V>(f: F) -> FnView<F>
159where
160    F: Fn() -> V + 'static,
161    V: View,
162{
163    FnView(f)
164}
165
166impl<F, V> View for FnView<F>
167where
168    F: Fn() -> V,
169    V: View,
170{
171    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
172        let view = (self.0)();
173        cx.child(&view)
174    }
175}
176
177/// An ordered collection of child views: tuples of up to 12 views, `Vec<V>`
178/// and `Vec<AnyView>`.
179pub trait ViewSeq {
180    /// Builds each child in order and appends its node to `out`.
181    fn build_each(&self, cx: &mut BuildCx<'_>, out: &mut Vec<NodeId>);
182}
183
184impl<C: ViewSeq + ?Sized> ViewSeq for &C {
185    fn build_each(&self, cx: &mut BuildCx<'_>, out: &mut Vec<NodeId>) {
186        (**self).build_each(cx, out);
187    }
188}
189
190impl<V: View> ViewSeq for Vec<V> {
191    fn build_each(&self, cx: &mut BuildCx<'_>, out: &mut Vec<NodeId>) {
192        for v in self {
193            out.push(cx.child(v));
194        }
195    }
196}
197
198macro_rules! impl_view_seq_for_tuple {
199    ($($name:ident),+) => {
200        impl<$($name: View),+> ViewSeq for ($($name,)+) {
201            #[allow(non_snake_case)]
202            fn build_each(&self, cx: &mut BuildCx<'_>, out: &mut Vec<NodeId>) {
203                let ($($name,)+) = self;
204                $(out.push(cx.child($name));)+
205            }
206        }
207    };
208}
209
210impl_view_seq_for_tuple!(A);
211impl_view_seq_for_tuple!(A, B);
212impl_view_seq_for_tuple!(A, B, C);
213impl_view_seq_for_tuple!(A, B, C, D);
214impl_view_seq_for_tuple!(A, B, C, D, E);
215impl_view_seq_for_tuple!(A, B, C, D, E, F);
216impl_view_seq_for_tuple!(A, B, C, D, E, F, G);
217impl_view_seq_for_tuple!(A, B, C, D, E, F, G, H);
218impl_view_seq_for_tuple!(A, B, C, D, E, F, G, H, I);
219impl_view_seq_for_tuple!(A, B, C, D, E, F, G, H, I, J);
220impl_view_seq_for_tuple!(A, B, C, D, E, F, G, H, I, J, K);
221impl_view_seq_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);