Skip to main content

iced_widget_kit/ribbon/
group.rs

1use iced_core::{
2    Alignment, Animation, Clipboard, Element, Event, Font, Layout, Length, Pixels, Rectangle,
3    Shell, Size as IcedSize, Vector, Widget,
4    animation::Easing,
5    layout::{Limits, Node},
6    mouse::{Cursor, Interaction},
7    overlay,
8    text::{LineHeight, Wrapping},
9    widget::{
10        Tree,
11        tree::{self, Tag},
12    },
13    window,
14};
15
16use iced_palace::widget::{EllipsizedText, ellipsized_text};
17use iced_widget::{Button, Svg, button, column, space::vertical, svg, text};
18use std::{iter::once, time::Instant, vec};
19
20const LAUNCHER_SIZE: IcedSize = IcedSize {
21    width: 16.0,
22    height: 16.0,
23};
24
25const LAUNCHER_ICON_SIZE: IcedSize = IcedSize {
26    width: 8.0,
27    height: 8.0,
28};
29
30const HEADER_SPACING: f32 = 2.0;
31
32/// A labeled section within a [`Ribbon`](super::Ribbon) that clusters related widgets.
33/// It is resized by the [`Ribbon`](super::Ribbon) depending on what the content function given to
34/// the constructor chooses to provide.
35///
36/// # Panics
37/// Panics if the content function does not produce at least one element.
38/// Panics if the elements produced by the content function are not sorted in size.
39pub struct Group<'a, Id, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
40where
41    Id: Clone + Eq,
42    Theme: button::Catalog + text::Catalog,
43    Renderer: iced_core::Renderer + iced_core::text::Renderer,
44{
45    id: Id,
46    header: EllipsizedText<'a, Theme, Renderer>,
47    launcher: Button<'a, Message, Theme, Renderer>,
48    is_launcher_visible: bool,
49    collapsed_button: Option<Button<'a, Message, Theme, Renderer>>,
50    // Small, Medium & Large content
51    content: Vec<Option<Element<'a, Message, Theme, Renderer>>>,
52    size_widths: Vec<Option<f32>>,
53    is_dropdown_open: bool,
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
57pub enum Size {
58    #[default]
59    Collapsed,
60    Small,
61    Medium,
62    Large,
63}
64
65impl Size {
66    fn smaller(&self) -> Self {
67        match self {
68            Size::Collapsed => Size::Collapsed,
69            Size::Small => Size::Collapsed,
70            Size::Medium => Size::Small,
71            Size::Large => Size::Medium,
72        }
73    }
74
75    pub(super) fn is_collapsed(&self) -> bool {
76        matches!(self, Self::Collapsed)
77    }
78}
79
80impl<'a, Id, Message, Theme, Renderer> Group<'a, Id, Message, Theme, Renderer>
81where
82    Id: Clone + Eq,
83    Message: 'a + Clone,
84    Theme: 'a + button::Catalog + svg::Catalog + text::Catalog,
85    <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
86    Renderer: 'a + iced_core::Renderer + iced_core::svg::Renderer + iced_core::text::Renderer,
87    <Renderer as iced_core::text::Renderer>::Font: From<Font>,
88    <Renderer as iced_core::text::Renderer>::Paragraph: Clone,
89{
90    /// Creates a new [`Group`] with the provided header and content function.
91    #[must_use]
92    pub fn new(
93        id: Id,
94        header: impl text::IntoFragment<'a>,
95        content: impl Fn(Size) -> Option<Element<'a, Message, Theme, Renderer>> + 'a,
96    ) -> Self
97    where
98        <Theme as svg::Catalog>::Class<'a>: From<svg::StyleFn<'a, Theme>>,
99    {
100        let content_fn = Box::new(content);
101
102        let mut content = [Size::Collapsed, Size::Small, Size::Medium, Size::Large]
103            .iter()
104            .map(|size| content_fn(*size))
105            .collect::<Vec<_>>();
106
107        assert!(
108            content.iter().any(Option::is_some),
109            "group should contain at least one element"
110        );
111
112        let header_fragment = header.into_fragment();
113        let header = ellipsized_text(header_fragment.clone()).wrapping(Wrapping::None);
114
115        let icon = text("⌄").size(20.0).align_x(Alignment::Center);
116
117        let collapsed_button = content.remove(0).map(|content| {
118            Button::new(column![content, vertical(), icon,].align_x(Alignment::Center))
119                .padding([8, 16])
120        });
121
122        let launcher_handle = svg::Handle::from_path(format!(
123            "{}/assets/image/ribbon_launcher.svg",
124            env!("CARGO_MANIFEST_DIR")
125        ));
126
127        let launcher = Button::new(
128            Svg::new(launcher_handle)
129                .width(LAUNCHER_ICON_SIZE.width)
130                .height(LAUNCHER_ICON_SIZE.height),
131        )
132        .width(LAUNCHER_SIZE.width)
133        .height(LAUNCHER_SIZE.height)
134        .padding([
135            (LAUNCHER_SIZE.height - LAUNCHER_ICON_SIZE.height) / 2.0,
136            (LAUNCHER_SIZE.width - LAUNCHER_ICON_SIZE.width) / 2.0,
137        ]);
138
139        Self {
140            id,
141            header,
142            launcher,
143            is_launcher_visible: false,
144            collapsed_button,
145            content,
146            size_widths: vec![],
147            is_dropdown_open: false,
148        }
149    }
150
151    /// Sets the size of the [`Group`] header.
152    pub fn header_size(mut self, size: impl Into<Pixels>) -> Self {
153        self.header = self.header.size(size);
154        self
155    }
156
157    /// Sets the line height of the [`Group`] header.
158    pub fn header_line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
159        self.header = self.header.line_height(line_height);
160        self
161    }
162
163    /// Sets the font of the [`Group`] header.
164    pub fn header_font(mut self, font: impl Into<Renderer::Font>) -> Self {
165        self.header = self.header.font(font);
166        self
167    }
168
169    /// Sets the [`Wrapping`] of the [`Group`] header.
170    pub fn header_wrapping(mut self, wrapping: Wrapping) -> Self {
171        self.header = self.header.wrapping(wrapping);
172        self
173    }
174
175    /// Sets the message that will be sent when the launcher button is pressed.
176    #[must_use]
177    pub fn on_launcher_press(mut self, on_press: Message) -> Self {
178        self.launcher = self.launcher.on_press(on_press);
179        self.is_launcher_visible = true;
180        self
181    }
182
183    /// Sets the message that will be sent when the [`Size::Collapsed`] button is pressed
184    #[must_use]
185    pub fn on_collapsed_press(mut self, on_press: Message) -> Self {
186        self.collapsed_button = self
187            .collapsed_button
188            .map(|button| button.on_press(on_press));
189
190        self
191    }
192
193    /// Sets the message that will be sent when the [`Size::Collapsed`] button is pressed
194    ///    
195    /// This is analogous to [`Group::on_collapsed_press`], but using a closure to produce
196    /// the message.
197    #[must_use]
198    pub fn on_collapsed_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
199        self.collapsed_button = self
200            .collapsed_button
201            .map(|button| button.on_press_with(on_press));
202
203        self
204    }
205
206    /// Sets the style of the [`Group`] header.
207    #[must_use]
208    pub fn header_style(mut self, style: impl Fn(&Theme) -> text::Style + 'a) -> Self
209    where
210        <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
211    {
212        self.header = self.header.style(style);
213        self
214    }
215
216    /// Sets the style of the [`Group`] launcher button.
217    #[must_use]
218    pub fn launcher_style(
219        mut self,
220        style: impl Fn(&Theme, button::Status) -> button::Style + 'a,
221    ) -> Self
222    where
223        <Theme as button::Catalog>::Class<'a>: From<button::StyleFn<'a, Theme>>,
224    {
225        self.launcher = self.launcher.style(style);
226        self
227    }
228
229    /// Sets the style of the [`Group`] [`Size::Collapsed`] button.
230    #[must_use]
231    pub fn collapsed_style(
232        mut self,
233        style: impl Fn(&Theme, button::Status) -> button::Style + 'a,
234    ) -> Self
235    where
236        <Theme as button::Catalog>::Class<'a>: From<button::StyleFn<'a, Theme>>,
237    {
238        self.collapsed_button = self.collapsed_button.map(|button| button.style(style));
239        self
240    }
241
242    pub(super) fn id(&self) -> Id {
243        self.id.clone()
244    }
245
246    fn iced_sizes(&self) -> impl Iterator<Item = Option<IcedSize<Length>>> {
247        once(
248            self.collapsed_button
249                .as_ref()
250                .map(|button| button.size_hint()),
251        )
252        .chain(self.content.iter().map(|opt_element| {
253            opt_element
254                .as_ref()
255                .map(|element| element.as_widget().size_hint())
256        }))
257    }
258
259    fn init_size_widths(&mut self, tree: &mut Tree, renderer: &Renderer) {
260        if self.size_widths.is_empty() {
261            let mut trees = tree.children[2..].iter_mut();
262            let limits = &Limits::NONE;
263
264            self.size_widths = vec![self.collapsed_button.as_mut().map(|button| {
265                let node = button.layout(trees.next().unwrap(), renderer, limits);
266                node.size().width
267            })];
268
269            self.size_widths
270                .extend(self.content.iter_mut().map(|opt_element| {
271                    opt_element.as_mut().map(|element| {
272                        let widget = element.as_widget_mut();
273
274                        if widget.size_hint().width.is_fill() {
275                            panic!("expect non-fill width")
276                        }
277
278                        let tree = trees.next().unwrap();
279                        let node = widget.layout(tree, renderer, limits);
280
281                        node.size().width
282                    })
283                }));
284
285            // Check that each content element widths are sorted otherwise the ribbon shrink
286            // algorithm gives weird results
287            assert!(
288                self.size_widths
289                    .iter()
290                    .flatten()
291                    .collect::<Vec<_>>()
292                    .windows(2)
293                    .all(|widths| widths[0] < widths[1]),
294                "group content widths must be in ascending order"
295            );
296        }
297    }
298
299    pub(super) fn size_width(&self, size: Size) -> Option<f32> {
300        self.size_widths[match size {
301            Size::Collapsed => 0,
302            Size::Small => 1,
303            Size::Medium => 2,
304            Size::Large => 3,
305        }]
306    }
307
308    fn content_sizes(&self) -> [Option<Size>; 4] {
309        [
310            self.content[2].as_ref().map(|_| Size::Large),
311            self.content[1].as_ref().map(|_| Size::Medium),
312            self.content[0].as_ref().map(|_| Size::Small),
313            self.collapsed_button.as_ref().map(|_| Size::Collapsed),
314        ]
315    }
316
317    pub(super) fn maximum_size(&self) -> Size {
318        self.content_sizes()
319            .iter()
320            .find(|size| size.is_some())
321            .unwrap()
322            .unwrap()
323    }
324
325    pub(super) fn shrink_hint(&self, current_size: Size) -> Option<Size> {
326        let content_sizes = self.content_sizes();
327        let has_size = |size| content_sizes.contains(&Some(size));
328        let mut check_size = current_size;
329
330        while check_size != Size::Collapsed {
331            let smaller_size = check_size.smaller();
332
333            if has_size(smaller_size) {
334                return Some(smaller_size);
335            }
336
337            check_size = smaller_size;
338        }
339
340        None
341    }
342
343    pub(super) fn can_shrink(&self, current_size: Size) -> bool {
344        self.shrink_hint(current_size).is_some()
345    }
346
347    fn header_tree(&self) -> Tree {
348        Tree {
349            tag: <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::tag(&self.header),
350            state: <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::state(&self.header),
351            children: <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::children(&self.header),
352        }
353    }
354
355    fn content_state_index(&self, size: Size) -> usize {
356        self.content_sizes()
357            .iter()
358            .rev()
359            .fold(1, |mut idx, check_size| {
360                if let Some(check_size) = check_size
361                    && *check_size <= size
362                {
363                    idx += 1;
364                }
365
366                idx
367            })
368    }
369
370    fn content_widget(&self, size: Size) -> &dyn Widget<Message, Theme, Renderer> {
371        match size {
372            Size::Collapsed => self.collapsed_button.as_ref().unwrap(),
373            Size::Small => self.content[0].as_ref().unwrap().as_widget(),
374            Size::Medium => self.content[1].as_ref().unwrap().as_widget(),
375            Size::Large => self.content[2].as_ref().unwrap().as_widget(),
376        }
377    }
378
379    fn content_widget_mut(&mut self, size: Size) -> &mut dyn Widget<Message, Theme, Renderer> {
380        match size {
381            Size::Collapsed => self.collapsed_button.as_mut().unwrap(),
382            Size::Small => self.content[0].as_mut().unwrap().as_widget_mut(),
383            Size::Medium => self.content[1].as_mut().unwrap().as_widget_mut(),
384            Size::Large => self.content[2].as_mut().unwrap().as_widget_mut(),
385        }
386    }
387
388    pub(super) fn is_dropdown_open(&self) -> bool {
389        self.is_dropdown_open
390    }
391
392    pub(super) fn open_dropdown(&mut self) {
393        self.is_dropdown_open = true;
394    }
395
396    pub(super) fn close_dropdown(&mut self) {
397        self.is_dropdown_open = false;
398    }
399
400    fn new_state(&self) -> State {
401        let max_size = if self.content[2].is_some() {
402            Size::Large
403        } else if self.content[1].is_some() {
404            Size::Medium
405        } else if self.content[0].is_some() {
406            Size::Small
407        } else if self.collapsed_button.is_some() {
408            Size::Collapsed
409        } else {
410            panic!()
411        };
412
413        State::new(max_size, self.iced_sizes().collect())
414    }
415
416    pub(super) fn tree(&self) -> Tree {
417        Tree {
418            tag: self.tag(),
419            state: self.state(),
420            children: self.children(),
421        }
422    }
423
424    // Widget trait methods kept private to Ribbon use only
425    pub(super) fn tag(&self) -> Tag {
426        Tag::of::<State>()
427    }
428
429    pub(super) fn state(&self) -> tree::State {
430        tree::State::new(self.new_state())
431    }
432
433    pub(super) fn children(&self) -> Vec<Tree> {
434        let mut children = vec![
435            self.header_tree(),
436            Tree {
437                tag: self.launcher.tag(),
438                state: self.launcher.state(),
439                children: self.launcher.children(),
440            },
441        ];
442
443        if let Some(button) = &self.collapsed_button {
444            children.push(Tree {
445                tag: button.tag(),
446                state: button.state(),
447                children: button.children(),
448            });
449        }
450
451        children.extend(
452            self.content
453                .iter()
454                .flatten()
455                .map(|element| Tree::new(element.as_widget())),
456        );
457
458        children
459    }
460
461    pub(super) fn diff(&self, tree: &mut Tree) {
462        // Diff state - ensure Group's possible iced::Sizes are the same else
463        // reset and stop any animation
464        let state = tree.state.downcast_mut::<State>();
465
466        if state
467            .content_sizes
468            .iter()
469            .zip(self.iced_sizes())
470            .any(|(a, b)| a != &b)
471        {
472            *state = self.new_state();
473        }
474
475        // Diff children
476        let child_count = 2 + self.content_sizes().iter().flatten().count();
477
478        if tree.children.len() > child_count {
479            tree.children.truncate(child_count);
480        }
481
482        if tree.children.is_empty() {
483            tree.children.push(self.header_tree());
484        } else {
485            <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::diff(
486                &self.header,
487                &mut tree.children[0],
488            );
489        }
490
491        if tree.children.len() < 2 {
492            tree.children.push(Tree {
493                tag: self.launcher.tag(),
494                state: self.launcher.state(),
495                children: self.launcher.children(),
496            });
497        } else {
498            self.launcher.diff(&mut tree.children[1]);
499        }
500
501        let mut content_idx = 2;
502
503        if let Some(button) = &self.collapsed_button {
504            content_idx += 1;
505
506            if tree.children.len() < 3 {
507                tree.children.push(Tree {
508                    tag: button.tag(),
509                    state: button.state(),
510                    children: button.children(),
511                });
512            } else {
513                button.diff(&mut tree.children[2]);
514            }
515        }
516
517        for (i, element) in self.content.iter().flatten().enumerate() {
518            let widget = element.as_widget();
519            let index = i + content_idx;
520
521            if tree.children.len() <= index {
522                let element_tree = Tree::new(widget);
523                tree.children.push(element_tree);
524            } else {
525                widget.diff(&mut tree.children[index]);
526            }
527        }
528    }
529
530    pub(super) fn size_layout(
531        &mut self,
532        size: Size,
533        compress_header: bool,
534        tree: &mut Tree,
535        renderer: &Renderer,
536        limits: &Limits,
537    ) -> Node {
538        self.init_size_widths(tree, renderer);
539        let limits = limits.loose();
540
541        // First pass of the header to get its intrinsic height
542        let header_node = <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::layout(
543            &mut self.header,
544            &mut tree.children[0],
545            renderer,
546            &limits,
547        );
548
549        let shrink_height = if size.is_collapsed() {
550            0.0
551        } else {
552            header_node.size().height + HEADER_SPACING
553        };
554
555        let content_limits = limits.shrink([0.0, shrink_height]);
556        let content_state_index = self.content_state_index(size);
557
558        let content_node = self.content_widget_mut(size).layout(
559            &mut tree.children[content_state_index],
560            renderer,
561            &content_limits,
562        );
563
564        let contents_size = content_node.size();
565
566        let mut launcher_node = self
567            .launcher
568            .layout(&mut tree.children[1], renderer, &limits);
569
570        let launcher_width = if self.is_launcher_visible {
571            launcher_node.size().width
572        } else {
573            0.0
574        };
575
576        // Second and final pass layout of header
577        let header_limits = limits
578            .shrink(IcedSize::new(0.0, contents_size.height))
579            .max_width(contents_size.width - launcher_width);
580
581        let mut header_node = <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::layout(
582            &mut self.header,
583            &mut tree.children[0],
584            renderer,
585            &header_limits,
586        );
587
588        let mut header_size = header_node.size();
589        let state = tree.state.downcast_mut::<State>();
590
591        if size.is_collapsed() {
592            header_size.height = 0.0;
593        }
594
595        let header_x = ((contents_size.width - header_size.width) / 2.0).max(0.0);
596
597        let header_y = if compress_header {
598            contents_size.height + HEADER_SPACING
599        } else {
600            (contents_size.height + HEADER_SPACING).max(limits.max().height - header_size.height)
601        };
602
603        header_node.move_to_mut([header_x, header_y]);
604
605        let width = match &state.width_status {
606            WidthStatus::Fixed { .. } => contents_size.width,
607            WidthStatus::Resizing {
608                from_size,
609                to_size,
610                animation,
611                now,
612                ..
613            } => {
614                let from_width = self.size_width(*from_size).unwrap();
615                let to_width = self.size_width(*to_size).unwrap();
616                animation.interpolate(from_width, to_width, *now)
617            }
618        }
619        .round();
620
621        let group_size = IcedSize::new(
622            width,
623            contents_size.height + HEADER_SPACING + header_size.height,
624        );
625
626        let launcher_x = contents_size.width - LAUNCHER_SIZE.width;
627
628        // Vertically centre align with header
629        let header_y_centre = header_y + header_size.height / 2.0;
630        let launcher_y = header_y_centre - LAUNCHER_SIZE.height / 2.0;
631        launcher_node.move_to_mut([launcher_x, launcher_y]);
632
633        let nodes = vec![header_node, launcher_node, content_node];
634        Node::with_children(group_size, nodes)
635    }
636
637    pub(super) fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
638        let size = current_size(tree);
639        self.size_layout(size, false, tree, renderer, limits)
640    }
641
642    pub(super) fn height_check_layout(
643        &mut self,
644        tree: &mut Tree,
645        renderer: &Renderer,
646        limits: &Limits,
647    ) -> Node {
648        let size = current_size(tree);
649        self.size_layout(size, true, tree, renderer, limits)
650    }
651
652    pub(super) fn operate(
653        &mut self,
654        tree: &mut Tree,
655        layout: Layout<'_>,
656        renderer: &Renderer,
657        operation: &mut dyn iced_core::widget::Operation,
658    ) {
659        operation.container(None, layout.bounds());
660
661        operation.traverse(&mut |operation| {
662            <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::operate(
663                &mut self.header,
664                tree.children.get_mut(0).unwrap(),
665                layout.child(0),
666                renderer,
667                operation,
668            );
669
670            if self.is_launcher_visible {
671                self.launcher.operate(
672                    tree.children.get_mut(1).unwrap(),
673                    layout.child(1),
674                    renderer,
675                    operation,
676                );
677            }
678
679            let size = current_size(tree);
680            let content_state_index = self.content_state_index(size);
681
682            self.content_widget_mut(size).operate(
683                &mut tree.children[content_state_index],
684                layout.child(2),
685                renderer,
686                operation,
687            );
688        });
689    }
690
691    #[allow(clippy::too_many_arguments)]
692    pub(super) fn size_update(
693        &mut self,
694        size: Size,
695        tree: &mut Tree,
696        event: &Event,
697        layout: Layout<'_>,
698        cursor: Cursor,
699        renderer: &Renderer,
700        clipboard: &mut dyn Clipboard,
701        shell: &mut Shell<'_, Message>,
702        viewport: &Rectangle,
703    ) {
704        self.header.update(
705            tree.children.get_mut(0).unwrap(),
706            event,
707            layout.child(0),
708            cursor,
709            renderer,
710            clipboard,
711            shell,
712            viewport,
713        );
714
715        if self.is_launcher_visible {
716            self.launcher.update(
717                tree.children.get_mut(1).unwrap(),
718                event,
719                layout.child(1),
720                cursor,
721                renderer,
722                clipboard,
723                shell,
724                viewport,
725            );
726
727            if shell.is_event_captured() {
728                return;
729            }
730        }
731
732        let content_state_index = self.content_state_index(size);
733
734        self.content_widget_mut(size).update(
735            &mut tree.children[content_state_index],
736            event,
737            layout.child(2),
738            cursor,
739            renderer,
740            clipboard,
741            shell,
742            viewport,
743        );
744
745        if shell.is_event_captured() {
746            return;
747        }
748
749        let state = tree.state.downcast_mut::<State>();
750
751        if let Event::Window(window::Event::RedrawRequested(now)) = event {
752            match &mut state.width_status {
753                WidthStatus::Fixed { .. } => {}
754                WidthStatus::Resizing {
755                    to_size,
756                    animation,
757                    now: animation_now,
758                    last_progress,
759                    ..
760                } => {
761                    *animation_now = *now;
762
763                    if !animation.is_animating(*now) {
764                        state.width_status = WidthStatus::new(*to_size);
765                        shell.request_redraw();
766                    } else {
767                        let progress = animation.interpolate(0.0, 1.0, *now);
768
769                        if let Some(last) = last_progress
770                            && *last != progress
771                        {
772                            *last_progress = Some(*last);
773                            shell.invalidate_layout();
774                        }
775
776                        shell.request_redraw();
777                    }
778                }
779            }
780        }
781    }
782
783    #[allow(clippy::too_many_arguments)]
784    pub(super) fn update(
785        &mut self,
786        tree: &mut Tree,
787        event: &Event,
788        layout: Layout<'_>,
789        cursor: Cursor,
790        renderer: &Renderer,
791        clipboard: &mut dyn Clipboard,
792        shell: &mut Shell<'_, Message>,
793        viewport: &Rectangle,
794    ) {
795        let size = current_size(tree);
796
797        self.size_update(
798            size, tree, event, layout, cursor, renderer, clipboard, shell, viewport,
799        );
800    }
801
802    #[allow(clippy::too_many_arguments)]
803    pub(super) fn size_draw(
804        &self,
805        size: Size,
806        tree: &Tree,
807        renderer: &mut Renderer,
808        theme: &Theme,
809        style: &iced_core::renderer::Style,
810        layout: Layout<'_>,
811        cursor: Cursor,
812        viewport: &Rectangle,
813    ) {
814        if !size.is_collapsed() {
815            <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::draw(
816                &self.header,
817                &tree.children[0],
818                renderer,
819                theme,
820                style,
821                layout.child(0),
822                cursor,
823                viewport,
824            );
825        }
826
827        if self.is_launcher_visible {
828            self.launcher.draw(
829                &tree.children[1],
830                renderer,
831                theme,
832                style,
833                layout.child(1),
834                cursor,
835                viewport,
836            );
837        }
838
839        let state = tree.state.downcast_ref::<State>();
840
841        if !state.width_status.is_resizing() {
842            self.content_widget(size).draw(
843                &tree.children[self.content_state_index(size)],
844                renderer,
845                theme,
846                style,
847                layout.child(2),
848                cursor,
849                viewport,
850            );
851        }
852    }
853
854    #[allow(clippy::too_many_arguments)]
855    pub(super) fn draw(
856        &self,
857        tree: &Tree,
858        renderer: &mut Renderer,
859        theme: &Theme,
860        style: &iced_core::renderer::Style,
861        layout: Layout<'_>,
862        cursor: Cursor,
863        viewport: &Rectangle,
864    ) {
865        let size = current_size(tree);
866        self.size_draw(size, tree, renderer, theme, style, layout, cursor, viewport);
867    }
868
869    pub(super) fn size_mouse_interaction(
870        &self,
871        size: Size,
872        tree: &Tree,
873        layout: Layout<'_>,
874        cursor: Cursor,
875        viewport: &Rectangle,
876        renderer: &Renderer,
877    ) -> Interaction {
878        let header_layout = layout.child(0);
879        let is_over_header = cursor.is_over(header_layout.bounds());
880
881        if !size.is_collapsed() && is_over_header {
882            return <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::mouse_interaction(
883                &self.header,
884                &tree.children[0],
885                header_layout,
886                cursor,
887                viewport,
888                renderer,
889            );
890        }
891
892        let launcher_layout = layout.child(1);
893        let is_over_launcher = cursor.is_over(launcher_layout.bounds());
894
895        if self.is_launcher_visible && is_over_launcher {
896            return self.launcher.mouse_interaction(
897                &tree.children[1],
898                launcher_layout,
899                cursor,
900                viewport,
901                renderer,
902            );
903        }
904
905        let content_state_index = self.content_state_index(size);
906
907        self.content_widget(size).mouse_interaction(
908            &tree.children[content_state_index],
909            layout.child(2),
910            cursor,
911            viewport,
912            renderer,
913        )
914    }
915
916    pub(super) fn mouse_interaction(
917        &self,
918        tree: &Tree,
919        layout: Layout<'_>,
920        cursor: Cursor,
921        viewport: &Rectangle,
922        renderer: &Renderer,
923    ) -> Interaction {
924        let size = current_size(tree);
925        self.size_mouse_interaction(size, tree, layout, cursor, viewport, renderer)
926    }
927
928    pub(super) fn size_overlay<'b>(
929        &'b mut self,
930        size: Size,
931        tree: &'b mut Tree,
932        layout: Layout<'b>,
933        renderer: &Renderer,
934        viewport: &Rectangle,
935        translation: Vector,
936    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
937        let idx = self.content_state_index(size);
938
939        self.content_widget_mut(size).overlay(
940            &mut tree.children[idx],
941            layout.child(2),
942            renderer,
943            viewport,
944            translation,
945        )
946    }
947
948    pub(super) fn overlay<'b>(
949        &'b mut self,
950        tree: &'b mut Tree,
951        layout: Layout<'b>,
952        renderer: &Renderer,
953        viewport: &Rectangle,
954        translation: Vector,
955    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
956        let size = current_size(tree);
957        self.size_overlay(size, tree, layout, renderer, viewport, translation)
958    }
959}
960
961pub(super) fn current_size(tree: &Tree) -> Size {
962    tree.state.downcast_ref::<State>().current_size()
963}
964
965pub(super) fn is_collapsed(tree: &Tree) -> bool {
966    tree.state.downcast_ref::<State>().is_collapsed()
967}
968
969pub(super) fn resize(tree: &mut Tree, target_size: Size, now: Instant) {
970    let width_status = &mut tree.state.downcast_mut::<State>().width_status;
971
972    let from_size = match width_status {
973        WidthStatus::Fixed { current_size: size } | WidthStatus::Resizing { to_size: size, .. } => {
974            size
975        }
976    };
977
978    if *from_size == target_size {
979        return;
980    }
981
982    *width_status = WidthStatus::Resizing {
983        from_size: *from_size,
984        to_size: target_size,
985        animation: Animation::new(false)
986            .quick()
987            .easing(Easing::EaseOutExpo)
988            .go(true, now),
989        now,
990        last_progress: None,
991    };
992}
993
994pub(super) fn resize_fixed(tree: &mut Tree, target_size: Size) {
995    let width_status = &mut tree.state.downcast_mut::<State>().width_status;
996
997    *width_status = WidthStatus::Fixed {
998        current_size: target_size,
999    }
1000}
1001
1002#[derive(Debug)]
1003pub(super) struct State {
1004    content_sizes: Vec<Option<IcedSize<Length>>>,
1005    width_status: WidthStatus,
1006}
1007
1008impl State {
1009    fn new(current_size: Size, content_sizes: Vec<Option<IcedSize<Length>>>) -> Self {
1010        State {
1011            content_sizes,
1012            width_status: WidthStatus::Fixed { current_size },
1013        }
1014    }
1015
1016    fn current_size(&self) -> Size {
1017        match self.width_status {
1018            WidthStatus::Fixed { current_size }
1019            | WidthStatus::Resizing {
1020                to_size: current_size,
1021                ..
1022            } => current_size,
1023        }
1024    }
1025
1026    fn is_collapsed(&self) -> bool {
1027        matches!(self.current_size(), Size::Collapsed)
1028    }
1029
1030    pub(super) fn is_resizing(&self) -> bool {
1031        self.width_status.is_resizing()
1032    }
1033}
1034
1035#[derive(Debug)]
1036enum WidthStatus {
1037    Fixed {
1038        current_size: Size,
1039    },
1040    Resizing {
1041        from_size: Size,
1042        to_size: Size,
1043        animation: Animation<bool>,
1044        now: Instant,
1045        // Track previous progress to notify of layout update when changed
1046        last_progress: Option<f32>,
1047    },
1048}
1049
1050impl WidthStatus {
1051    fn new(current_size: Size) -> Self {
1052        Self::Fixed { current_size }
1053    }
1054
1055    fn is_resizing(&self) -> bool {
1056        matches!(self, WidthStatus::Resizing { .. })
1057    }
1058}