iced_widget/
float.rs

1//! Make elements float!
2use crate::core;
3use crate::core::border;
4use crate::core::layout;
5use crate::core::mouse;
6use crate::core::overlay;
7use crate::core::renderer;
8use crate::core::widget;
9use crate::core::widget::tree;
10use crate::core::{
11    Clipboard, Element, Event, Layout, Length, Rectangle, Shadow, Shell, Size,
12    Transformation, Vector, Widget,
13};
14
15/// A widget that can make its contents float over other widgets.
16pub struct Float<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
17where
18    Theme: Catalog,
19{
20    content: Element<'a, Message, Theme, Renderer>,
21    scale: f32,
22    translate: Option<Box<dyn Fn(Rectangle, Rectangle) -> Vector + 'a>>,
23    class: Theme::Class<'a>,
24}
25
26impl<'a, Message, Theme, Renderer> Float<'a, Message, Theme, Renderer>
27where
28    Theme: Catalog,
29{
30    /// Creates a new [`Float`] widget with the given content.
31    pub fn new(
32        content: impl Into<Element<'a, Message, Theme, Renderer>>,
33    ) -> Self {
34        Self {
35            content: content.into(),
36            scale: 1.0,
37            translate: None,
38            class: Theme::default(),
39        }
40    }
41
42    /// Sets the scale to be applied to the contents of the [`Float`].
43    pub fn scale(mut self, scale: f32) -> Self {
44        self.scale = scale;
45        self
46    }
47
48    /// Sets the translation logic to be applied to the contents of the [`Float`].
49    ///
50    /// The logic takes the original (non-scaled) bounds of the contents and the
51    /// viewport bounds. These bounds can be useful to ensure the floating elements
52    /// always stay on screen.
53    pub fn translate(
54        mut self,
55        translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a,
56    ) -> Self {
57        self.translate = Some(Box::new(translate));
58        self
59    }
60
61    /// Sets the style of the [`Float`].
62    #[must_use]
63    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
64    where
65        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
66    {
67        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
68        self
69    }
70
71    /// Sets the style class of the [`Float`].
72    #[cfg(feature = "advanced")]
73    #[must_use]
74    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
75        self.class = class.into();
76        self
77    }
78
79    fn is_floating(&self, bounds: Rectangle, viewport: Rectangle) -> bool {
80        self.scale > 1.0
81            || self.translate.as_ref().is_some_and(|translate| {
82                translate(bounds, viewport) != Vector::ZERO
83            })
84    }
85}
86
87impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
88    for Float<'_, Message, Theme, Renderer>
89where
90    Theme: Catalog,
91    Renderer: core::Renderer,
92{
93    fn tag(&self) -> tree::Tag {
94        self.content.as_widget().tag()
95    }
96
97    fn state(&self) -> tree::State {
98        self.content.as_widget().state()
99    }
100
101    fn children(&self) -> Vec<tree::Tree> {
102        self.content.as_widget().children()
103    }
104
105    fn diff(&self, tree: &mut widget::Tree) {
106        self.content.as_widget().diff(tree);
107    }
108
109    fn size(&self) -> Size<Length> {
110        self.content.as_widget().size()
111    }
112
113    fn size_hint(&self) -> Size<Length> {
114        self.content.as_widget().size_hint()
115    }
116
117    fn layout(
118        &mut self,
119        tree: &mut widget::Tree,
120        renderer: &Renderer,
121        limits: &layout::Limits,
122    ) -> layout::Node {
123        self.content.as_widget_mut().layout(tree, renderer, limits)
124    }
125
126    fn update(
127        &mut self,
128        state: &mut widget::Tree,
129        event: &Event,
130        layout: Layout<'_>,
131        cursor: mouse::Cursor,
132        renderer: &Renderer,
133        clipboard: &mut dyn Clipboard,
134        shell: &mut Shell<'_, Message>,
135        viewport: &Rectangle,
136    ) {
137        if self.is_floating(layout.bounds(), *viewport) {
138            return;
139        }
140
141        self.content.as_widget_mut().update(
142            state, event, layout, cursor, renderer, clipboard, shell, viewport,
143        );
144    }
145
146    fn draw(
147        &self,
148        tree: &widget::Tree,
149        renderer: &mut Renderer,
150        theme: &Theme,
151        style: &renderer::Style,
152        layout: Layout<'_>,
153        cursor: mouse::Cursor,
154        viewport: &Rectangle,
155    ) {
156        if self.is_floating(layout.bounds(), *viewport) {
157            return;
158        }
159
160        {
161            let style = theme.style(&self.class);
162
163            if style.shadow.color.a > 0.0 {
164                renderer.fill_quad(
165                    renderer::Quad {
166                        bounds: layout.bounds().shrink(1.0),
167                        shadow: style.shadow,
168                        border: border::rounded(style.shadow_border_radius),
169                        snap: false,
170                    },
171                    style.shadow.color,
172                );
173            }
174        }
175
176        self.content
177            .as_widget()
178            .draw(tree, renderer, theme, style, layout, cursor, viewport);
179    }
180
181    fn mouse_interaction(
182        &self,
183        state: &widget::Tree,
184        layout: Layout<'_>,
185        cursor: mouse::Cursor,
186        viewport: &Rectangle,
187        renderer: &Renderer,
188    ) -> mouse::Interaction {
189        if self.is_floating(layout.bounds(), *viewport) {
190            return mouse::Interaction::None;
191        }
192
193        self.content
194            .as_widget()
195            .mouse_interaction(state, layout, cursor, viewport, renderer)
196    }
197
198    fn operate(
199        &mut self,
200        state: &mut widget::Tree,
201        layout: Layout<'_>,
202        renderer: &Renderer,
203        operation: &mut dyn widget::Operation,
204    ) {
205        self.content
206            .as_widget_mut()
207            .operate(state, layout, renderer, operation);
208    }
209
210    fn overlay<'a>(
211        &'a mut self,
212        state: &'a mut widget::Tree,
213        layout: Layout<'a>,
214        renderer: &Renderer,
215        viewport: &Rectangle,
216        offset: Vector,
217    ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
218        let bounds = layout.bounds();
219
220        let translation = self
221            .translate
222            .as_ref()
223            .map(|translate| translate(bounds + offset, *viewport))
224            .unwrap_or(Vector::ZERO);
225
226        if self.scale > 1.0 || translation != Vector::ZERO {
227            let translation = translation + offset;
228
229            let transformation = Transformation::translate(
230                bounds.x + bounds.width / 2.0 + translation.x,
231                bounds.y + bounds.height / 2.0 + translation.y,
232            ) * Transformation::scale(self.scale)
233                * Transformation::translate(
234                    -bounds.x - bounds.width / 2.0,
235                    -bounds.y - bounds.height / 2.0,
236                );
237
238            Some(overlay::Element::new(Box::new(Overlay {
239                float: self,
240                state,
241                layout,
242                viewport: *viewport,
243                transformation,
244            })))
245        } else {
246            self.content
247                .as_widget_mut()
248                .overlay(state, layout, renderer, viewport, offset)
249        }
250    }
251}
252
253impl<'a, Message, Theme, Renderer> From<Float<'a, Message, Theme, Renderer>>
254    for Element<'a, Message, Theme, Renderer>
255where
256    Message: 'a,
257    Theme: Catalog + 'a,
258    Renderer: core::Renderer + 'a,
259{
260    fn from(float: Float<'a, Message, Theme, Renderer>) -> Self {
261        Element::new(float)
262    }
263}
264
265struct Overlay<'a, 'b, Message, Theme, Renderer>
266where
267    Theme: Catalog,
268{
269    float: &'a mut Float<'b, Message, Theme, Renderer>,
270    state: &'a mut widget::Tree,
271    layout: Layout<'a>,
272    viewport: Rectangle,
273    transformation: Transformation,
274}
275
276impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
277    for Overlay<'_, '_, Message, Theme, Renderer>
278where
279    Theme: Catalog,
280    Renderer: core::Renderer,
281{
282    fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
283        let bounds = self.layout.bounds() * self.transformation;
284
285        layout::Node::new(bounds.size()).move_to(bounds.position())
286    }
287
288    fn update(
289        &mut self,
290        event: &Event,
291        _layout: Layout<'_>,
292        cursor: mouse::Cursor,
293        renderer: &Renderer,
294        clipboard: &mut dyn Clipboard,
295        shell: &mut Shell<'_, Message>,
296    ) {
297        let inverse = self.transformation.inverse();
298
299        self.float.content.as_widget_mut().update(
300            self.state,
301            event,
302            self.layout,
303            cursor * inverse,
304            renderer,
305            clipboard,
306            shell,
307            &(self.viewport * inverse),
308        );
309    }
310
311    fn draw(
312        &self,
313        renderer: &mut Renderer,
314        theme: &Theme,
315        style: &renderer::Style,
316        _layout: Layout<'_>,
317        cursor: mouse::Cursor,
318    ) {
319        let bounds = self.layout.bounds();
320        let inverse = self.transformation.inverse();
321
322        renderer.with_layer(self.viewport, |renderer| {
323            renderer.with_transformation(self.transformation, |renderer| {
324                {
325                    let style = theme.style(&self.float.class);
326
327                    if style.shadow.color.a > 0.0 {
328                        renderer.fill_quad(
329                            renderer::Quad {
330                                bounds: bounds.shrink(1.0),
331                                shadow: style.shadow,
332                                border: border::rounded(
333                                    style.shadow_border_radius,
334                                ),
335                                snap: false,
336                            },
337                            style.shadow.color,
338                        );
339                    }
340                }
341
342                self.float.content.as_widget().draw(
343                    self.state,
344                    renderer,
345                    theme,
346                    style,
347                    self.layout,
348                    cursor * inverse,
349                    &(self.viewport * inverse),
350                );
351            });
352        });
353    }
354
355    fn mouse_interaction(
356        &self,
357        layout: Layout<'_>,
358        cursor: mouse::Cursor,
359        renderer: &Renderer,
360    ) -> mouse::Interaction {
361        if !cursor.is_over(layout.bounds()) {
362            return mouse::Interaction::None;
363        }
364
365        let inverse = self.transformation.inverse();
366
367        self.float.content.as_widget().mouse_interaction(
368            self.state,
369            self.layout,
370            cursor * inverse,
371            &(self.viewport * inverse),
372            renderer,
373        )
374    }
375
376    fn index(&self) -> f32 {
377        self.float.scale * 0.5
378    }
379
380    fn overlay<'a>(
381        &'a mut self,
382        _layout: Layout<'_>,
383        renderer: &Renderer,
384    ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
385        self.float.content.as_widget_mut().overlay(
386            self.state,
387            self.layout,
388            renderer,
389            &(self.viewport * self.transformation.inverse()),
390            self.transformation.translation(),
391        )
392    }
393}
394
395/// The theme catalog of a [`Float`].
396///
397/// All themes that can be used with [`Float`]
398/// must implement this trait.
399pub trait Catalog {
400    /// The item class of the [`Catalog`].
401    type Class<'a>;
402
403    /// The default class produced by the [`Catalog`].
404    fn default<'a>() -> Self::Class<'a>;
405
406    /// The [`Style`] of a class with the given status.
407    fn style(&self, class: &Self::Class<'_>) -> Style;
408}
409
410/// A styling function for a [`Float`].
411pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
412
413impl Catalog for crate::Theme {
414    type Class<'a> = StyleFn<'a, Self>;
415
416    fn default<'a>() -> Self::Class<'a> {
417        Box::new(|_| Style::default())
418    }
419
420    fn style(&self, class: &Self::Class<'_>) -> Style {
421        class(self)
422    }
423}
424
425/// The style of a [`Float`].
426#[derive(Debug, Clone, Copy, PartialEq, Default)]
427pub struct Style {
428    /// The [`Shadow`] of the [`Float`].
429    pub shadow: Shadow,
430    /// The border radius of the shadow.
431    pub shadow_border_radius: border::Radius,
432}