Skip to main content

fission_core/
view.rs

1//! Read-only view, widget trait, and selector pattern.
2//!
3//! During [`Widget::build`], the framework provides a [`View`] that gives
4//! read-only access to the current [`AppState`], theme, i18n registry,
5//! layout snapshot, and animation values. Widgets use this to decide what
6//! to render without any side-effects.
7
8use crate::{
9    env::VideoState,
10    registry::{AnimationPropertyId, VideoRegistration},
11    ui::{
12        Align, Button, Checkbox, Column, Container, Grid, GridItem, Image, LazyColumn, Node,
13        Overlay, Positioned, Radio, Row, Scroll, Slider, Spacer, Switch, Text, TextInput, Video,
14        ZStack,
15    },
16    AppState, BuildCtx, Env, LayoutRect, LayoutSize, LayoutSnapshot, RuntimeState,
17};
18use fission_i18n::I18nRegistry;
19use fission_ir::{NodeId, WidgetNodeId};
20use fission_layout::BoxConstraints;
21use fission_theme::Theme;
22
23/// Read-only access to application state and environment during widget building.
24///
25/// `View` is the primary way widgets read data. It is parameterised over the
26/// concrete [`AppState`] type `S`, giving type-safe access to `state` while
27/// also exposing the theme, i18n registry, layout snapshot from the previous
28/// frame, and animation values.
29///
30/// # Example
31///
32/// ```rust,ignore
33/// fn build(&self, _ctx: &mut BuildCtx<MyState>, view: &View<MyState>) -> Node {
34///     let name = &view.state.user_name;
35///     let theme = view.theme();
36///     Text::new(format!("Hello, {}!", name))
37///         .color(theme.tokens.colors.primary)
38///         .into_node()
39/// }
40/// ```
41pub struct View<'a, S: AppState> {
42    /// Reference to the current application state.
43    pub state: &'a S,
44    /// Runtime interaction, scroll, text-edit, and animation state.
45    pub runtime: &'a RuntimeState,
46    /// Environment (theme, i18n, viewport size, locale).
47    pub env: &'a Env,
48    /// Layout snapshot from the previous frame, if available.
49    pub layout: Option<&'a LayoutSnapshot>,
50}
51
52impl<'a, S: AppState> View<'a, S> {
53    pub fn new(
54        state: &'a S,
55        runtime: &'a RuntimeState,
56        env: &'a Env,
57        layout: Option<&'a LayoutSnapshot>,
58    ) -> Self {
59        Self {
60            state,
61            runtime,
62            env,
63            layout,
64        }
65    }
66
67    pub fn theme(&self) -> &Theme {
68        &self.env.theme
69    }
70    pub fn i18n(&self) -> &I18nRegistry {
71        &self.env.i18n
72    }
73
74    pub fn get_rect(&self, id: WidgetNodeId) -> Option<LayoutRect> {
75        let node_id: NodeId = id.into();
76        self.layout.and_then(|l| l.get_node_rect(node_id))
77    }
78
79    pub fn get_constraints(&self, id: WidgetNodeId) -> Option<BoxConstraints> {
80        let node_id: NodeId = id.into();
81        self.layout.and_then(|l| l.get_node_constraints(node_id))
82    }
83
84    pub fn viewport_size(&self) -> LayoutSize {
85        self.env.viewport_size
86    }
87
88    pub fn select<T: Selector<S>>(&self) -> T::Output {
89        T::select(self)
90    }
91
92    pub fn animation_value(&self, widget_id: WidgetNodeId, property: &AnimationPropertyId) -> f32 {
93        self.runtime
94            .animation
95            .values
96            .get(&(widget_id, property.clone()))
97            .copied()
98            .unwrap_or_else(|| property.default_value())
99    }
100
101    pub fn video_state(&self, widget_id: WidgetNodeId) -> Option<&VideoState> {
102        self.runtime.video.states.get(&widget_id)
103    }
104}
105
106/// A selector that derives a value from the [`View`].
107///
108/// Selectors extract and transform data from state so widgets can depend on
109/// derived values without coupling to the full state shape.
110///
111/// # Example
112///
113/// ```rust,ignore
114/// struct ItemCount;
115/// impl Selector<MyState> for ItemCount {
116///     type Output = usize;
117///     fn select(view: &View<MyState>) -> usize {
118///         view.state.items.len()
119///     }
120/// }
121///
122/// // In a widget:
123/// let count: usize = view.select::<ItemCount>();
124/// ```
125pub trait Selector<S: AppState> {
126    /// The type produced by the selector.
127    type Output;
128    /// Extract the value from the given view.
129    fn select(view: &View<S>) -> Self::Output;
130}
131
132/// The core trait for composable UI components.
133///
134/// A `Widget` produces a [`Node`] tree given read-only access to state
135/// ([`View`]) and a mutable build context ([`BuildCtx`]) for binding actions,
136/// registering portals, and requesting animations.
137///
138/// # Example
139///
140/// ```rust,ignore
141/// struct Greeting;
142///
143/// impl Widget<AppState> for Greeting {
144///     fn build(&self, ctx: &mut BuildCtx<AppState>, view: &View<AppState>) -> Node {
145///         let on_press = ctx.bind(SayHello, reduce_with!(handle_hello));
146///         Button {
147///             child: Some(Box::new(Text::new("Hello!").into_node())),
148///             on_press: Some(on_press),
149///             ..Default::default()
150///         }.into_node()
151///     }
152/// }
153/// ```
154pub trait Widget<S: AppState> {
155    /// Build the widget's node tree.
156    ///
157    /// Called once per frame. Implementations must be pure -- all side-effects
158    /// go through `ctx` (action binding, portals, animations).
159    fn build(&self, ctx: &mut BuildCtx<S>, view: &View<S>) -> Node;
160}
161
162// Implement Widget for Node (identity)
163impl<S: AppState> Widget<S> for Node {
164    fn build(&self, _ctx: &mut BuildCtx<S>, _view: &View<S>) -> Node {
165        self.clone()
166    }
167}
168
169macro_rules! impl_widget_for_primitive {
170    ($t:ty, $v:ident) => {
171        impl<S: AppState> Widget<S> for $t {
172            fn build(&self, _ctx: &mut BuildCtx<S>, _view: &View<S>) -> Node {
173                Node::$v(self.clone())
174            }
175        }
176    };
177}
178
179impl_widget_for_primitive!(Row, Row);
180impl_widget_for_primitive!(Column, Column);
181impl_widget_for_primitive!(Align, Align);
182impl_widget_for_primitive!(Text, Text);
183impl_widget_for_primitive!(Button, Button);
184impl_widget_for_primitive!(TextInput, TextInput);
185impl_widget_for_primitive!(Scroll, Scroll);
186impl_widget_for_primitive!(Image, Image);
187impl_widget_for_primitive!(ZStack, ZStack);
188impl_widget_for_primitive!(Overlay, Overlay);
189impl_widget_for_primitive!(Container, Container);
190impl_widget_for_primitive!(Grid, Grid);
191impl_widget_for_primitive!(GridItem, GridItem);
192impl_widget_for_primitive!(Checkbox, Checkbox);
193impl_widget_for_primitive!(Switch, Switch);
194impl_widget_for_primitive!(Radio, Radio);
195impl_widget_for_primitive!(Positioned, Positioned);
196impl_widget_for_primitive!(Spacer, Spacer);
197impl_widget_for_primitive!(Slider, Slider);
198impl_widget_for_primitive!(LazyColumn, LazyColumn);
199
200impl<S: AppState> Widget<S> for Video {
201    fn build(&self, ctx: &mut BuildCtx<S>, _view: &View<S>) -> Node {
202        let mut video = self.clone();
203        let id = video
204            .id
205            .unwrap_or_else(|| WidgetNodeId::explicit(&video.source));
206        video.id = Some(id);
207
208        ctx.register_video(VideoRegistration {
209            node_id: id,
210            source: video.source.clone(),
211            autoplay: video.autoplay,
212            loop_playback: video.loop_playback,
213        });
214
215        Node::Video(video)
216    }
217}