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                        snap: false,
171                    },
172                    style.shadow.color,
173                );
174            }
175        }
176
177        self.content
178            .as_widget()
179            .draw(tree, renderer, theme, style, layout, cursor, viewport);
180    }
181
182    fn mouse_interaction(
183        &self,
184        state: &widget::Tree,
185        layout: Layout<'_>,
186        cursor: mouse::Cursor,
187        viewport: &Rectangle,
188        renderer: &Renderer,
189    ) -> mouse::Interaction {
190        if self.is_floating(layout.bounds(), *viewport) {
191            return mouse::Interaction::None;
192        }
193
194        self.content
195            .as_widget()
196            .mouse_interaction(state, layout, cursor, viewport, renderer)
197    }
198
199    fn operate(
200        &self,
201        state: &mut widget::Tree,
202        layout: Layout<'_>,
203        renderer: &Renderer,
204        operation: &mut dyn widget::Operation,
205    ) {
206        self.content
207            .as_widget()
208            .operate(state, layout, renderer, operation);
209    }
210
211    fn overlay<'a>(
212        &'a mut self,
213        state: &'a mut widget::Tree,
214        layout: Layout<'a>,
215        renderer: &Renderer,
216        viewport: &Rectangle,
217        offset: Vector,
218    ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
219        let bounds = layout.bounds();
220
221        let translation = self
222            .translate
223            .as_ref()
224            .map(|translate| translate(bounds + offset, *viewport))
225            .unwrap_or(Vector::ZERO);
226
227        if self.scale > 1.0 || translation != Vector::ZERO {
228            let translation = translation + offset;
229
230            let transformation = Transformation::translate(
231                bounds.x + bounds.width / 2.0 + translation.x,
232                bounds.y + bounds.height / 2.0 + translation.y,
233            ) * Transformation::scale(self.scale)
234                * Transformation::translate(
235                    -bounds.x - bounds.width / 2.0,
236                    -bounds.y - bounds.height / 2.0,
237                );
238
239            Some(overlay::Element::new(Box::new(Overlay {
240                float: self,
241                state,
242                layout,
243                viewport: *viewport,
244                transformation,
245            })))
246        } else {
247            self.content
248                .as_widget_mut()
249                .overlay(state, layout, renderer, viewport, offset)
250        }
251    }
252}
253
254impl<'a, Message, Theme, Renderer> From<Float<'a, Message, Theme, Renderer>>
255    for Element<'a, Message, Theme, Renderer>
256where
257    Message: 'a,
258    Theme: Catalog + 'a,
259    Renderer: core::Renderer + 'a,
260{
261    fn from(float: Float<'a, Message, Theme, Renderer>) -> Self {
262        Element::new(float)
263    }
264}
265
266struct Overlay<'a, 'b, Message, Theme, Renderer>
267where
268    Theme: Catalog,
269{
270    float: &'a mut Float<'b, Message, Theme, Renderer>,
271    state: &'a mut widget::Tree,
272    layout: Layout<'a>,
273    viewport: Rectangle,
274    transformation: Transformation,
275}
276
277impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
278    for Overlay<'_, '_, Message, Theme, Renderer>
279where
280    Theme: Catalog,
281    Renderer: core::Renderer,
282{
283    fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
284        let bounds = self.layout.bounds() * self.transformation;
285
286        layout::Node::new(bounds.size()).move_to(bounds.position())
287    }
288
289    fn update(
290        &mut self,
291        event: &Event,
292        _layout: Layout<'_>,
293        cursor: mouse::Cursor,
294        renderer: &Renderer,
295        clipboard: &mut dyn Clipboard,
296        shell: &mut Shell<'_, Message>,
297    ) {
298        let inverse = self.transformation.inverse();
299
300        self.float.content.as_widget_mut().update(
301            self.state,
302            event,
303            self.layout,
304            cursor * inverse,
305            renderer,
306            clipboard,
307            shell,
308            &(self.viewport * inverse),
309        );
310    }
311
312    fn draw(
313        &self,
314        renderer: &mut Renderer,
315        theme: &Theme,
316        style: &renderer::Style,
317        _layout: Layout<'_>,
318        cursor: mouse::Cursor,
319    ) {
320        let bounds = self.layout.bounds();
321        let inverse = self.transformation.inverse();
322
323        renderer.with_layer(self.viewport, |renderer| {
324            renderer.with_transformation(self.transformation, |renderer| {
325                {
326                    let style = theme.style(&self.float.class);
327
328                    if style.shadow.color.a > 0.0 {
329                        renderer.fill_quad(
330                            renderer::Quad {
331                                bounds: bounds.shrink(1.0),
332                                shadow: style.shadow,
333                                border: border::rounded(
334                                    style.shadow_border_radius,
335                                ),
336                                snap: false,
337                            },
338                            style.shadow.color,
339                        );
340                    }
341                }
342
343                self.float.content.as_widget().draw(
344                    self.state,
345                    renderer,
346                    theme,
347                    style,
348                    self.layout,
349                    cursor * inverse,
350                    &(self.viewport * inverse),
351                );
352            });
353        });
354    }
355
356    fn mouse_interaction(
357        &self,
358        layout: Layout<'_>,
359        cursor: mouse::Cursor,
360        renderer: &Renderer,
361    ) -> mouse::Interaction {
362        if !cursor.is_over(layout.bounds()) {
363            return mouse::Interaction::None;
364        }
365
366        let inverse = self.transformation.inverse();
367
368        self.float.content.as_widget().mouse_interaction(
369            self.state,
370            self.layout,
371            cursor * inverse,
372            &(self.viewport * inverse),
373            renderer,
374        )
375    }
376
377    fn index(&self) -> f32 {
378        self.float.scale * 0.5
379    }
380
381    fn overlay<'a>(
382        &'a mut self,
383        _layout: Layout<'_>,
384        renderer: &Renderer,
385    ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
386        self.float.content.as_widget_mut().overlay(
387            self.state,
388            self.layout,
389            renderer,
390            &(self.viewport * self.transformation.inverse()),
391            self.transformation.translation(),
392        )
393    }
394}
395
396/// The theme catalog of a [`Float`].
397///
398/// All themes that can be used with [`Float`]
399/// must implement this trait.
400pub trait Catalog {
401    /// The item class of the [`Catalog`].
402    type Class<'a>;
403
404    /// The default class produced by the [`Catalog`].
405    fn default<'a>() -> Self::Class<'a>;
406
407    /// The [`Style`] of a class with the given status.
408    fn style(&self, class: &Self::Class<'_>) -> Style;
409}
410
411/// A styling function for a [`Float`].
412pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
413
414impl Catalog for crate::Theme {
415    type Class<'a> = StyleFn<'a, Self>;
416
417    fn default<'a>() -> Self::Class<'a> {
418        Box::new(|_| Style::default())
419    }
420
421    fn style(&self, class: &Self::Class<'_>) -> Style {
422        class(self)
423    }
424}
425
426/// The style of a [`Float`].
427#[derive(Debug, Clone, Copy, PartialEq, Default)]
428pub struct Style {
429    /// The [`Shadow`] of the [`Float`].
430    pub shadow: Shadow,
431    /// The border radius of the shadow.
432    pub shadow_border_radius: border::Radius,
433}