Skip to main content

iced_widget_kit/
ribbon.rs

1//! A horizontal set of [Group]s that adapt their appearance and arrangement when the [Ribbon]
2//! is resized.
3pub mod group;
4
5pub use group::{Group, Size};
6
7use crate::ribbon::group::is_collapsed;
8
9use iced_core::{
10    Background, Border, Clipboard, Color, Element, Event, Font, Gradient, Layout, Length, Padding,
11    Pixels, Point, Rectangle, Shadow, Shell, Size as IcedSize, Theme, Vector, Widget,
12    gradient::Linear,
13    keyboard,
14    layout::{self, Limits, Node},
15    mouse::{self, Cursor, Interaction},
16    overlay,
17    renderer::{self, Quad},
18    touch,
19    widget::{
20        Tree,
21        tree::{self, Tag},
22    },
23    window,
24};
25
26use core::panic;
27use iced_widget::{button, svg, text};
28use std::{f32, time::Instant};
29
30const SEPARATOR_WIDTH: f32 = 1.0;
31
32/// A horizontal collection of [`Group`]s which change their appearance and layout
33/// when the [`Ribbon`] is resized.
34pub struct Ribbon<'a, Id, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
35where
36    Id: Clone + Eq,
37    Theme: Catalog + button::Catalog + text::Catalog,
38    Renderer: iced_core::Renderer + iced_core::text::Renderer,
39{
40    groups: Vec<Group<'a, Id, Message, Theme, Renderer>>,
41    width: Length,
42    height: Length,
43    padding: Padding,
44    spacing: f32,
45    on_group_dropdown_dismiss: Option<OnDismiss<'a, Message>>,
46    class: <Theme as Catalog>::Class<'a>,
47}
48
49impl<'a, Id, Message, Theme, Renderer> Ribbon<'a, Id, Message, Theme, Renderer>
50where
51    Id: Clone + Eq,
52    Message: 'a + Clone,
53    Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
54    <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
55    Renderer: 'a + iced_core::Renderer + iced_core::svg::Renderer + iced_core::text::Renderer,
56    <Renderer as iced_core::text::Renderer>::Font: From<iced_core::Font>,
57    <Renderer as iced_core::text::Renderer>::Paragraph: Clone,
58{
59    /// Creates a new [`Ribbon`] which the provided [`Group`]s.
60    #[must_use]
61    pub fn new(groups: impl IntoIterator<Item = Group<'a, Id, Message, Theme, Renderer>>) -> Self {
62        Self {
63            groups: groups.into_iter().collect(),
64            width: Length::Fill,
65            height: Length::Shrink,
66            padding: Padding::new(4.0),
67            spacing: 0.0,
68            on_group_dropdown_dismiss: None,
69            class: <Theme as Catalog>::default(),
70        }
71    }
72
73    /// Sets the width of the [`Ribbon`].
74    #[must_use]
75    pub fn width(mut self, width: impl Into<Length>) -> Self {
76        self.width = width.into();
77        self
78    }
79
80    /// Sets the height of the [`Ribbon`].
81    #[must_use]
82    pub fn height(mut self, height: impl Into<Length>) -> Self {
83        self.height = height.into();
84        self
85    }
86
87    /// Sets the spacing between [`Group`]s.
88    #[must_use]
89    pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
90        self.spacing = amount.into().0;
91        self
92    }
93
94    /// Sets the [`Padding`] of the [`Ribbon`].
95    #[must_use]
96    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
97        self.padding = padding.into();
98        self
99    }
100
101    /// Opens the [`Group`] as a dropdown when collapsed.
102    ///
103    /// # Panics
104    /// Panics if no [`Group`] in the [`Ribbon`] has the provided id.
105    #[must_use]
106    pub fn open_group(mut self, id: Id) -> Self {
107        let mut is_found = false;
108
109        for group in &mut self.groups {
110            if group.id() == id {
111                is_found = true;
112                group.open_dropdown();
113            } else {
114                group.close_dropdown();
115            }
116        }
117
118        if !is_found {
119            panic!("expect group with id")
120        };
121
122        self
123    }
124
125    /// Sets the message that will be sent when the [`Group`] collapsed dropdown is dismissed.
126    #[must_use]
127    pub fn on_group_dropdown_dismiss(mut self, on_dismiss: Message) -> Self {
128        self.on_group_dropdown_dismiss = Some(OnDismiss::Direct(on_dismiss));
129        self
130    }
131
132    /// Sets the message that will be sent when the [`Group`] collapsed dropdown is dismissed.
133    ///
134    /// This is analogous to [`Ribbon::on_group_dropdown_dismiss`], but using a closure to produce
135    /// the message.
136    #[must_use]
137    pub fn on_group_dropdown_dismiss_with(mut self, on_dismiss: impl Fn() -> Message + 'a) -> Self {
138        self.on_group_dropdown_dismiss = Some(OnDismiss::Closure(Box::new(on_dismiss)));
139        self
140    }
141
142    /// Sets the style of the [`Ribbon`].
143    #[must_use]
144    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
145    where
146        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
147    {
148        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
149        self
150    }
151
152    fn total_spacing(&self) -> f32 {
153        (self.groups.len() - 1) as f32 * self.spacing
154    }
155
156    fn can_shrink(&self, current_sizes: &[group::Size]) -> bool {
157        self.groups
158            .iter()
159            .zip(current_sizes)
160            .any(|(group, current_size)| group.shrink_hint(*current_size).is_some())
161    }
162
163    fn should_shrink(&mut self, available_width: f32, current_sizes: &[group::Size]) -> bool {
164        let groups_width = self
165            .groups
166            .iter()
167            .zip(current_sizes)
168            .fold(0.0, |total, (group, size)| {
169                total + group.size_width(*size).unwrap()
170            })
171            + self.total_spacing()
172            + self.padding.left
173            + self.padding.right;
174
175        (available_width - groups_width) < 0.0 && self.can_shrink(current_sizes)
176    }
177}
178
179fn group_sizes(tree: &Tree) -> Vec<group::Size> {
180    tree.children.iter().map(group::current_size).collect()
181}
182
183enum OnDismiss<'a, Message> {
184    Direct(Message),
185    Closure(Box<dyn Fn() -> Message + 'a>),
186}
187
188impl<'a, Message: Clone> OnDismiss<'a, Message> {
189    fn get(&self) -> Message {
190        match self {
191            OnDismiss::Direct(msg) => msg.clone(),
192            OnDismiss::Closure(f) => f(),
193        }
194    }
195}
196
197#[derive(Debug, Default)]
198struct State {
199    now: Option<Instant>,
200    open_collapsed_group: Option<usize>,
201    available_width: f32,
202    group_sizes: Vec<group::Size>,
203}
204
205impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
206    for Ribbon<'a, Id, Message, Theme, Renderer>
207where
208    Id: Clone + Eq,
209    Message: 'a + Clone,
210    Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
211    Renderer: 'a + iced_core::svg::Renderer + iced_core::text::Renderer,
212    <Renderer as iced_core::text::Renderer>::Font: From<iced_core::Font>,
213    <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
214    <Renderer as iced_core::text::Renderer>::Paragraph: Clone,
215{
216    fn tag(&self) -> Tag {
217        Tag::of::<State>()
218    }
219
220    fn state(&self) -> tree::State {
221        tree::State::new(State::default())
222    }
223
224    fn children(&self) -> Vec<Tree> {
225        self.groups.iter().map(Group::tree).collect()
226    }
227
228    fn diff(&self, tree: &mut Tree) {
229        tree.diff_children_custom(&self.groups, |tree, group| group.diff(tree), Group::tree);
230    }
231
232    fn size(&self) -> IcedSize<Length> {
233        IcedSize::new(self.width, self.height)
234    }
235
236    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
237        let limits = limits.width(self.width).height(self.height);
238
239        let groups_max_height = self.groups.iter_mut().zip(&mut tree.children).fold(
240            0.0_f32,
241            |max_height, (group, tree)| {
242                group
243                    .height_check_layout(tree, renderer, &limits)
244                    .size()
245                    .height
246                    .max(max_height)
247            },
248        );
249
250        // Calculate if Groups need their group::Size changed
251        let previous_available_width = tree.state.downcast_ref::<State>().available_width;
252        let available_width = limits.shrink(self.padding).width(self.width).max().width;
253        let previous_sizes = &tree.state.downcast_ref::<State>().group_sizes;
254
255        if previous_available_width != available_width || *previous_sizes != group_sizes(tree) {
256            let is_group_animating = tree.children.iter().any(|child| {
257                let state = child.state.downcast_ref::<group::State>();
258                state.is_resizing()
259            });
260
261            if !is_group_animating {
262                let mut current_sizes = self
263                    .groups
264                    .iter()
265                    .map(|group| group.maximum_size())
266                    .collect::<Vec<_>>();
267
268                // Starting from the right, step through each group reducing its size until
269                // the Ribbon fits in the available width
270                while self.should_shrink(available_width, &current_sizes) {
271                    let max_size = self
272                        .groups
273                        .iter()
274                        .zip(&current_sizes)
275                        .filter(|(group, current_size)| group.can_shrink(**current_size))
276                        .fold(group::Size::Collapsed, |size, (_, current_size)| {
277                            group::Size::max(size, *current_size)
278                        });
279
280                    for (group, current_size) in
281                        self.groups.iter_mut().zip(current_sizes.iter_mut()).rev()
282                    {
283                        if let Some(size_hint) = group.shrink_hint(*current_size)
284                            && *current_size >= max_size
285                        {
286                            *current_size = size_hint;
287                            break;
288                        }
289                    }
290                }
291
292                for (group_tree, size) in tree.children.iter_mut().zip(&current_sizes) {
293                    match tree.state.downcast_ref::<State>().now {
294                        Some(now) => group::resize(group_tree, *size, now),
295                        None => group::resize_fixed(group_tree, *size),
296                    }
297                }
298
299                let group_sizes = group_sizes(tree);
300                let state = tree.state.downcast_mut::<State>();
301                state.available_width = available_width;
302                state.group_sizes = group_sizes;
303            }
304        }
305
306        let groups_limits =
307            limits.max_height(groups_max_height + self.padding.top + self.padding.bottom);
308
309        layout::padded(
310            &groups_limits,
311            self.width,
312            self.height,
313            self.padding,
314            |limits| {
315                // Interleave groups with separators and position
316                let (size, nodes) = self.groups.iter_mut().zip(&mut tree.children).fold(
317                    (IcedSize::ZERO, vec![]),
318                    |(mut total_size, mut nodes), (group, group_tree)| {
319                        let group_node = group
320                            .layout(group_tree, renderer, limits)
321                            .move_to([total_size.width, 0.0]);
322
323                        let total_width = total_size.width + group_node.size().width;
324                        nodes.push(group_node);
325
326                        // Every second node is a separator line node
327                        let separator_width = SEPARATOR_WIDTH + self.spacing;
328
329                        let separator_node =
330                            Node::new(IcedSize::new(separator_width, limits.max().height))
331                                .move_to([total_width, 0.0]);
332
333                        total_size =
334                            IcedSize::new(total_width + separator_width, groups_max_height);
335
336                        nodes.push(separator_node);
337                        (total_size, nodes)
338                    },
339                );
340
341                Node::with_children(size, nodes)
342            },
343        )
344    }
345
346    fn operate(
347        &mut self,
348        tree: &mut Tree,
349        layout: Layout<'_>,
350        renderer: &Renderer,
351        operation: &mut dyn iced_core::widget::Operation,
352    ) {
353        operation.container(None, layout.bounds());
354
355        operation.traverse(&mut |operation| {
356            self.groups
357                .iter_mut()
358                .zip(&mut tree.children)
359                .zip(layout.child(0).children().step_by(2))
360                .for_each(|((group, state), layout)| {
361                    group.operate(state, layout, renderer, operation);
362                });
363        });
364    }
365
366    fn update(
367        &mut self,
368        tree: &mut Tree,
369        event: &Event,
370        layout: Layout<'_>,
371        cursor: Cursor,
372        renderer: &Renderer,
373        clipboard: &mut dyn Clipboard,
374        shell: &mut Shell<'_, Message>,
375        viewport: &Rectangle,
376    ) {
377        for ((group, group_tree), layout) in self
378            .groups
379            .iter_mut()
380            .zip(&mut tree.children)
381            .zip(layout.child(0).children().step_by(2))
382        {
383            group.update(
384                group_tree, event, layout, cursor, renderer, clipboard, shell, viewport,
385            );
386
387            if shell.is_event_captured() {
388                return;
389            }
390        }
391
392        let state = tree.state.downcast_mut::<State>();
393
394        match event {
395            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
396            | Event::Touch(touch::Event::FingerLifted { .. })
397            | Event::Touch(touch::Event::FingerLost { .. }) => {
398                state.open_collapsed_group = None;
399            }
400            Event::Window(window::Event::RedrawRequested(now)) => {
401                state.now = Some(*now);
402            }
403            _ => {}
404        }
405    }
406
407    fn draw(
408        &self,
409        tree: &Tree,
410        renderer: &mut Renderer,
411        theme: &Theme,
412        style: &renderer::Style,
413        layout: Layout<'_>,
414        cursor: Cursor,
415        viewport: &Rectangle,
416    ) {
417        let ribbon_style = <Theme as Catalog>::style(theme, &self.class);
418        let groups_bounds = layout.bounds();
419
420        renderer.fill_quad(
421            Quad {
422                bounds: groups_bounds,
423                border: ribbon_style.border,
424                shadow: ribbon_style.shadow,
425                snap: ribbon_style.snap,
426            },
427            ribbon_style.background.unwrap_or(Color::TRANSPARENT.into()),
428        );
429
430        let mut layouts = layout.child(0).children();
431        let mut trees = tree.children.iter();
432        let mut next_group_trees = tree.children.iter().skip(1);
433
434        for group in self.groups.iter() {
435            let group_tree = trees.next().unwrap();
436
437            // Every odd node is a Group
438            group.draw(
439                group_tree,
440                renderer,
441                theme,
442                style,
443                layouts.next().unwrap(),
444                cursor,
445                viewport,
446            );
447
448            // Every even node is a separator line
449            let layout = layouts.next().unwrap();
450            let bounds = layout.bounds();
451
452            let is_group_collapsed = is_collapsed(group_tree);
453            let is_next_group_collapsed = next_group_trees.next().is_some_and(is_collapsed);
454
455            let x = match (is_group_collapsed, is_next_group_collapsed) {
456                (true, _) => bounds.x,
457                (_, true) => bounds.x + (bounds.width - 1.0),
458                (false, false) => bounds.x + (bounds.width - 1.0) / 2.0,
459            };
460
461            renderer.fill_quad(
462                Quad {
463                    bounds: Rectangle {
464                        x,
465                        y: bounds.y,
466                        width: 1.0,
467                        height: bounds.height,
468                    },
469                    ..Quad::default()
470                },
471                ribbon_style.separator_color.unwrap_or(Color::TRANSPARENT),
472            );
473        }
474    }
475
476    fn mouse_interaction(
477        &self,
478        tree: &Tree,
479        layout: Layout<'_>,
480        cursor: Cursor,
481        viewport: &Rectangle,
482        renderer: &Renderer,
483    ) -> Interaction {
484        self.groups
485            .iter()
486            .zip(&tree.children)
487            .zip(layout.child(0).children().step_by(2))
488            .map(|((group, tree), layout)| {
489                group.mouse_interaction(tree, layout, cursor, viewport, renderer)
490            })
491            .max()
492            .unwrap_or_default()
493    }
494
495    fn overlay<'b>(
496        &'b mut self,
497        tree: &'b mut Tree,
498        layout: Layout<'b>,
499        renderer: &Renderer,
500        viewport: &Rectangle,
501        translation: Vector,
502    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
503        let overlays = self
504            .groups
505            .iter_mut()
506            .zip(&mut tree.children)
507            .zip(layout.child(0).children().step_by(2))
508            .flat_map(|((group, tree), layout)| {
509                if is_collapsed(tree) && group.is_dropdown_open() {
510                    Some(overlay::Element::new(Box::new(GroupDropdown {
511                        position: layout.position(),
512                        tree,
513                        group,
514                        padding: self.padding,
515                        on_dismiss: self.on_group_dropdown_dismiss.as_ref(),
516                        class: &self.class,
517                    })))
518                } else {
519                    group.overlay(tree, layout, renderer, viewport, translation)
520                }
521            })
522            .collect();
523
524        Some(overlay::Group::with_children(overlays).overlay())
525    }
526}
527
528impl<'a, Id, Message, Theme, Renderer> From<Ribbon<'a, Id, Message, Theme, Renderer>>
529    for Element<'a, Message, Theme, Renderer>
530where
531    Id: 'a + Clone + Eq,
532    Message: 'a + Clone,
533    Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
534    Renderer: 'a + iced_core::svg::Renderer + iced_core::text::Renderer,
535    <Renderer as iced_core::text::Renderer>::Font: From<Font>,
536    <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
537    <Renderer as iced_core::text::Renderer>::Paragraph: Clone,
538{
539    fn from(ribbon: Ribbon<'a, Id, Message, Theme, Renderer>) -> Self {
540        Element::new(ribbon)
541    }
542}
543
544struct GroupDropdown<
545    'a,
546    'b,
547    Id,
548    Message,
549    Theme = iced_core::Theme,
550    Renderer = iced_widget::Renderer,
551> where
552    Id: Clone + Eq,
553    Theme: Catalog + button::Catalog + text::Catalog,
554    Renderer: iced_core::Renderer + iced_core::text::Renderer,
555{
556    position: Point,
557    tree: &'b mut Tree,
558    group: &'b mut Group<'a, Id, Message, Theme, Renderer>,
559    padding: Padding,
560    on_dismiss: Option<&'b OnDismiss<'b, Message>>,
561    class: &'b <Theme as Catalog>::Class<'a>,
562}
563
564impl<'a, 'b, Id, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
565    for GroupDropdown<'a, 'b, Id, Message, Theme, Renderer>
566where
567    Id: Clone + Eq,
568    Message: 'a + Clone,
569    Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
570    <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
571    Renderer: 'a + iced_core::Renderer + iced_core::svg::Renderer + iced_core::text::Renderer,
572    <Renderer as iced_core::text::Renderer>::Font: From<Font>,
573    <Renderer as iced_core::text::Renderer>::Paragraph: Clone,
574{
575    fn update(
576        &mut self,
577        event: &Event,
578        layout: Layout<'_>,
579        cursor: Cursor,
580        renderer: &Renderer,
581        clipboard: &mut dyn Clipboard,
582        shell: &mut Shell<'_, Message>,
583    ) {
584        let bounds = layout.bounds();
585        let max_size = self.group.maximum_size();
586
587        self.group.size_update(
588            max_size,
589            self.tree,
590            event,
591            layout.child(0),
592            cursor,
593            renderer,
594            clipboard,
595            shell,
596            &bounds,
597        );
598
599        let mut publish_dismiss = || {
600            if let Some(on_dismiss) = self.on_dismiss {
601                shell.publish(on_dismiss.get());
602            }
603        };
604
605        if cursor.position_over(bounds).is_none() {
606            match event {
607                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
608                | Event::Touch(touch::Event::FingerLifted { .. })
609                | Event::Window(window::Event::Resized(_)) => publish_dismiss(),
610                Event::Keyboard(keyboard::Event::KeyPressed { key, .. })
611                    if *key == keyboard::Key::Named(keyboard::key::Named::Escape) =>
612                {
613                    publish_dismiss()
614                }
615
616                _ => {}
617            }
618        }
619    }
620
621    fn layout(&mut self, renderer: &Renderer, bounds: IcedSize) -> layout::Node {
622        let limits = Limits::new(IcedSize::ZERO, bounds);
623
624        let mut node = layout::padded(
625            &limits,
626            Length::Shrink,
627            Length::Shrink,
628            self.padding,
629            |limits| {
630                self.group
631                    .size_layout(self.group.maximum_size(), true, self.tree, renderer, limits)
632            },
633        );
634
635        let size = node.size();
636
637        // Try to stay inside borders
638        let mut position = Point::new(self.position.x, self.position.y + size.height);
639
640        if position.x + size.width > bounds.width {
641            position.x = f32::max(0.0, bounds.width - size.width);
642        }
643        if position.y + size.height > bounds.height {
644            position.y = f32::max(0.0, bounds.height - size.height);
645        }
646
647        node.move_to_mut(position);
648        node
649    }
650
651    fn draw(
652        &self,
653        renderer: &mut Renderer,
654        theme: &Theme,
655        style: &renderer::Style,
656        layout: Layout<'_>,
657        cursor: Cursor,
658    ) {
659        let bounds = layout.bounds();
660        let background_style = <Theme as Catalog>::style(theme, self.class);
661
662        renderer.fill_quad(
663            renderer::Quad {
664                bounds,
665                border: background_style.border,
666                shadow: background_style.shadow,
667                snap: background_style.snap,
668            },
669            background_style
670                .background
671                .unwrap_or(Color::TRANSPARENT.into()),
672        );
673
674        let max_size = self.group.maximum_size();
675        let layout = layout.child(0);
676        let bounds = layout.bounds();
677
678        self.group.size_draw(
679            max_size, self.tree, renderer, theme, style, layout, cursor, &bounds,
680        );
681    }
682
683    fn mouse_interaction(
684        &self,
685        layout: Layout<'_>,
686        cursor: Cursor,
687        renderer: &Renderer,
688    ) -> Interaction {
689        self.group.size_mouse_interaction(
690            self.group.maximum_size(),
691            self.tree,
692            layout.child(0),
693            cursor,
694            &layout.bounds(),
695            renderer,
696        )
697    }
698}
699
700/// The appearance of a [`Ribbon`].
701#[derive(Debug, Clone, Copy, PartialEq, Default)]
702pub struct Style {
703    /// The [`Background`] of the [`Ribbon`].
704    pub background: Option<Background>,
705    /// The [`Border`] of the [`Ribbon`].
706    pub border: Border,
707    /// The [`Shadow`] of the [`Ribbon`].
708    pub shadow: Shadow,
709    /// The color of the separator between groups.
710    pub separator_color: Option<Color>,
711    /// Whether the [`Ribbon`] should be snapped to the pixel grid.
712    pub snap: bool,
713}
714
715impl Style {
716    /// Updates the border of the [`Style`].
717    pub fn border(self, border: impl Into<Border>) -> Self {
718        Self {
719            border: border.into(),
720            ..self
721        }
722    }
723
724    /// Updates the background of the [`Style`].
725    pub fn background(self, background: impl Into<Background>) -> Self {
726        Self {
727            background: Some(background.into()),
728            ..self
729        }
730    }
731
732    /// Updates the shadow of the [`Style`].
733    pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
734        Self {
735            shadow: shadow.into(),
736            ..self
737        }
738    }
739
740    /// Update the separator color.
741    pub fn separator_color(self, color: Color) -> Self {
742        Self {
743            separator_color: Some(color),
744            ..self
745        }
746    }
747}
748
749impl From<Color> for Style {
750    fn from(color: Color) -> Self {
751        Self::default().background(color)
752    }
753}
754
755impl From<Gradient> for Style {
756    fn from(gradient: Gradient) -> Self {
757        Self::default().background(gradient)
758    }
759}
760
761impl From<Linear> for Style {
762    fn from(gradient: Linear) -> Self {
763        Self::default().background(gradient)
764    }
765}
766
767/// The theme catalog of a [`Ribbon`].
768pub trait Catalog {
769    /// The item class of the [`Catalog`].
770    type Class<'a>;
771
772    /// The default class produced by the [`Catalog`].
773    fn default<'a>() -> Self::Class<'a>;
774
775    /// The [`Style`] of a class.
776    fn style(&self, class: &Self::Class<'_>) -> Style;
777}
778
779/// A styling function for a [`Ribbon`].
780pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
781
782impl<Theme> From<Style> for StyleFn<'_, Theme> {
783    fn from(style: Style) -> Self {
784        Box::new(move |_theme| style)
785    }
786}
787
788impl Catalog for Theme {
789    type Class<'a> = StyleFn<'a, Self>;
790
791    fn default<'a>() -> Self::Class<'a> {
792        Box::new(|_| Style::default())
793    }
794
795    fn style(&self, class: &Self::Class<'_>) -> Style {
796        class(self)
797    }
798}
799
800pub fn group<'a, Id, Message, Theme, Renderer>(
801    id: Id,
802    header: impl text::IntoFragment<'a>,
803    content: impl Fn(Size) -> Option<Element<'a, Message, Theme, Renderer>> + 'a,
804) -> Group<'a, Id, Message, Theme, Renderer>
805where
806    Id: Clone + Eq,
807    Message: 'a + Clone,
808    Theme: 'a + button::Catalog + svg::Catalog + text::Catalog,
809    <Theme as svg::Catalog>::Class<'a>: From<svg::StyleFn<'a, Theme>>,
810    <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
811    Renderer: 'a + iced_core::Renderer + iced_core::svg::Renderer + iced_core::text::Renderer,
812    <Renderer as iced_core::text::Renderer>::Font: From<Font>,
813    <Renderer as iced_core::text::Renderer>::Paragraph: Clone,
814{
815    Group::new(id, header, content)
816}