1pub 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
32pub 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 #[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 #[must_use]
75 pub fn width(mut self, width: impl Into<Length>) -> Self {
76 self.width = width.into();
77 self
78 }
79
80 #[must_use]
82 pub fn height(mut self, height: impl Into<Length>) -> Self {
83 self.height = height.into();
84 self
85 }
86
87 #[must_use]
89 pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
90 self.spacing = amount.into().0;
91 self
92 }
93
94 #[must_use]
96 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
97 self.padding = padding.into();
98 self
99 }
100
101 #[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 #[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 #[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 #[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 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 while self.should_shrink(available_width, ¤t_sizes) {
271 let max_size = self
272 .groups
273 .iter()
274 .zip(¤t_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(¤t_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 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 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 group.draw(
439 group_tree,
440 renderer,
441 theme,
442 style,
443 layouts.next().unwrap(),
444 cursor,
445 viewport,
446 );
447
448 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 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#[derive(Debug, Clone, Copy, PartialEq, Default)]
702pub struct Style {
703 pub background: Option<Background>,
705 pub border: Border,
707 pub shadow: Shadow,
709 pub separator_color: Option<Color>,
711 pub snap: bool,
713}
714
715impl Style {
716 pub fn border(self, border: impl Into<Border>) -> Self {
718 Self {
719 border: border.into(),
720 ..self
721 }
722 }
723
724 pub fn background(self, background: impl Into<Background>) -> Self {
726 Self {
727 background: Some(background.into()),
728 ..self
729 }
730 }
731
732 pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
734 Self {
735 shadow: shadow.into(),
736 ..self
737 }
738 }
739
740 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
767pub trait Catalog {
769 type Class<'a>;
771
772 fn default<'a>() -> Self::Class<'a>;
774
775 fn style(&self, class: &Self::Class<'_>) -> Style;
777}
778
779pub 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}