Skip to main content

iced_widget_kit/
selector_bar.rs

1//! A [`SelectorBar`] provides single selection from a collection of [`Item`]s highlighted by an
2//! indicator.
3//!
4//! # Example
5//! ```ignore
6//! #[derive(Clone)]
7//! enum Message {
8//!    TabSelected(u32),
9//! }
10//!
11//! fn view(&self) -> Element<'_, Message, Theme> {
12//!     let tabs = (0..5).map(|i| item(i, text!("Tab {}", i + 1)));
13//!     selector_bar(tabs, self.selected_tab, Message::TabSelected).into()
14//! }
15//! ```
16pub mod item;
17
18pub use item::Item;
19
20use iced_core::{
21    Animation, Background, Border, Clipboard, Color, Element, Event, Gradient, Layout, Length,
22    Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
23    animation::Easing,
24    gradient::Linear,
25    layout::{self, Limits, Node},
26    mouse::{Cursor, Interaction},
27    overlay,
28    renderer::Quad,
29    widget::{
30        Tree,
31        tree::{self, Tag},
32    },
33    window,
34};
35use std::time::Instant;
36
37const INDICATOR_HEIGHT: f32 = 2.0;
38
39/// A [`SelectorBar`] provides single selection from a collection of [`Item`]s highlighted by an
40/// indicator.
41///
42/// # Example
43/// ```ignore
44/// #[derive(Clone)]
45/// enum Message {
46///    TabSelected(u32),
47/// }
48///
49/// fn view(&self) -> Element<'_, Message, Theme> {
50///     let tabs = (0..5).map(|i| item(i, text!("Tab {}", i + 1)));
51///     selector_bar(tabs, self.selected_tab, Message::TabSelected).into()
52/// }
53/// ```
54pub struct SelectorBar<'a, Id, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer>
55where
56    Theme: Catalog + item::Catalog,
57{
58    width: Length,
59    height: Length,
60    padding: Padding,
61    items: Vec<Item<'a, Id, Message, Theme, Renderer>>,
62    selected_id: Id,
63    on_select: Box<dyn Fn(Id) -> Message + 'a>,
64    spacing: Pixels,
65    class: <Theme as Catalog>::Class<'a>,
66}
67
68impl<'a, Id, Message, Theme, Renderer> SelectorBar<'a, Id, Message, Theme, Renderer>
69where
70    Id: Eq,
71    Theme: Catalog + item::Catalog,
72{
73    /// Creates a new [`SelectorBar`] which the provided [`Item`]s.
74    pub fn new(
75        items: impl IntoIterator<Item = Item<'a, Id, Message, Theme, Renderer>>,
76        selected_id: Id,
77        on_select: impl Fn(Id) -> Message + 'a,
78    ) -> Self {
79        let items = items.into_iter().collect::<Vec<_>>();
80        assert!(
81            items.iter().any(|item| item.id == selected_id),
82            "selected item ID does not exist"
83        );
84
85        Self {
86            width: Length::Fill,
87            height: Length::Shrink,
88            padding: Padding::ZERO,
89            items,
90            selected_id,
91            on_select: Box::new(on_select),
92            spacing: 0.into(),
93            class: <Theme as Catalog>::default(),
94        }
95    }
96
97    /// Sets the width of the [`SelectorBar`].
98    #[must_use]
99    pub fn width(mut self, width: impl Into<Length>) -> Self {
100        self.width = width.into();
101        self
102    }
103
104    /// Sets the height of the [`SelectorBar`].
105    #[must_use]
106    pub fn height(mut self, height: impl Into<Length>) -> Self {
107        self.height = height.into();
108        self
109    }
110
111    /// Sets the spacing between [`Item`]s.
112    #[must_use]
113    pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
114        self.spacing = amount.into();
115        self
116    }
117
118    /// Sets the [`Padding`] of the [`SelectorBar`].
119    #[must_use]
120    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
121        self.padding = padding.into();
122        self
123    }
124
125    /// Sets the style of the [`SelectorBar`].
126    #[must_use]
127    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
128    where
129        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
130    {
131        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
132        self
133    }
134}
135
136#[derive(Debug)]
137enum IndicatorStatus<Id> {
138    Fixed {
139        current: Id,
140    },
141    Widening {
142        current: Id,
143        animation: Animation<f32>,
144    },
145    Hovered {
146        current: Id,
147    },
148    Narrowing {
149        current: Id,
150        animation: Animation<f32>,
151    },
152    Moving {
153        from: Id,
154        to: Id,
155        animation: Animation<bool>,
156    },
157}
158
159impl<Id> IndicatorStatus<Id>
160where
161    Id: Clone + PartialEq,
162{
163    fn is_animating(&self, at: Instant) -> bool {
164        match self {
165            IndicatorStatus::Fixed { .. } => false,
166            IndicatorStatus::Widening { animation, .. } => animation.is_animating(at),
167            IndicatorStatus::Hovered { .. } => false,
168            IndicatorStatus::Narrowing { animation, .. } => animation.is_animating(at),
169            IndicatorStatus::Moving { animation, .. } => animation.is_animating(at),
170        }
171    }
172
173    fn current_id(&self) -> Id {
174        match self {
175            IndicatorStatus::Fixed { current }
176            | IndicatorStatus::Widening { current, .. }
177            | IndicatorStatus::Hovered { current }
178            | IndicatorStatus::Narrowing { current, .. }
179            | IndicatorStatus::Moving { to: current, .. } => current.clone(),
180        }
181    }
182
183    fn widen(&mut self, from: f32, at: Instant) {
184        *self = IndicatorStatus::Widening {
185            current: self.current_id(),
186            animation: Animation::new(from)
187                .slow()
188                .easing(Easing::EaseOutQuart)
189                .go(1.0, at),
190        };
191    }
192
193    fn narrow(&mut self, from: f32, at: Instant) {
194        *self = IndicatorStatus::Narrowing {
195            current: self.current_id(),
196            animation: Animation::new(from)
197                .slow()
198                .easing(Easing::EaseOutQuart)
199                .go(1.0, at),
200        };
201    }
202
203    fn move_to(&mut self, from: Id, to: Id, at: Instant) {
204        *self = IndicatorStatus::Moving {
205            from,
206            to,
207            animation: Animation::new(false)
208                .very_quick()
209                .easing(Easing::EaseOutQuart)
210                .go(true, at),
211        };
212    }
213}
214
215#[derive(Debug)]
216struct State<Id> {
217    now: Option<Instant>,
218    pressed_item: Option<Id>,
219    indicator_status: IndicatorStatus<Id>,
220}
221
222impl<Id> State<Id> {
223    fn new(current: Id) -> Self {
224        Self {
225            now: None,
226            pressed_item: None,
227            indicator_status: IndicatorStatus::Fixed { current },
228        }
229    }
230}
231
232impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
233    for SelectorBar<'a, Id, Message, Theme, Renderer>
234where
235    Id: 'static + Clone + Default + PartialEq,
236    Theme: Catalog + item::Catalog,
237    Renderer: iced_core::Renderer,
238{
239    fn tag(&self) -> Tag {
240        Tag::of::<State<Id>>()
241    }
242
243    fn state(&self) -> tree::State {
244        tree::State::new(State::<Id>::new(self.selected_id.clone()))
245    }
246
247    fn children(&self) -> Vec<Tree> {
248        self.items
249            .iter()
250            .map(|tab| Tree {
251                tag: tab.tag(),
252                state: tab.state(),
253                children: tab.children(),
254            })
255            .collect()
256    }
257
258    fn diff(&self, tree: &mut Tree) {
259        tree.diff_children_custom(
260            &self.items[..],
261            |tree, item| item.diff(tree),
262            |item| Tree {
263                tag: item.tag(),
264                state: item.state(),
265                children: item.children(),
266            },
267        );
268    }
269
270    fn size(&self) -> Size<Length> {
271        Size::new(self.width, self.height)
272    }
273
274    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
275        layout::padded(limits, self.width, self.height, self.padding, |limits| {
276            let (nodes, items_size) = self.items.iter_mut().zip(&mut tree.children).fold(
277                (vec![], Size::ZERO),
278                |(mut nodes, total_size), (tab, tree)| {
279                    let node = tab
280                        .layout(tree, renderer, limits)
281                        .move_to([total_size.width, 0.0]);
282
283                    let width = total_size.width + node.size().width + self.spacing.0;
284                    let height = total_size.height.max(node.size().height);
285                    let total_size = Size::new(width, height);
286
287                    nodes.push(node);
288                    (nodes, total_size)
289                },
290            );
291
292            Node::with_children(items_size, nodes)
293        })
294    }
295
296    fn operate(
297        &mut self,
298        tree: &mut Tree,
299        layout: Layout<'_>,
300        renderer: &Renderer,
301        operation: &mut dyn iced_core::widget::Operation,
302    ) {
303        operation.container(None, layout.bounds());
304
305        operation.traverse(&mut |operation| {
306            self.items
307                .iter_mut()
308                .zip(&mut tree.children)
309                .zip(layout.child(0).children())
310                .for_each(|((item, state), layout)| {
311                    item.operate(state, layout, renderer, operation);
312                });
313        });
314    }
315
316    fn update(
317        &mut self,
318        tree: &mut Tree,
319        event: &Event,
320        layout: Layout<'_>,
321        cursor: Cursor,
322        renderer: &Renderer,
323        clipboard: &mut dyn Clipboard,
324        shell: &mut Shell<'_, Message>,
325        viewport: &Rectangle,
326    ) {
327        let state = tree.state.downcast_mut::<State<Id>>();
328
329        for ((item, tree), layout) in &mut self
330            .items
331            .iter_mut()
332            .zip(&mut tree.children)
333            .zip(layout.child(0).children())
334        {
335            item.update(
336                tree, event, layout, cursor, renderer, clipboard, shell, viewport,
337            );
338
339            if shell.is_event_captured() {
340                return;
341            }
342        }
343
344        // Check if any Item has been pressed to publish Message
345        if let Some(item) = self.items.iter().find(|item| item.is_pressed()) {
346            state.pressed_item = Some(item.id.clone());
347        } else if let Some(item) = self.items.iter().find(|item| item.is_hovered()) {
348            if state.pressed_item.as_ref().is_some_and(|id| *id == item.id) {
349                shell.publish((self.on_select)(item.id.clone()));
350            }
351
352            state.pressed_item = None;
353        } else {
354            state.pressed_item = None;
355        }
356
357        // In case SelectorBar is completely different and current ID stored in
358        // IndicatorStatus does not exist
359        if !self
360            .items
361            .iter()
362            .any(|item| item.id == state.indicator_status.current_id())
363        {
364            state.indicator_status = IndicatorStatus::Fixed {
365                current: self.selected_id.clone(),
366            };
367
368            return;
369        }
370
371        if let Event::Window(window::Event::RedrawRequested(now)) = event {
372            let hovered_item = self.items.iter().find(|item| {
373                item.status
374                    .is_some_and(|status| matches!(status, item::Status::Hovered))
375            });
376
377            let previous_id = state.indicator_status.current_id();
378
379            // Start moving indicator if selected item has changed
380            if previous_id != self.selected_id {
381                state
382                    .indicator_status
383                    .move_to(previous_id.clone(), self.selected_id.clone(), *now);
384            } else {
385                match &mut state.indicator_status {
386                    IndicatorStatus::Fixed { current } => {
387                        if hovered_item.is_some_and(|item| item.id == *current) {
388                            state.indicator_status.widen(0.0, *now);
389                        }
390                    }
391                    IndicatorStatus::Widening { current, animation } => {
392                        if hovered_item.is_none_or(|item| item.id != *current) {
393                            let value_now = animation.interpolate_with(|v| v, *now);
394                            let from = 1.0 - value_now;
395                            state.indicator_status.narrow(from, *now);
396                        } else if !animation.is_animating(*now) {
397                            state.indicator_status = IndicatorStatus::Hovered {
398                                current: current.clone(),
399                            };
400                        }
401                    }
402                    IndicatorStatus::Hovered { current } => {
403                        if hovered_item.is_none_or(|item| item.id != *current) {
404                            state.indicator_status.narrow(0.0, *now);
405                        }
406                    }
407                    IndicatorStatus::Narrowing { current, animation } => {
408                        if hovered_item.is_some_and(|item| item.id == *current) {
409                            let value_now = animation.interpolate_with(|v| v, *now);
410                            let from = 1.0 - value_now;
411                            state.indicator_status.widen(from, *now);
412                        } else if !animation.is_animating(*now) {
413                            state.indicator_status = IndicatorStatus::Fixed {
414                                current: current.clone(),
415                            };
416                        }
417                    }
418                    IndicatorStatus::Moving { animation, .. } => {
419                        if !animation.is_animating(*now) {
420                            state.indicator_status.widen(0.0, *now);
421                        }
422                    }
423                }
424            }
425
426            if state.indicator_status.is_animating(*now) {
427                shell.request_redraw();
428            }
429
430            state.now = Some(*now);
431        }
432    }
433
434    fn mouse_interaction(
435        &self,
436        tree: &Tree,
437        layout: Layout<'_>,
438        cursor: Cursor,
439        viewport: &Rectangle,
440        renderer: &Renderer,
441    ) -> Interaction {
442        self.items
443            .iter()
444            .zip(&tree.children)
445            .zip(layout.children())
446            .map(|((item, tree), layout)| {
447                item.mouse_interaction(tree, layout, cursor, viewport, renderer)
448            })
449            .max()
450            .unwrap_or_default()
451    }
452
453    fn draw(
454        &self,
455        tree: &Tree,
456        renderer: &mut Renderer,
457        theme: &Theme,
458        style: &iced_core::renderer::Style,
459        layout: Layout<'_>,
460        cursor: Cursor,
461        viewport: &Rectangle,
462    ) {
463        let bounds = layout.bounds();
464        let selector_bar_style = <Theme as Catalog>::style(theme, &self.class);
465
466        // Draw SelectorBar (i.e. background)
467        renderer.fill_quad(
468            Quad {
469                bounds,
470                border: selector_bar_style.border,
471                shadow: selector_bar_style.shadow,
472                snap: selector_bar_style.snap,
473            },
474            selector_bar_style
475                .background
476                .unwrap_or(Color::TRANSPARENT.into()),
477        );
478
479        // Draw each Item
480        for ((item, tree), layout) in self
481            .items
482            .iter()
483            .zip(&tree.children)
484            .zip(layout.child(0).children())
485        {
486            item.draw(tree, renderer, theme, style, layout, cursor, viewport);
487        }
488
489        let item = |id| {
490            self.items
491                .iter()
492                .zip(layout.child(0).children())
493                .find(|(item, _)| item.id == id)
494                .unwrap()
495        };
496
497        // Draw Indicator
498        let state = tree.state.downcast_ref::<State<Id>>();
499
500        let Some(now) = state.now else {
501            return;
502        };
503
504        let (selected_item, selected_layout) = item(self.selected_id.clone());
505        let padding = selected_item.padding;
506        let base_x = selected_layout.position().x;
507        let outer_width = selected_layout.bounds().width;
508        let inner_width = outer_width - padding.left - padding.right;
509
510        let (x, width) = match &state.indicator_status {
511            IndicatorStatus::Fixed { .. } => (base_x + padding.left, inner_width),
512            IndicatorStatus::Widening { animation, .. } => {
513                let value = animation.interpolate_with(|v| v, now);
514                let x = base_x + padding.left * (1.0 - value);
515                let width = inner_width + 2.0 * padding.left * value;
516                (x, width)
517            }
518            IndicatorStatus::Hovered { .. } => (base_x, inner_width + padding.left + padding.right),
519            IndicatorStatus::Narrowing { animation, .. } => {
520                let value = animation.interpolate_with(|v| v, now);
521                let x = base_x + padding.left * value;
522                let width = outer_width - 2.0 * padding.left * value;
523                (x, width)
524            }
525            IndicatorStatus::Moving {
526                from, animation, ..
527            } => {
528                let from_x = item(from.clone()).1.position().x;
529                let to_x = selected_layout.bounds().x;
530                let x = animation.interpolate(from_x, to_x, now);
531                (x, inner_width)
532            }
533        };
534
535        let bounds = Rectangle {
536            x,
537            y: bounds.y + bounds.height - self.padding.top - INDICATOR_HEIGHT,
538            width,
539            height: INDICATOR_HEIGHT,
540        };
541
542        let indicator_style = <Theme as item::Catalog>::style(
543            theme,
544            &selected_item.class,
545            selected_item.status.unwrap_or_default(),
546        )
547        .active_indicator;
548
549        renderer.fill_quad(
550            Quad {
551                bounds,
552                border: indicator_style.border,
553                shadow: indicator_style.shadow,
554                snap: indicator_style.snap,
555            },
556            indicator_style
557                .background
558                .unwrap_or(Color::TRANSPARENT.into()),
559        );
560    }
561
562    fn overlay<'b>(
563        &'b mut self,
564        tree: &'b mut Tree,
565        layout: Layout<'b>,
566        renderer: &Renderer,
567        viewport: &Rectangle,
568        translation: Vector,
569    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
570        let overlays = self
571            .items
572            .iter_mut()
573            .zip(&mut tree.children)
574            .zip(layout.child(0).children())
575            .flat_map(|((item, tree), layout)| {
576                item.overlay(tree, layout, renderer, viewport, translation)
577            })
578            .collect();
579
580        Some(overlay::Group::with_children(overlays).overlay())
581    }
582}
583
584impl<'a, Id, Message, Theme, Renderer> From<SelectorBar<'a, Id, Message, Theme, Renderer>>
585    for Element<'a, Message, Theme, Renderer>
586where
587    Id: 'static + Clone + std::fmt::Debug + Default + PartialEq,
588    Message: 'a,
589    Theme: 'a + Catalog + item::Catalog,
590    Renderer: 'a + iced_core::Renderer,
591{
592    fn from(bar: SelectorBar<'a, Id, Message, Theme, Renderer>) -> Self {
593        Element::new(bar)
594    }
595}
596
597/// The appearance of a [`SelectorBar`].
598#[derive(Debug, Clone, Copy, PartialEq, Default)]
599pub struct Style {
600    /// The [`Background`] of the [`SelectorBar`].
601    pub background: Option<Background>,
602    /// The [`Border`] of the [`SelectorBar`].
603    pub border: Border,
604    /// The [`Shadow`] of the [`SelectorBar`].
605    pub shadow: Shadow,
606    /// Whether the [`SelectorBar`] should be snapped to the pixel grid.
607    pub snap: bool,
608}
609
610impl Style {
611    /// Updates the border of the [`Style`].
612    pub fn border(self, border: impl Into<Border>) -> Self {
613        Self {
614            border: border.into(),
615            ..self
616        }
617    }
618
619    /// Updates the background of the [`Style`].
620    pub fn background(self, background: impl Into<Background>) -> Self {
621        Self {
622            background: Some(background.into()),
623            ..self
624        }
625    }
626
627    /// Updates the shadow of the [`Style`].
628    pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
629        Self {
630            shadow: shadow.into(),
631            ..self
632        }
633    }
634}
635
636impl From<Color> for Style {
637    fn from(color: Color) -> Self {
638        Self::default().background(color)
639    }
640}
641
642impl From<Gradient> for Style {
643    fn from(gradient: Gradient) -> Self {
644        Self::default().background(gradient)
645    }
646}
647
648impl From<Linear> for Style {
649    fn from(gradient: Linear) -> Self {
650        Self::default().background(gradient)
651    }
652}
653
654pub trait Catalog {
655    type Class<'a>;
656
657    fn default<'a>() -> Self::Class<'a>;
658    fn style(&self, class: &Self::Class<'_>) -> Style;
659}
660
661pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
662
663impl Catalog for Theme {
664    type Class<'a> = StyleFn<'a, Self>;
665
666    fn default<'a>() -> Self::Class<'a> {
667        Box::new(transparent)
668    }
669
670    fn style(&self, class: &Self::Class<'_>) -> Style {
671        class(self)
672    }
673}
674
675pub fn transparent(_theme: &Theme) -> Style {
676    Style {
677        background: None,
678        border: Border::default(),
679        shadow: Shadow::default(),
680        snap: true,
681    }
682}
683
684pub fn item<'a, Id, Message, Theme, Renderer>(
685    id: Id,
686    content: impl Into<Element<'a, Message, Theme, Renderer>>,
687) -> Item<'a, Id, Message, Theme, Renderer>
688where
689    Theme: item::Catalog,
690{
691    Item::new(id, content)
692}