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
32pub 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 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 #[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 pub fn header_size(mut self, size: impl Into<Pixels>) -> Self {
153 self.header = self.header.size(size);
154 self
155 }
156
157 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 pub fn header_font(mut self, font: impl Into<Renderer::Font>) -> Self {
165 self.header = self.header.font(font);
166 self
167 }
168
169 pub fn header_wrapping(mut self, wrapping: Wrapping) -> Self {
171 self.header = self.header.wrapping(wrapping);
172 self
173 }
174
175 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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 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}