Skip to main content

cosmic/widget/
cards.rs

1//! An expandable stack of cards
2use std::time::Duration;
3
4use crate::anim;
5use crate::widget::card::style::Style;
6use crate::widget::icon::{self, Handle};
7use crate::widget::{button, column, row, text};
8use float_cmp::approx_eq;
9use iced::widget;
10use iced_core::border::Radius;
11use iced_core::id::Id;
12use iced_core::layout::Node;
13use iced_core::renderer::Quad;
14use iced_core::widget::{Tree, tree};
15use iced_core::{Border, Element, Event, Length, Shadow, Size, Vector, Widget, window};
16
17const ICON_SIZE: u16 = 16;
18const TOP_SPACING: u16 = 4;
19const VERTICAL_SPACING: f32 = 8.0;
20const PADDING: u16 = 16;
21const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0;
22const BG_CARD_BORDER_RADIUS: f32 = 8.0;
23const BG_CARD_MARGIN_STEP: f32 = 8.0;
24
25/// get an expandable stack of cards
26#[allow(clippy::too_many_arguments)]
27pub fn cards<'a, Message, F, G>(
28    id: widget::Id,
29    card_inner_elements: Vec<Element<'a, Message, crate::Theme, crate::Renderer>>,
30    on_clear_all: Message,
31    on_show_more: Option<F>,
32    on_activate: Option<G>,
33    show_more_label: &'a str,
34    show_less_label: &'a str,
35    clear_all_label: &'a str,
36    show_less_icon: Option<Handle>,
37    expanded: bool,
38) -> Cards<'a, Message, crate::Renderer>
39where
40    Message: 'static + Clone,
41    F: 'a + Fn(bool) -> Message,
42    G: 'a + Fn(usize) -> Message,
43{
44    Cards::new(
45        id,
46        card_inner_elements,
47        on_clear_all,
48        on_show_more,
49        on_activate,
50        show_more_label,
51        show_less_label,
52        clear_all_label,
53        show_less_icon,
54        expanded,
55    )
56}
57
58impl<'a, Message, Renderer> Cards<'a, Message, Renderer>
59where
60    Renderer: iced_core::text::Renderer,
61{
62    fn fully_expanded(&self, t: f32) -> bool {
63        self.expanded && self.elements.len() > 1 && self.can_show_more && approx_eq!(f32, t, 1.0)
64    }
65
66    fn fully_unexpanded(&self, t: f32) -> bool {
67        self.elements.len() == 1
68            || (!self.expanded && (!self.can_show_more || approx_eq!(f32, t, 0.0)))
69    }
70}
71
72/// An expandable stack of cards.
73#[allow(missing_debug_implementations)]
74pub struct Cards<'a, Message, Renderer = crate::Renderer>
75where
76    Renderer: iced_core::text::Renderer,
77{
78    id: Id,
79    show_less_button: Element<'a, Message, crate::Theme, Renderer>,
80    clear_all_button: Element<'a, Message, crate::Theme, Renderer>,
81    elements: Vec<Element<'a, Message, crate::Theme, Renderer>>,
82    expanded: bool,
83    can_show_more: bool,
84    width: Length,
85    anim_multiplier: f32,
86    duration: Duration,
87}
88
89impl<'a, Message> Cards<'a, Message, crate::Renderer>
90where
91    Message: Clone + 'static,
92{
93    /// Get an expandable stack of cards
94    #[allow(clippy::too_many_arguments)]
95    pub fn new<F, G>(
96        id: widget::Id,
97        card_inner_elements: Vec<Element<'a, Message, crate::Theme, crate::Renderer>>,
98        on_clear_all: Message,
99        on_show_more: Option<F>,
100        on_activate: Option<G>,
101        show_more_label: &'a str,
102        show_less_label: &'a str,
103        clear_all_label: &'a str,
104        show_less_icon: Option<Handle>,
105        expanded: bool,
106    ) -> Self
107    where
108        F: 'a + Fn(bool) -> Message,
109        G: 'a + Fn(usize) -> Message,
110    {
111        let can_show_more = card_inner_elements.len() > 1 && on_show_more.is_some();
112
113        Self {
114            can_show_more,
115            id: Id::unique(),
116            show_less_button: {
117                let mut show_less_children = Vec::with_capacity(3);
118                if let Some(source) = show_less_icon {
119                    show_less_children.push(icon::icon(source).size(ICON_SIZE).into());
120                }
121                show_less_children.push(text::body(show_less_label).width(Length::Shrink).into());
122                show_less_children.push(
123                    icon::from_name("pan-up-symbolic")
124                        .size(ICON_SIZE)
125                        .icon()
126                        .into(),
127                );
128
129                let button_content = row::with_children(show_less_children)
130                    .align_y(iced_core::Alignment::Center)
131                    .spacing(TOP_SPACING)
132                    .width(Length::Shrink);
133
134                Element::from(
135                    button::custom(button_content)
136                        .class(crate::theme::Button::Text)
137                        .width(Length::Shrink)
138                        .on_press_maybe(on_show_more.as_ref().map(|f| f(false)))
139                        .padding([PADDING / 2, PADDING]),
140                )
141            },
142            clear_all_button: Element::from(
143                button::custom(text(clear_all_label))
144                    .class(crate::theme::Button::Text)
145                    .width(Length::Shrink)
146                    .on_press(on_clear_all)
147                    .padding([PADDING / 2, PADDING]),
148            ),
149            elements: card_inner_elements
150                .into_iter()
151                .enumerate()
152                .map(|(i, w)| {
153                    let custom_content = if i == 0 && !expanded && can_show_more {
154                        column::with_capacity(2)
155                            .push(w)
156                            .push(text::caption(show_more_label))
157                            .spacing(VERTICAL_SPACING)
158                            .align_x(iced_core::Alignment::Center)
159                            .into()
160                    } else {
161                        w
162                    };
163
164                    let b = crate::iced::widget::button(custom_content)
165                        .class(crate::theme::iced::Button::Card)
166                        .padding(PADDING);
167                    if i == 0 && !expanded && can_show_more {
168                        b.on_press_maybe(on_show_more.as_ref().map(|f| f(true)))
169                    } else {
170                        b.on_press_maybe(on_activate.as_ref().map(|f| f(i)))
171                    }
172                    .into()
173                })
174                // we will set the width of the container to shrink, then when laying out the top bar
175                // we will set the fill limit to the max of the shrink top bar width and the max shrink width of the
176                // cards
177                .collect(),
178            width: Length::Shrink,
179            anim_multiplier: 1.0,
180            expanded,
181            duration: Duration::from_millis(200),
182        }
183    }
184
185    ///  Set the width of the cards stack
186    #[must_use]
187    pub fn width(mut self, width: Length) -> Self {
188        self.width = width;
189        self
190    }
191
192    #[must_use]
193    /// The default animation time is 100ms, to speed up the toggle
194    /// animation use a value less than 1.0, and to slow down the
195    /// animation use a value greater than 1.0.
196    pub fn anim_multiplier(mut self, multiplier: f32) -> Self {
197        self.anim_multiplier = multiplier;
198        self
199    }
200
201    pub fn duration(mut self, dur: Duration) -> Self {
202        self.duration = dur;
203        self
204    }
205
206    pub fn id(mut self, id: Id) -> Self {
207        self.id = id;
208        self
209    }
210}
211
212impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer> for Cards<'a, Message, Renderer>
213where
214    Message: 'a + Clone,
215    Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer,
216{
217    fn children(&self) -> Vec<Tree> {
218        [&self.show_less_button, &self.clear_all_button]
219            .iter()
220            .map(|w| Tree::new(w.as_widget()))
221            .chain(self.elements.iter().map(|w| Tree::new(w.as_widget())))
222            .collect()
223    }
224
225    fn diff(&mut self, tree: &mut Tree) {
226        let mut children: Vec<_> = vec![
227            self.show_less_button.as_widget_mut(),
228            self.clear_all_button.as_widget_mut(),
229        ]
230        .into_iter()
231        .chain(
232            self.elements
233                .iter_mut()
234                .map(iced_core::Element::as_widget_mut),
235        )
236        .collect();
237
238        tree.diff_children(children.as_mut_slice());
239    }
240
241    #[allow(clippy::too_many_lines)]
242    fn layout(
243        &mut self,
244        tree: &mut Tree,
245        renderer: &Renderer,
246        limits: &iced_core::layout::Limits,
247    ) -> iced_core::layout::Node {
248        let my_state = tree.state.downcast_ref::<State>();
249
250        let mut children = Vec::with_capacity(1 + self.elements.len());
251        let mut size = Size::new(0.0, 0.0);
252        let tree_children = &mut tree.children;
253        let count = self.elements.len();
254        if self.elements.is_empty() {
255            return Node::with_children(Size::new(1., 1.), children);
256        }
257        let s = anim::smootherstep(my_state.anim.t(self.duration, self.expanded));
258        let fully_expanded: bool = self.fully_expanded(s);
259        let fully_unexpanded: bool = self.fully_unexpanded(s);
260
261        let show_less = &mut self.show_less_button;
262        let clear_all = &mut self.clear_all_button;
263
264        let show_less_node = if self.can_show_more {
265            show_less
266                .as_widget_mut()
267                .layout(&mut tree_children[0], renderer, limits)
268        } else {
269            Node::new(Size::default())
270        };
271        let clear_all_node =
272            clear_all
273                .as_widget_mut()
274                .layout(&mut tree_children[1], renderer, limits);
275        size.width += show_less_node.size().width + clear_all_node.size().width;
276
277        let custom_limits = limits.min_width(size.width);
278        for (c, t) in self.elements.iter_mut().zip(tree_children[2..].iter_mut()) {
279            let card_node = c.as_widget_mut().layout(t, renderer, &custom_limits);
280            size.width = size.width.max(card_node.size().width);
281        }
282
283        if fully_expanded {
284            let show_less = &mut self.show_less_button;
285            let clear_all = &mut self.clear_all_button;
286
287            let show_less_node = if self.can_show_more {
288                show_less
289                    .as_widget_mut()
290                    .layout(&mut tree_children[0], renderer, limits)
291            } else {
292                Node::new(Size::default())
293            };
294            let clear_all_node = if self.can_show_more {
295                let mut n =
296                    clear_all
297                        .as_widget_mut()
298                        .layout(&mut tree_children[1], renderer, limits);
299                let clear_all_node_size = n.size();
300                n = clear_all_node
301                    .translate(Vector::new(size.width - clear_all_node_size.width, 0.0));
302                size.height += show_less_node.size().height.max(n.size().height) + VERTICAL_SPACING;
303                n
304            } else {
305                Node::new(Size::default())
306            };
307
308            children.push(show_less_node);
309            children.push(clear_all_node);
310        }
311
312        let custom_limits = limits
313            .min_width(size.width)
314            .max_width(size.width)
315            .width(Length::Fixed(size.width));
316
317        for (i, (c, t)) in self
318            .elements
319            .iter_mut()
320            .zip(tree_children[2..].iter_mut())
321            .enumerate()
322        {
323            let progress = s * size.height;
324            let card_node = c
325                .as_widget_mut()
326                .layout(t, renderer, &custom_limits)
327                .translate(Vector::new(0.0, progress));
328
329            size.height = size.height.max(progress + card_node.size().height);
330
331            children.push(card_node);
332
333            if fully_unexpanded {
334                let width = children.last().unwrap().bounds().width;
335
336                // push the background card nodes
337                for i in 1..self.elements.len().min(3) {
338                    // height must be 16px for 8px padding
339                    // but we only want 4px visible
340
341                    let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP;
342                    let node =
343                        Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0))
344                            .translate(Vector::new(
345                                margin,
346                                size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT,
347                            ));
348                    size.height += BG_CARD_VISIBLE_HEIGHT;
349                    children.push(node);
350                }
351                break;
352            }
353
354            if i + 1 < count {
355                size.height += VERTICAL_SPACING;
356            }
357        }
358
359        Node::with_children(size, children)
360    }
361
362    fn draw(
363        &self,
364        state: &iced_core::widget::Tree,
365        renderer: &mut Renderer,
366        theme: &crate::Theme,
367        style: &iced_core::renderer::Style,
368        layout: iced_core::Layout<'_>,
369        cursor: iced_core::mouse::Cursor,
370        viewport: &iced_core::Rectangle,
371    ) {
372        let my_state = state.state.downcast_ref::<State>();
373
374        // there are 4 cases for drawing
375        // 1. empty entries list
376        //      Nothing to draw
377        // 2. un-expanded
378        //      go through the layout, draw the card, the inner card, and the bg cards
379        // 3. expanding / unexpanding
380        //      go through the layout. draw each card and its inner card
381        // 4. expanded =>
382        //      go through the layout. draw the top bar, and do all of 3
383        // cards may be hovered
384        // any buttons may have a hover state as well
385        if self.elements.is_empty() {
386            return;
387        }
388
389        let t = my_state.anim.t(self.duration, self.expanded);
390        let fully_unexpanded = self.fully_unexpanded(t);
391        let fully_expanded = self.fully_expanded(t);
392
393        let mut layout = layout.children();
394        let mut tree_children = state.children.iter();
395
396        if fully_expanded {
397            let show_less = &self.show_less_button;
398            let clear_all = &self.clear_all_button;
399
400            let show_less_layout = layout.next().unwrap();
401            let clear_all_layout = layout.next().unwrap();
402
403            show_less.as_widget().draw(
404                tree_children.next().unwrap(),
405                renderer,
406                theme,
407                style,
408                show_less_layout,
409                cursor,
410                viewport,
411            );
412
413            clear_all.as_widget().draw(
414                tree_children.next().unwrap(),
415                renderer,
416                theme,
417                style,
418                clear_all_layout,
419                cursor,
420                viewport,
421            );
422        } else {
423            _ = tree_children.next();
424            _ = tree_children.next();
425        }
426
427        // Draw first to appear behind
428        if fully_unexpanded {
429            let card_layout = layout.next().unwrap();
430            let appearance = Style::default();
431            let bg_layout = layout.collect::<Vec<_>>();
432            for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() {
433                renderer.fill_quad(
434                    Quad {
435                        bounds: layout.bounds(),
436                        border: Border {
437                            radius: Radius::from([
438                                0.0,
439                                0.0,
440                                BG_CARD_BORDER_RADIUS,
441                                BG_CARD_BORDER_RADIUS,
442                            ]),
443                            ..Default::default()
444                        },
445                        shadow: Shadow::default(),
446                        snap: true,
447                    },
448                    if i == 0 {
449                        appearance.card_1
450                    } else {
451                        appearance.card_2
452                    },
453                );
454            }
455            self.elements[0].as_widget().draw(
456                tree_children.next().unwrap(),
457                renderer,
458                theme,
459                style,
460                card_layout,
461                cursor,
462                viewport,
463            );
464        } else {
465            let layout = layout.collect::<Vec<_>>();
466            // draw in reverse order so later cards appear behind earlier cards
467            for ((inner, layout), c_state) in self
468                .elements
469                .iter()
470                .rev()
471                .zip(layout.into_iter().rev())
472                .zip(tree_children.rev())
473            {
474                inner
475                    .as_widget()
476                    .draw(c_state, renderer, theme, style, layout, cursor, viewport);
477            }
478        }
479    }
480
481    fn update(
482        &mut self,
483        state: &mut Tree,
484        event: &iced_core::Event,
485        layout: iced_core::Layout<'_>,
486        cursor: iced_core::mouse::Cursor,
487        renderer: &Renderer,
488        clipboard: &mut dyn iced_core::Clipboard,
489        shell: &mut iced_core::Shell<'_, Message>,
490        viewport: &iced_core::Rectangle,
491    ) {
492        if self.elements.is_empty() {
493            return;
494        }
495
496        if let Event::Window(window::Event::RedrawRequested(_)) = event {
497            let state = state.state.downcast_mut::<State>();
498
499            state.anim.anim_done(self.duration);
500            if state.anim.last_change.is_some() {
501                shell.request_redraw();
502                shell.invalidate_layout();
503            }
504        }
505
506        let my_state = state.state.downcast_ref::<State>();
507
508        let mut layout = layout.children();
509        let mut tree_children = state.children.iter_mut();
510        let t = my_state.anim.t(self.duration, self.expanded);
511        let fully_expanded = self.fully_expanded(t);
512        let fully_unexpanded = self.fully_unexpanded(t);
513        let show_less_state = tree_children.next();
514        let clear_all_state = tree_children.next();
515
516        if fully_expanded {
517            let c_layout = layout.next().unwrap();
518            let state = show_less_state.unwrap();
519            self.show_less_button.as_widget_mut().update(
520                state, event, c_layout, cursor, renderer, clipboard, shell, viewport,
521            );
522
523            if shell.is_event_captured() {
524                return;
525            }
526
527            let c_layout = layout.next().unwrap();
528            let state = clear_all_state.unwrap();
529            self.clear_all_button.as_widget_mut().update(
530                state, &event, c_layout, cursor, renderer, clipboard, shell, viewport,
531            );
532        }
533
534        if shell.is_event_captured() {
535            return;
536        }
537
538        for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) {
539            inner.as_widget_mut().update(
540                c_state, &event, layout, cursor, renderer, clipboard, shell, viewport,
541            );
542            if shell.is_event_captured() || fully_unexpanded {
543                break;
544            }
545        }
546    }
547
548    fn size(&self) -> Size<Length> {
549        Size::new(self.width, Length::Shrink)
550    }
551
552    fn tag(&self) -> tree::Tag {
553        tree::Tag::of::<State>()
554    }
555
556    fn state(&self) -> tree::State {
557        tree::State::new(State::default())
558    }
559
560    fn id(&self) -> Option<Id> {
561        Some(self.id.clone())
562    }
563
564    fn set_id(&mut self, id: Id) {
565        self.id = id;
566    }
567}
568
569impl<'a, Message> From<Cards<'a, Message>> for Element<'a, Message, crate::Theme, crate::Renderer>
570where
571    Message: Clone + 'a,
572{
573    fn from(cards: Cards<'a, Message>) -> Self {
574        Self::new(cards)
575    }
576}
577
578#[derive(Debug, Default)]
579pub struct State {
580    anim: anim::State,
581}