Skip to main content

iced_widget_kit/selector_bar/
item.rs

1use iced_core::{
2    Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Rectangle,
3    Shadow, Shell, Size, Theme, Vector, Widget,
4    layout::{self, Limits, Node},
5    mouse::{self, Cursor, Interaction},
6    overlay,
7    renderer::Quad,
8    touch,
9    widget::{
10        Operation, Tree,
11        tree::{self, Tag},
12    },
13    window,
14};
15
16const INDICATOR_HEIGHT: f32 = 2.0;
17
18pub struct Item<'a, Id, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer>
19where
20    Theme: Catalog,
21{
22    width: Length,
23    height: Length,
24    pub(super) id: Id,
25    content: Element<'a, Message, Theme, Renderer>,
26    pub(super) padding: Padding,
27    clip: bool,
28    pub(super) class: Theme::Class<'a>,
29    pub(super) status: Option<Status>,
30}
31
32impl<'a, Id, Message, Theme, Renderer> Item<'a, Id, Message, Theme, Renderer>
33where
34    Theme: Catalog,
35{
36    /// Creates a new [`Item`] which the provided content.
37    pub fn new(id: Id, content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
38        Self {
39            width: Length::Shrink,
40            height: 32.into(),
41            id,
42            content: content.into(),
43            padding: [0.0, 10.0].into(),
44            clip: true,
45            class: Theme::default(),
46            status: None,
47        }
48    }
49
50    /// Sets the width of the [`SelectorBar`](super::SelectorBar).
51    #[must_use]
52    pub fn width(mut self, width: impl Into<Length>) -> Self {
53        self.width = width.into();
54        self
55    }
56
57    /// Sets the height of the [`SelectorBar`](super::SelectorBar).
58    #[must_use]
59    pub fn height(mut self, height: impl Into<Length>) -> Self {
60        self.height = height.into();
61        self
62    }
63
64    /// Sets the spacing between [`Item`]s.
65    #[must_use]
66    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
67        self.padding = padding.into();
68        self
69    }
70
71    /// Sets whether the contents of the [`Item`] should be clipped on
72    /// overflow.
73    #[must_use]
74    pub fn clip(mut self, clip: bool) -> Self {
75        self.clip = clip;
76        self
77    }
78
79    pub(crate) fn is_hovered(&self) -> bool {
80        self.status
81            .is_some_and(|status| matches!(status, Status::Hovered))
82    }
83
84    pub(crate) fn is_pressed(&self) -> bool {
85        self.status
86            .is_some_and(|status| matches!(status, Status::Pressed))
87    }
88}
89
90#[derive(Debug, Default)]
91struct State {
92    is_pressed: bool,
93}
94
95impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
96    for Item<'a, Id, Message, Theme, Renderer>
97where
98    Theme: Catalog,
99    Renderer: iced_core::Renderer,
100{
101    fn tag(&self) -> Tag {
102        Tag::of::<State>()
103    }
104
105    fn state(&self) -> tree::State {
106        tree::State::new(State::default())
107    }
108
109    fn children(&self) -> Vec<Tree> {
110        vec![Tree::new(&self.content)]
111    }
112
113    fn diff(&self, tree: &mut Tree) {
114        tree.diff_children(std::slice::from_ref(&self.content));
115    }
116
117    fn size(&self) -> Size<Length> {
118        Size::new(self.width, self.height)
119    }
120
121    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
122        layout::padded(limits, self.width, self.height, self.padding, |limits| {
123            self.content
124                .as_widget_mut()
125                .layout(&mut tree.children[0], renderer, limits)
126        })
127    }
128
129    fn operate(
130        &mut self,
131        tree: &mut Tree,
132        layout: Layout<'_>,
133        renderer: &Renderer,
134        operation: &mut dyn Operation,
135    ) {
136        operation.container(None, layout.bounds());
137
138        operation.traverse(&mut |operation| {
139            self.content.as_widget_mut().operate(
140                &mut tree.children[0],
141                layout.children().next().unwrap(),
142                renderer,
143                operation,
144            );
145        });
146    }
147
148    fn update(
149        &mut self,
150        tree: &mut Tree,
151        event: &Event,
152        layout: Layout<'_>,
153        cursor: Cursor,
154        renderer: &Renderer,
155        clipboard: &mut dyn Clipboard,
156        shell: &mut Shell<'_, Message>,
157        viewport: &Rectangle,
158    ) {
159        self.content.as_widget_mut().update(
160            &mut tree.children[0],
161            event,
162            layout.children().next().unwrap(),
163            cursor,
164            renderer,
165            clipboard,
166            shell,
167            viewport,
168        );
169
170        match event {
171            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
172            | Event::Touch(touch::Event::FingerPressed { .. }) => {
173                if cursor.is_over(layout.bounds()) {
174                    let state = tree.state.downcast_mut::<State>();
175                    state.is_pressed = true;
176                    shell.capture_event();
177                }
178            }
179            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
180            | Event::Touch(touch::Event::FingerLifted { .. }) => {
181                let state = tree.state.downcast_mut::<State>();
182
183                if state.is_pressed {
184                    state.is_pressed = false;
185                    shell.capture_event();
186                }
187            }
188            _ => {}
189        }
190
191        let current_status = if cursor.is_over(layout.bounds()) {
192            let state = tree.state.downcast_mut::<State>();
193
194            if state.is_pressed {
195                Status::Pressed
196            } else {
197                Status::Hovered
198            }
199        } else {
200            Status::Active
201        };
202
203        if let Event::Window(window::Event::RedrawRequested(_)) = event {
204            self.status = Some(current_status);
205        } else if self.status.is_some_and(|status| status != current_status) {
206            shell.request_redraw();
207        }
208    }
209
210    fn mouse_interaction(
211        &self,
212        tree: &Tree,
213        layout: Layout<'_>,
214        cursor: Cursor,
215        viewport: &Rectangle,
216        renderer: &Renderer,
217    ) -> Interaction {
218        self.content.as_widget().mouse_interaction(
219            &tree.children[0],
220            layout.child(0),
221            cursor,
222            viewport,
223            renderer,
224        )
225    }
226
227    fn draw(
228        &self,
229        tree: &Tree,
230        renderer: &mut Renderer,
231        theme: &Theme,
232        style: &iced_core::renderer::Style,
233        layout: Layout<'_>,
234        cursor: Cursor,
235        viewport: &Rectangle,
236    ) {
237        let bounds = if self.clip {
238            layout.bounds().intersection(viewport).unwrap_or(*viewport)
239        } else {
240            *viewport
241        };
242
243        let item_style = theme.style(&self.class, self.status.unwrap_or_default());
244
245        // Draw Item background
246        renderer.fill_quad(
247            Quad {
248                bounds,
249                border: item_style.border,
250                shadow: item_style.shadow,
251                snap: item_style.snap,
252            },
253            item_style.background.unwrap_or(Color::TRANSPARENT.into()),
254        );
255
256        // Draw contents
257        let child_layout = layout.children().next().unwrap();
258
259        self.content.as_widget().draw(
260            &tree.children[0],
261            renderer,
262            theme,
263            style,
264            child_layout,
265            cursor,
266            viewport,
267        );
268
269        // Draw pending Indicator. Active Indicator drawn by SelectorBar.
270        if let Some(status) = &self.status {
271            let style = item_style.pending_indicator;
272            let bounds = child_layout.bounds();
273
274            match status {
275                Status::Active => {}
276                Status::Hovered | Status::Pressed => {
277                    renderer.fill_quad(
278                        Quad {
279                            bounds: Rectangle {
280                                x: bounds.x,
281                                y: bounds.y + bounds.height - INDICATOR_HEIGHT,
282                                width: bounds.width,
283                                height: INDICATOR_HEIGHT,
284                            },
285                            border: style.border,
286                            shadow: style.shadow,
287                            snap: style.snap,
288                        },
289                        style.background.unwrap_or(Color::TRANSPARENT.into()),
290                    );
291                }
292            }
293        }
294    }
295
296    fn overlay<'b>(
297        &'b mut self,
298        tree: &'b mut Tree,
299        layout: Layout<'b>,
300        renderer: &Renderer,
301        viewport: &Rectangle,
302        translation: Vector,
303    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
304        self.content.as_widget_mut().overlay(
305            &mut tree.children[0],
306            layout.child(0),
307            renderer,
308            viewport,
309            translation,
310        )
311    }
312}
313
314#[derive(Clone, Copy, Debug, Default, PartialEq)]
315pub enum Status {
316    #[default]
317    Active,
318    Hovered,
319    Pressed,
320}
321
322#[derive(Debug)]
323pub struct Style {
324    pub background: Option<Background>,
325    pub border: Border,
326    pub shadow: Shadow,
327    pub snap: bool,
328    pub active_indicator: Indicator,
329    pub pending_indicator: Indicator,
330}
331
332#[derive(Debug)]
333pub struct Indicator {
334    pub background: Option<Background>,
335    pub border: Border,
336    pub shadow: Shadow,
337    pub snap: bool,
338}
339
340pub trait Catalog {
341    type Class<'a>;
342
343    fn default<'a>() -> Self::Class<'a>;
344    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
345}
346
347pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
348
349impl Catalog for Theme {
350    type Class<'a> = StyleFn<'a, Self>;
351
352    fn default<'a>() -> Self::Class<'a> {
353        Box::new(default)
354    }
355
356    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
357        class(self, status)
358    }
359}
360
361pub fn default(theme: &Theme, status: Status) -> Style {
362    let palette = theme.extended_palette();
363
364    let active_color = match status {
365        Status::Active => palette.primary.base.color,
366        Status::Hovered => palette.primary.base.color,
367        Status::Pressed => palette.primary.weak.color,
368    };
369
370    let pending_color = match status {
371        Status::Active => palette.secondary.base.color,
372        Status::Hovered => palette.secondary.base.color,
373        Status::Pressed => palette.secondary.weak.color,
374    };
375
376    let border = Border::default();
377
378    Style {
379        background: Some(palette.background.base.color.into()),
380        border,
381        shadow: Shadow::default(),
382        snap: true,
383        active_indicator: Indicator {
384            background: Some(active_color.into()),
385            border,
386            shadow: Shadow::default(),
387            snap: true,
388        },
389        pending_indicator: Indicator {
390            background: Some(pending_color.into()),
391            border,
392            shadow: Shadow::default(),
393            snap: true,
394        },
395    }
396}