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 {}