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.
16#[allow(missing_debug_implementations)]
17pub struct Float<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
18where
19    Theme: Catalog,
20{
21    content: Element<'a, Message, Theme, Renderer>,
22    scale: f32,
23    translate: Option<Box<dyn Fn(Rectangle, Rectangle) -> Vector + 'a>>,
24    class: Theme::Class<'a>,
25}
26
27impl<'a, Message, Theme, Renderer> Float<'a, Message, Theme, Renderer>
28where
29    Theme: Catalog,
30{
31    /// Creates a new [`Float`] widget with the given content.
32    pub fn new(
33        content: impl Into<Element<'a, Message, Theme, Renderer>>,
34    ) -> Self {
35        Self {
36            content: content.into(),
37            scale: 1.0,
38            translate: None,
39            class: Theme::default(),
40        }
41    }
42
43    /// Sets the scale to be applied to the contents of the [`Float`].
44    pub fn scale(mut self, scale: f32) -> Self {
45        self.scale = scale;
46        self
47    }
48
49    /// Sets the translation logic to be applied to the contents of the [`Float`].
50    ///
51    /// The logic takes the original (non-scaled) bounds of the contents and the
52    /// viewport bounds. These bounds can be useful to ensure the floating elements
53    /// always stay on screen.
54    pub fn translate(
55        mut self,
56        translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a,
57    ) -> Self {
58        self.translate = Some(Box::new(translate));
59        self
60    }
61
62    /// Sets the style of the [`Float`].
63    #[must_use]
64    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
65    where
66        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
67    {
68        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
69        self
70    }
71
72    /// Sets the style class of the [`Float`].
73    #[cfg(feature = "advanced")]
74    #[must_use]
75    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
76        self.class = class.into();
77        self
78    }
79
80    fn is_floating(&self, bounds: Rectangle, viewport: Rectangle) -> bool {
81        self.scale > 1.0
82            || self.translate.as_ref().is_some_and(|translate| {
83                translate(bounds, viewport) != Vector::ZERO
84            })
85    }
86}
87
88impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
89    for Float<'_, Message, Theme, Renderer>
90where
91    Theme: Catalog,
92    Renderer: core::Renderer,
93{
94    fn tag(&self) -> tree::Tag {
95        self.content.as_widget().tag()
96    }
97
98    fn state(&self) -> tree::State {
99        self.content.as_widget().state()
100    }
101
102    fn children(&self) -> Vec<tree::Tree> {
103        self.content.as_widget().children()
104    }
105
106    fn diff(&self, tree: &mut widget::Tree) {
107        self.content.as_widget().diff(tree);
108    }
109
110    fn size(&self) -> Size<Length> {
111        self.content.as_widget().size()
112    }
113
114    fn size_hint(&self) -> Size<Length> {
115        self.content.as_widget().size_hint()
116    }
117
118    fn layout(
119        &self,
120        tree: &mut widget::Tree,
121        renderer: &Renderer,
122        limits: &layout::Limits,
123    ) -> layout::Node {
124        self.content.as_widget().layout(tree, renderer, limits)
125    }
126
127    fn update(
128        &mut self,
129        state: &mut widget::Tree,
130        event: &Event,
131        layout: Layout<'_>,
132        cursor: mouse::Cursor,
133        renderer: &Renderer,
134        clipboard: &mut dyn Clipboard,
135        shell: &mut Shell<'_, Message>,
136        viewport: &Rectangle,
137    ) {
138        if self.is_floating(layout.bounds(), *viewport) {
139            return;
140        }
141
142        self.content.as_widget_mut().update(
143            state, event, layout, cursor, renderer, clipboard, shell, viewport,
144        );
145    }
146
147    fn draw(
148        &self,
149        tree: &widget::Tree,
150        renderer: &mut Renderer,
151        theme: &Theme,
152        style: &renderer::Style,
153        layout: Layout<'_>,
154        cursor: mouse::Cursor,
155        viewport: &Rectangle,
156    ) {
157        if self.is_floating(layout.bounds(), *viewport) {
158            return;
159        }
160
161        {
162            let style = theme.style(&self.class);
163
164            if style.shadow.color.a > 0.0 {
165                renderer.fill_quad(
166                    renderer::Quad {
167                        bounds: layout.bounds().shrink(1.0),
168                        shadow: style.shadow,
169                        border: border::rounded(style.shadow_border_radius),
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        &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()
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                            },
336                            style.shadow.color,
337                        );
338                    }
339                }
340
341                self.float.content.as_widget().draw(
342                    self.state,
343                    renderer,
344                    theme,
345                    style,
346                    self.layout,
347                    cursor * inverse,
348                    &(self.viewport * inverse),
349                );
350            });
351        });
352    }
353
354    fn mouse_interaction(
355        &self,
356        layout: Layout<'_>,
357        cursor: mouse::Cursor,
358        renderer: &Renderer,
359    ) -> mouse::Interaction {
360        if !cursor.is_over(layout.bounds()) {
361            return mouse::Interaction::None;
362        }
363
364        let inverse = self.transformation.inverse();
365
366        self.float.content.as_widget().mouse_interaction(
367            self.state,
368            self.layout,
369            cursor * inverse,
370            &(self.viewport * inverse),
371            renderer,
372        )
373    }
374
375    fn index(&self) -> f32 {
376        self.float.scale * 0.5
377    }
378
379    fn overlay<'a>(
380        &'a mut self,
381        _layout: Layout<'_>,
382        renderer: &Renderer,
383    ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
384        self.float.content.as_widget_mut().overlay(
385            self.state,
386            self.layout,
387            renderer,
388            &(self.viewport * self.transformation.inverse()),
389            self.transformation.translation(),
390        )
391    }
392}
393
394/// The theme catalog of a [`Float`].
395///
396/// All themes that can be used with [`Float`]
397/// must implement this trait.
398pub trait Catalog {
399    /// The item class of the [`Catalog`].
400    type Class<'a>;
401
402    /// The default class produced by the [`Catalog`].
403    fn default<'a>() -> Self::Class<'a>;
404
405    /// The [`Style`] of a class with the given status.
406    fn style(&self, class: &Self::Class<'_>) -> Style;
407}
408
409/// A styling function for a [`Float`].
410pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
411
412impl Catalog for crate::Theme {
413    type Class<'a> = StyleFn<'a, Self>;
414
415    fn default<'a>() -> Self::Class<'a> {
416        Box::new(|_| Style::default())
417    }
418
419    fn style(&self, class: &Self::Class<'_>) -> Style {
420        class(self)
421    }
422}
423
424/// The style of a [`Float`].
425#[derive(Debug, Clone, Copy, PartialEq, Default)]
426pub struct Style {
427    /// The [`Shadow`] of the [`Float`].
428    pub shadow: Shadow,
429    /// The border radius of the shadow.
430    pub shadow_border_radius: border::Radius,
431}