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