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