Skip to main content

floem/views/
decorator.rs

1#![deny(missing_docs)]
2
3//! # Decorator
4//!
5//! The decorator trait is the primary interface for extending the appearance and functionality of ['View']s.
6
7use floem_reactive::{Effect, SignalUpdate, UpdaterEffect};
8use peniko::kurbo::{Point, Rect};
9use std::rc::Rc;
10use ui_events::keyboard::{Key, KeyState, KeyboardEvent, Modifiers};
11
12use crate::{
13    ViewId,
14    action::{set_window_scale, set_window_title},
15    animate::Animation,
16    event::{Event, EventListener, EventPropagation},
17    platform::menu::Menu,
18    style::{Style, StyleClass},
19    view::{HasViewId, IntoView},
20};
21
22/// A trait that extends the appearance and functionality of Views through styling and event handling.
23///
24/// This trait is automatically implemented for all [`IntoView`] types via a blanket implementation.
25/// The decoration behavior depends on the type's [`IntoView::Intermediate`] type:
26///
27/// - **[`View`] types**: Decorated directly (already have a [`ViewId`])
28/// - **Primitives** (`&str`, `String`, `i32`, etc.): Wrapped in [`LazyView`](crate::LazyView)
29///   which creates a [`ViewId`] eagerly but defers view construction
30/// - **Tuples/Vecs**: Converted eagerly to their view type
31pub trait Decorators: IntoView {
32    /// Alter the style of the view.
33    ///
34    /// The Floem style system provides comprehensive styling capabilities including:
35    ///
36    /// ## Layout & Sizing
37    /// - **Flexbox & Grid**: Full CSS-style layout with `flex()`, `grid()`, alignment, and gap controls
38    /// - **Dimensions**: Width, height, min/max sizes with pixels, percentages, or auto sizing
39    /// - **Spacing**: Padding, margins with individual side control or shorthand methods
40    /// - **Positioning**: Absolute positioning with inset controls
41    ///
42    /// ## Visual Styling
43    /// - **Colors & Brushes**: Solid colors, gradients, and custom brushes for backgrounds and text
44    /// - **Borders**: Individual border styling per side with colors, widths, and radius
45    /// - **Shadows**: Box shadows with blur, spread, offset, and color customization
46    /// - **Typography**: Font family, size, weight, style, and line height control
47    ///
48    /// ## Interactive States
49    /// - **Pseudo-states**: Styling for hover, focus, active, disabled, and selected states
50    /// - **Dark Mode**: Automatic dark mode styling support
51    /// - **Responsive Design**: Breakpoint-based styling for different screen sizes
52    ///
53    /// ## Advanced Features
54    /// - **Animations**: Smooth transitions between style changes with easing functions
55    /// - **Custom Properties**: Define and use custom style properties for specialized views
56    /// - **Style Classes**: Reusable style definitions that can be applied across views
57    /// - **Conditional Styling**: Apply styles based on conditions using `apply_if()` and `apply_opt()`
58    /// - **Transform**: Scale, translate, and rotate transformations
59    ///
60    /// ## Style Application
61    /// Styles are reactive and will automatically update when dependencies change.
62    /// Subsequent calls to `style` will overwrite previous ones.
63    /// ```rust
64    /// # use floem::{peniko::color::palette, View, views::{Decorators, label, stack}};
65    /// fn view() -> impl View {
66    ///     label(|| "Hello".to_string())
67    ///         .style(|s| s.font_size(20.0).color(palette::css::RED))
68    /// }
69    ///
70    /// fn other() -> impl View {
71    ///     stack((
72    ///         view(), // will be red and size 20
73    ///         // will be green and default size due to the previous style being overwritten
74    ///         view().style(|s| s.color(palette::css::GREEN)),
75    ///     ))
76    /// }
77    /// ```
78    fn style(self, style: impl Fn(Style) -> Style + 'static) -> Self::Intermediate {
79        let intermediate = self.into_intermediate();
80        let view_id = intermediate.view_id();
81        let state = view_id.state();
82
83        let offset = state.borrow_mut().style.next_offset();
84        let style = UpdaterEffect::new(
85            move || style(Style::new()),
86            move |style| {
87                view_id.update_style(offset, style);
88            },
89        );
90        state.borrow_mut().style.push(style);
91
92        intermediate
93    }
94
95    /// Add a debug name to the view that will be shown in the inspector.
96    ///
97    /// This can be called multiple times and each name will be shown in the inspector with the most recent name showing first.
98    fn debug_name(self, name: impl Into<String>) -> Self::Intermediate {
99        let intermediate = self.into_intermediate();
100        let view_id = intermediate.view_id();
101        let state = view_id.state();
102        state.borrow_mut().debug_name.push(name.into());
103        intermediate
104    }
105
106    /// Conditionally add a debug name to the view that will be shown in the inspector.
107    ///
108    /// # Reactivity
109    /// Both the `apply` and `name` functions are reactive.
110    fn debug_name_if<S: Into<String>>(
111        self,
112        apply: impl Fn() -> bool + 'static,
113        name: impl Fn() -> S + 'static,
114    ) -> Self::Intermediate {
115        let intermediate = self.into_intermediate();
116        let view_id = intermediate.view_id();
117        Effect::new(move |_| {
118            let apply = apply();
119            let state = view_id.state();
120            if apply {
121                state.borrow_mut().debug_name.push(name().into());
122            } else {
123                state
124                    .borrow_mut()
125                    .debug_name
126                    .retain_mut(|n| n != &name().into());
127            }
128        });
129
130        intermediate
131    }
132
133    /// The visual style to apply when the view is being dragged
134    fn dragging_style(self, style: impl Fn(Style) -> Style + 'static) -> Self::Intermediate {
135        let intermediate = self.into_intermediate();
136        let view_id = intermediate.view_id();
137        Effect::new(move |_| {
138            let style = style(Style::new());
139            {
140                let state = view_id.state();
141                state.borrow_mut().dragging_style = Some(style);
142            }
143            view_id.request_style();
144        });
145        intermediate
146    }
147
148    /// Add a style class to the view
149    fn class<C: StyleClass>(self, _class: C) -> Self::Intermediate {
150        let intermediate = self.into_intermediate();
151        intermediate.view_id().add_class(C::class_ref());
152        intermediate
153    }
154
155    /// Conditionally add a style class to the view
156    fn class_if<C: StyleClass>(
157        self,
158        apply: impl Fn() -> bool + 'static,
159        _class: C,
160    ) -> Self::Intermediate {
161        let intermediate = self.into_intermediate();
162        let id = intermediate.view_id();
163        Effect::new(move |_| {
164            let apply = apply();
165            if apply {
166                id.add_class(C::class_ref());
167            } else {
168                ViewId::remove_class(&id, C::class_ref());
169            }
170        });
171        intermediate
172    }
173
174    /// Remove a style class from the view
175    fn remove_class<C: StyleClass>(self, _class: C) -> Self::Intermediate {
176        let intermediate = self.into_intermediate();
177        intermediate.view_id().remove_class(C::class_ref());
178        intermediate
179    }
180
181    /// Allows the element to be navigated to with the keyboard. Similar to setting tabindex="0" in html.
182    #[deprecated(note = "Set this property using `Style::focusable` instead")]
183    fn keyboard_navigable(self) -> Self::Intermediate {
184        self.style(|s| s.focusable(true))
185    }
186
187    /// Dynamically controls whether the default view behavior for an event should be disabled.
188    /// When disable is true, children will still see the event, but the view event function will not be called nor
189    /// the event listeners on the view.
190    ///
191    /// # Reactivity
192    /// This function is reactive and will re-run the disable function automatically in response to changes in signals
193    fn disable_default_event(
194        self,
195        disable: impl Fn() -> (EventListener, bool) + 'static,
196    ) -> Self::Intermediate {
197        let intermediate = self.into_intermediate();
198        let id = intermediate.view_id();
199        Effect::new(move |_| {
200            let (event, disable) = disable();
201            if disable {
202                id.disable_default_event(event);
203            } else {
204                id.remove_disable_default_event(event);
205            }
206        });
207        intermediate
208    }
209
210    /// Mark the view as draggable
211    #[deprecated(note = "use `Style::draggable` directly instead")]
212    fn draggable(self) -> Self::Intermediate {
213        self.style(move |s| s.draggable(true))
214    }
215
216    /// Mark the view as disabled
217    ///
218    /// # Reactivity
219    /// The `disabled_fn` is reactive.
220    #[deprecated(note = "use `Style::set_disabled` directly instead")]
221    fn disabled(self, disabled_fn: impl Fn() -> bool + 'static) -> Self::Intermediate {
222        self.style(move |s| s.set_disabled(disabled_fn()))
223    }
224
225    /// Add an event handler for the given [`EventListener`].
226    fn on_event(
227        self,
228        listener: EventListener,
229        action: impl FnMut(&Event) -> EventPropagation + 'static,
230    ) -> Self::Intermediate {
231        let intermediate = self.into_intermediate();
232        intermediate
233            .view_id()
234            .add_event_listener(listener, Box::new(action));
235        intermediate
236    }
237
238    /// Add an handler for pressing down a specific key.
239    ///
240    /// NOTE: View should have `.keyboard_navigable()` in order to receive keyboard events
241    fn on_key_down(
242        self,
243        key: Key,
244        cmp: impl Fn(Modifiers) -> bool + 'static,
245        action: impl Fn(&Event) + 'static,
246    ) -> Self::Intermediate {
247        self.on_event(EventListener::KeyDown, move |e| {
248            if let Event::Key(KeyboardEvent {
249                state: KeyState::Down,
250                key: event_key,
251                modifiers,
252                ..
253            }) = e
254            {
255                if *event_key == key && cmp(*modifiers) {
256                    action(e);
257                    return EventPropagation::Stop;
258                }
259            }
260            EventPropagation::Continue
261        })
262    }
263
264    /// Add an handler for a specific key being released.
265    ///
266    /// NOTE: View should have `.keyboard_navigable()` in order to receive keyboard events
267    fn on_key_up(
268        self,
269        key: Key,
270        cmp: impl Fn(Modifiers) -> bool + 'static,
271        action: impl Fn(&Event) + 'static,
272    ) -> Self::Intermediate {
273        self.on_event(EventListener::KeyUp, move |e| {
274            if let Event::Key(KeyboardEvent {
275                state: KeyState::Up,
276                key: event_key,
277                modifiers,
278                ..
279            }) = e
280            {
281                if *event_key == key && cmp(*modifiers) {
282                    action(e);
283                    return EventPropagation::Stop;
284                }
285            }
286            EventPropagation::Continue
287        })
288    }
289
290    /// Add an event handler for the given [`EventListener`]. This event will be handled with
291    /// the given handler and the event will continue propagating.
292    fn on_event_cont(
293        self,
294        listener: EventListener,
295        action: impl Fn(&Event) + 'static,
296    ) -> Self::Intermediate {
297        self.on_event(listener, move |e| {
298            action(e);
299            EventPropagation::Continue
300        })
301    }
302
303    /// Add an event handler for the given [`EventListener`]. This event will be handled with
304    /// the given handler and the event will stop propagating.
305    fn on_event_stop(
306        self,
307        listener: EventListener,
308        action: impl Fn(&Event) + 'static,
309    ) -> Self::Intermediate {
310        self.on_event(listener, move |e| {
311            action(e);
312            EventPropagation::Stop
313        })
314    }
315
316    /// Add an event handler for [`EventListener::Click`].
317    fn on_click(
318        self,
319        action: impl FnMut(&Event) -> EventPropagation + 'static,
320    ) -> Self::Intermediate {
321        self.on_event(EventListener::Click, action)
322    }
323
324    /// Add an event handler for [`EventListener::Click`]. This event will be handled with
325    /// the given handler and the event will continue propagating.
326    fn on_click_cont(self, action: impl Fn(&Event) + 'static) -> Self::Intermediate {
327        self.on_click(move |e| {
328            action(e);
329            EventPropagation::Continue
330        })
331    }
332
333    /// Add an event handler for [`EventListener::Click`]. This event will be handled with
334    /// the given handler and the event will stop propagating.
335    fn on_click_stop(self, mut action: impl FnMut(&Event) + 'static) -> Self::Intermediate {
336        self.on_click(move |e| {
337            action(e);
338            EventPropagation::Stop
339        })
340    }
341
342    /// Attach action executed on button click or Enter or Space Key.
343    fn action(self, mut action: impl FnMut() + 'static) -> Self::Intermediate {
344        self.on_click(move |_| {
345            action();
346            EventPropagation::Stop
347        })
348    }
349
350    /// Add an event handler for [`EventListener::DoubleClick`]
351    fn on_double_click(
352        self,
353        action: impl Fn(&Event) -> EventPropagation + 'static,
354    ) -> Self::Intermediate {
355        self.on_event(EventListener::DoubleClick, action)
356    }
357
358    /// Add an event handler for [`EventListener::DoubleClick`]. This event will be handled with
359    /// the given handler and the event will continue propagating.
360    fn on_double_click_cont(self, action: impl Fn(&Event) + 'static) -> Self::Intermediate {
361        self.on_double_click(move |e| {
362            action(e);
363            EventPropagation::Continue
364        })
365    }
366
367    /// Add an event handler for [`EventListener::DoubleClick`]. This event will be handled with
368    /// the given handler and the event will stop propagating.
369    fn on_double_click_stop(self, action: impl Fn(&Event) + 'static) -> Self::Intermediate {
370        self.on_double_click(move |e| {
371            action(e);
372            EventPropagation::Stop
373        })
374    }
375
376    /// Add an event handler for [`EventListener::SecondaryClick`]. This is most often the "Right" click.
377    fn on_secondary_click(
378        self,
379        action: impl Fn(&Event) -> EventPropagation + 'static,
380    ) -> Self::Intermediate {
381        self.on_event(EventListener::SecondaryClick, action)
382    }
383
384    /// Add an event handler for [`EventListener::SecondaryClick`]. This is most often the "Right" click.
385    /// This event will be handled with the given handler and the event will continue propagating.
386    fn on_secondary_click_cont(self, action: impl Fn(&Event) + 'static) -> Self::Intermediate {
387        self.on_secondary_click(move |e| {
388            action(e);
389            EventPropagation::Continue
390        })
391    }
392
393    /// Add an event handler for [`EventListener::SecondaryClick`]. This is most often the "Right" click.
394    /// This event will be handled with the given handler and the event will stop propagating.
395    fn on_secondary_click_stop(self, action: impl Fn(&Event) + 'static) -> Self::Intermediate {
396        self.on_secondary_click(move |e| {
397            action(e);
398            EventPropagation::Stop
399        })
400    }
401
402    /// Adds an event handler for resize events for this view.
403    ///
404    /// # Reactivity
405    /// The action will be called whenever the view is resized but will not rerun automatically in response to signal changes
406    fn on_resize(self, action: impl Fn(Rect) + 'static) -> Self::Intermediate {
407        let intermediate = self.into_intermediate();
408        let id = intermediate.view_id();
409        let state = id.state();
410        state.borrow_mut().add_resize_listener(Rc::new(action));
411        intermediate
412    }
413
414    /// Adds an event handler for move events for this view.
415    ///
416    /// # Reactivity
417    /// The action will be called whenever the view is moved but will not rerun automatically in response to signal changes
418    fn on_move(self, action: impl Fn(Point) + 'static) -> Self::Intermediate {
419        let intermediate = self.into_intermediate();
420        let id = intermediate.view_id();
421        let state = id.state();
422        state.borrow_mut().add_move_listener(Rc::new(action));
423        intermediate
424    }
425
426    /// Adds an event handler for cleanup events for this view.
427    ///
428    /// The cleanup event occurs when the view is removed from the view tree.
429    ///
430    /// # Reactivity
431    /// The action will be called when the view is removed from the view tree but will not rerun automatically in response to signal changes
432    fn on_cleanup(self, action: impl Fn() + 'static) -> Self::Intermediate {
433        let intermediate = self.into_intermediate();
434        let id = intermediate.view_id();
435        let state = id.state();
436        state.borrow_mut().add_cleanup_listener(Rc::new(action));
437        intermediate
438    }
439
440    /// Add an animation to the view.
441    ///
442    /// You can add more than one animation to a view and all of them can be active at the same time.
443    ///
444    /// See the [`Animation`] struct for more information on how to create animations.
445    ///
446    /// # Reactivity
447    /// The animation function will be updated in response to signal changes in the function. The behavior is the same as the [`Decorators::style`] method.
448    fn animation(self, animation: impl Fn(Animation) -> Animation + 'static) -> Self::Intermediate {
449        let intermediate = self.into_intermediate();
450        let view_id = intermediate.view_id();
451        let state = view_id.state();
452
453        let offset = state.borrow_mut().animations.next_offset();
454        let initial_animation = UpdaterEffect::new(
455            move || animation(Animation::new()),
456            move |animation| {
457                view_id.update_animation(offset, animation);
458            },
459        );
460        for effect_state in &initial_animation.effect_states {
461            effect_state.update(|stack| stack.push((view_id, offset)));
462        }
463
464        state.borrow_mut().animations.push(initial_animation);
465
466        intermediate
467    }
468
469    /// Clear the focus from the window.
470    ///
471    /// # Reactivity
472    /// The when function is reactive and will rereun in response to any signal changes in the function.
473    fn clear_focus(self, when: impl Fn() + 'static) -> Self::Intermediate {
474        let intermediate = self.into_intermediate();
475        let id = intermediate.view_id();
476        Effect::new(move |_| {
477            when();
478            id.clear_focus();
479        });
480        intermediate
481    }
482
483    /// Request that this view gets the focus for the window.
484    ///
485    /// # Reactivity
486    /// The when function is reactive and will rereun in response to any signal changes in the function.
487    fn request_focus(self, when: impl Fn() + 'static) -> Self::Intermediate {
488        let intermediate = self.into_intermediate();
489        let id = intermediate.view_id();
490        Effect::new(move |_| {
491            when();
492            id.request_focus();
493        });
494        intermediate
495    }
496
497    /// Set the window scale factor.
498    ///
499    /// This internally calls the [`crate::action::set_window_scale`] function.
500    ///
501    /// # Reactivity
502    /// The scale function is reactive and will rereun in response to any signal changes in the function.
503    fn window_scale(self, scale_fn: impl Fn() -> f64 + 'static) -> Self::Intermediate {
504        let intermediate = self.into_intermediate();
505        Effect::new(move |_| {
506            let window_scale = scale_fn();
507            set_window_scale(window_scale);
508        });
509        intermediate
510    }
511
512    /// Set the window title.
513    ///
514    /// This internally calls the [`crate::action::set_window_title`] function.
515    ///
516    /// # Reactivity
517    /// The title function is reactive and will rereun in response to any signal changes in the function.
518    fn window_title(self, title_fn: impl Fn() -> String + 'static) -> Self::Intermediate {
519        let intermediate = self.into_intermediate();
520        Effect::new(move |_| {
521            let window_title = title_fn();
522            set_window_title(window_title);
523        });
524        intermediate
525    }
526
527    /// Set the system window menu
528    ///
529    /// This internally calls the [`crate::action::set_window_menu`] function.
530    ///
531    /// Platform support:
532    /// - Windows: No
533    /// - macOS: Yes (not currently implemented)
534    /// - Linux: No
535    /// - wasm32: No
536    ///
537    /// # Reactivity
538    /// The menu function is reactive and will rereun in response to any signal changes in the function.
539    #[cfg(not(target_arch = "wasm32"))]
540    fn window_menu(self, menu_fn: impl Fn() -> Menu + 'static) -> Self::Intermediate {
541        let intermediate = self.into_intermediate();
542        Effect::new(move |_| {
543            let menu = menu_fn();
544            crate::action::set_window_menu(menu);
545        });
546        intermediate
547    }
548
549    /// Adds a secondary-click context menu to the view, which opens at the mouse position.
550    ///
551    /// # Reactivity
552    /// The menu function is not reactive and will not rerun automatically in response to signal changes while the menu is showing and will only update the menu items each time that it is created
553    fn context_menu(self, menu: impl Fn() -> Menu + 'static) -> Self::Intermediate {
554        let intermediate = self.into_intermediate();
555        let id = intermediate.view_id();
556        id.update_context_menu(menu);
557        intermediate
558    }
559
560    /// Adds a primary-click context menu, which opens below the view.
561    ///
562    /// # Reactivity
563    /// The menu function is not reactive and will not rerun automatically in response to signal changes while the menu is showing and will only update the menu items each time that it is created
564    fn popout_menu(self, menu: impl Fn() -> Menu + 'static) -> Self::Intermediate {
565        let intermediate = self.into_intermediate();
566        let id = intermediate.view_id();
567        id.update_popout_menu(menu);
568        intermediate
569    }
570}
571
572/// Blanket implementation for all [`IntoView`] types.
573impl<T: IntoView> Decorators for T {}