floem/
view_state.rs

1use crate::{
2    animate::Animation,
3    context::{
4        CleanupListeners, EventCallback, InteractionState, MenuCallback, MoveListeners,
5        ResizeCallback, ResizeListeners,
6    },
7    event::EventListener,
8    prop_extractor,
9    responsive::ScreenSizeBp,
10    style::{
11        Background, BorderColorProp, BorderRadiusProp, BoxShadowProp, LayoutProps, Outline,
12        OutlineColor, Style, StyleClassRef, StyleSelectors, resolve_nested_maps,
13    },
14};
15use bitflags::bitflags;
16use imbl::HashSet;
17use peniko::kurbo::{Affine, Point, Rect};
18use smallvec::SmallVec;
19use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc};
20use taffy::tree::NodeId;
21use ui_events::pointer::PointerState;
22
23/// A stack of view attributes. Each entry is associated with a view decorator call.
24#[derive(Debug)]
25pub struct Stack<T> {
26    pub stack: SmallVec<[T; 1]>,
27}
28
29impl<T> Default for Stack<T> {
30    fn default() -> Self {
31        Stack {
32            stack: SmallVec::new(),
33        }
34    }
35}
36
37pub struct StackOffset<T> {
38    offset: usize,
39    phantom: PhantomData<T>,
40}
41
42impl<T> Clone for StackOffset<T> {
43    fn clone(&self) -> Self {
44        *self
45    }
46}
47
48impl<T> Copy for StackOffset<T> {}
49
50impl<T> Stack<T> {
51    pub fn next_offset(&mut self) -> StackOffset<T> {
52        StackOffset {
53            offset: self.stack.len(),
54            phantom: PhantomData,
55        }
56    }
57    pub fn push(&mut self, value: T) {
58        self.stack.push(value);
59    }
60    pub fn set(&mut self, offset: StackOffset<T>, value: T) {
61        self.stack[offset.offset] = value;
62    }
63
64    pub fn update(&mut self, offset: StackOffset<T>, update: impl Fn(&mut T) + 'static) {
65        update(&mut self.stack[offset.offset]);
66    }
67
68    pub fn get(&mut self, offset: StackOffset<T>) -> &T {
69        &self.stack[offset.offset]
70    }
71}
72
73#[cfg(feature = "vello")]
74prop_extractor! {
75    pub(crate) ViewStyleProps {
76        pub border_radius: BorderRadiusProp,
77        pub border_progress: crate::style::BorderProgress,
78
79        pub outline: Outline,
80        pub outline_color: OutlineColor,
81        pub outline_progress: crate::style::OutlineProgress,
82        pub border_color: BorderColorProp,
83        pub background: Background,
84        pub shadow: BoxShadowProp,
85    }
86}
87// removing outlines to make clippy happy about progress fields not being read
88#[cfg(not(feature = "vello"))]
89prop_extractor! {
90    pub(crate) ViewStyleProps {
91        pub border_radius: BorderRadiusProp,
92
93        pub outline: Outline,
94        pub outline_color: OutlineColor,
95        pub border_color: BorderColorProp,
96        pub background: Background,
97        pub shadow: BoxShadowProp,
98    }
99}
100
101bitflags! {
102    #[derive(Default, Copy, Clone, Debug)]
103    #[must_use]
104    pub(crate) struct ChangeFlags: u8 {
105        const STYLE = 1;
106        const LAYOUT = 1 << 1;
107        const VIEW_STYLE = 1 << 2;
108    }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum IsHiddenState {
113    Visible(taffy::style::Display),
114    AnimatingOut(taffy::style::Display),
115    Hidden,
116    None,
117}
118impl IsHiddenState {
119    pub(crate) fn get_display(&self) -> Option<taffy::style::Display> {
120        match self {
121            IsHiddenState::AnimatingOut(dis) => Some(*dis),
122            _ => None,
123        }
124    }
125
126    pub(crate) fn transition(
127        &mut self,
128        computed_display: taffy::Display,
129        remove_animations: impl FnOnce() -> bool,
130        add_animations: impl FnOnce(),
131        stop_reset_animations: impl FnOnce(),
132        num_waiting_anim: impl FnOnce() -> u16,
133    ) {
134        let computed_has_hide = computed_display == taffy::Display::None;
135        *self = match self {
136            // initial states (makes it so that the animations aren't run on initial app/view load)
137            Self::None if computed_has_hide => Self::Hidden,
138            Self::None if !computed_has_hide => Self::Visible(computed_display),
139            // do nothing
140            Self::Visible(dis) if !computed_has_hide => Self::Visible(*dis),
141            // transition to hidden
142            Self::Visible(dis) if computed_has_hide => {
143                let active_animations = remove_animations();
144                if active_animations {
145                    Self::AnimatingOut(*dis)
146                } else {
147                    Self::Hidden
148                }
149            }
150            Self::AnimatingOut(_) if !computed_has_hide => {
151                stop_reset_animations();
152                Self::Visible(computed_display)
153            }
154            Self::AnimatingOut(dis) if computed_has_hide => {
155                if num_waiting_anim() == 0 {
156                    Self::Hidden
157                } else {
158                    Self::AnimatingOut(*dis)
159                }
160            }
161            Self::Hidden if computed_has_hide => Self::Hidden,
162            Self::Hidden if !computed_has_hide => {
163                add_animations();
164                Self::Visible(computed_display)
165            }
166            _ => unreachable!(),
167        };
168    }
169}
170
171/// View state stores internal state associated with a view which is owned and managed by Floem.
172pub struct ViewState {
173    pub(crate) node: NodeId,
174    pub(crate) requested_changes: ChangeFlags,
175    pub(crate) style: Stack<Style>,
176    /// We store the stack offset to the view style to keep the api consistent but it should
177    /// always be the first offset.
178    pub(crate) view_style_offset: StackOffset<Style>,
179    /// Layout is requested on all direct and indirect children.
180    pub(crate) request_style_recursive: bool,
181    pub(crate) has_style_selectors: StyleSelectors,
182    pub(crate) viewport: Option<Rect>,
183    pub(crate) layout_rect: Rect,
184    pub(crate) layout_props: LayoutProps,
185    pub(crate) view_style_props: ViewStyleProps,
186    pub(crate) animations: Stack<Animation>,
187    pub(crate) classes: Vec<StyleClassRef>,
188    pub(crate) dragging_style: Option<Style>,
189    /// Combine the stacked style into one style, and apply the interact state.
190    pub(crate) combined_style: Style,
191    /// The final style including inherited style from parent.
192    pub(crate) computed_style: Style,
193    pub(crate) taffy_style: taffy::style::Style,
194    pub(crate) event_listeners: HashMap<EventListener, Vec<Rc<RefCell<EventCallback>>>>,
195    pub(crate) context_menu: Option<Rc<MenuCallback>>,
196    pub(crate) popout_menu: Option<Rc<MenuCallback>>,
197    pub(crate) resize_listeners: Rc<RefCell<ResizeListeners>>,
198    pub(crate) window_origin: Point,
199    pub(crate) move_listeners: Rc<RefCell<MoveListeners>>,
200    pub(crate) cleanup_listeners: Rc<RefCell<CleanupListeners>>,
201    pub(crate) last_pointer_down: Option<PointerState>,
202    pub(crate) is_hidden_state: IsHiddenState,
203    pub(crate) num_waiting_animations: u16,
204    pub(crate) disable_default_events: HashSet<EventListener>,
205    pub(crate) transform: Affine,
206    pub(crate) debug_name: SmallVec<[String; 1]>,
207}
208
209impl ViewState {
210    pub(crate) fn new(taffy: &mut taffy::TaffyTree) -> Self {
211        let mut style = Stack::<Style>::default();
212        let view_style_offset = style.next_offset();
213        style.push(Style::new());
214
215        Self {
216            node: taffy.new_leaf(taffy::style::Style::DEFAULT).unwrap(),
217            viewport: None,
218            style,
219            view_style_offset,
220            layout_rect: Rect::ZERO,
221            layout_props: Default::default(),
222            view_style_props: Default::default(),
223            requested_changes: ChangeFlags::all(),
224            request_style_recursive: false,
225            has_style_selectors: StyleSelectors::default(),
226            animations: Default::default(),
227            classes: Vec::new(),
228            combined_style: Style::new(),
229            computed_style: Style::new(),
230            taffy_style: taffy::style::Style::DEFAULT,
231            dragging_style: None,
232            event_listeners: HashMap::new(),
233            context_menu: None,
234            popout_menu: None,
235            resize_listeners: Default::default(),
236            move_listeners: Default::default(),
237            cleanup_listeners: Default::default(),
238            last_pointer_down: None,
239            window_origin: Point::ZERO,
240            is_hidden_state: IsHiddenState::None,
241            num_waiting_animations: 0,
242            disable_default_events: HashSet::new(),
243            transform: Affine::IDENTITY,
244            debug_name: Default::default(),
245        }
246    }
247
248    /// the first returned bool is new_frame. the second is classes_applied
249    /// Returns `true` if a new frame is requested.
250    ///
251    // The context has the nested maps of classes and inherited properties
252    pub(crate) fn compute_combined(
253        &mut self,
254        interact_state: InteractionState,
255        screen_size_bp: ScreenSizeBp,
256        view_class: Option<StyleClassRef>,
257        context: &Style,
258        cx_hidden: bool,
259    ) -> (bool, bool) {
260        let mut new_frame = false;
261
262        // Build the initial combined style
263        let mut combined_style = Style::new();
264
265        let mut classes: SmallVec<[_; 4]> = SmallVec::new();
266
267        // Apply view class if provided
268        if let Some(view_class) = view_class {
269            classes.insert(0, view_class);
270        }
271
272        for class in &self.classes {
273            classes.push(*class);
274        }
275
276        let mut new_context = context.clone();
277        let mut new_classes = false;
278
279        let (resolved_style, classes_applied) = resolve_nested_maps(
280            combined_style,
281            &interact_state,
282            screen_size_bp,
283            &classes,
284            &mut new_context,
285        );
286        combined_style = resolved_style;
287        new_classes |= classes_applied;
288
289        let self_style = self.style();
290
291        combined_style.apply_mut(self_style.clone());
292
293        let (resolved_style, classes_applied) = resolve_nested_maps(
294            combined_style,
295            &interact_state,
296            screen_size_bp,
297            &classes,
298            &mut new_context,
299        );
300        combined_style = resolved_style;
301        new_classes |= classes_applied;
302
303        // Track if this style has selectors for optimization purposes
304        self.has_style_selectors = combined_style.selectors();
305
306        // Process animations
307        for animation in self
308            .animations
309            .stack
310            .iter_mut()
311            .filter(|anim| anim.can_advance() || anim.should_apply_folded())
312        {
313            if animation.can_advance() {
314                new_frame = true;
315                animation.animate_into(&mut combined_style);
316                animation.advance();
317            } else {
318                animation.apply_folded(&mut combined_style)
319            }
320            debug_assert!(!animation.is_idle());
321        }
322
323        // Apply visibility
324        if cx_hidden {
325            combined_style = combined_style.hide();
326        }
327
328        self.combined_style = combined_style;
329        (new_frame, new_classes)
330    }
331
332    pub(crate) fn has_active_animation(&self) -> bool {
333        for animation in self.animations.stack.iter() {
334            if animation.is_in_progress() {
335                return true;
336            }
337        }
338        false
339    }
340
341    pub(crate) fn style(&self) -> Style {
342        let mut result = Style::new();
343        for entry in self.style.stack.iter() {
344            result.apply_mut(entry.clone());
345        }
346        result
347    }
348
349    pub(crate) fn add_event_listener(
350        &mut self,
351        listener: EventListener,
352        action: Box<EventCallback>,
353    ) {
354        self.event_listeners
355            .entry(listener)
356            .or_default()
357            .push(Rc::new(RefCell::new(action)));
358    }
359
360    pub(crate) fn add_resize_listener(&mut self, action: Rc<ResizeCallback>) {
361        self.resize_listeners.borrow_mut().callbacks.push(action);
362    }
363
364    pub(crate) fn add_move_listener(&mut self, action: Rc<dyn Fn(Point)>) {
365        self.move_listeners.borrow_mut().callbacks.push(action);
366    }
367
368    pub(crate) fn add_cleanup_listener(&mut self, action: Rc<dyn Fn()>) {
369        self.cleanup_listeners.borrow_mut().push(action);
370    }
371}