Skip to main content

cosmic/widget/
cards.rs

1//! An expandable stack of cards
2use std::time::Duration;
3
4use crate::anim;
5use crate::theme::THEME;
6use crate::widget::card::style::Style;
7use crate::widget::icon::{self, Handle};
8use crate::widget::{button, column, row, text};
9use float_cmp::approx_eq;
10use iced::widget;
11use iced_core::border::Radius;
12use iced_core::id::Id;
13use iced_core::layout::Node;
14use iced_core::renderer::Quad;
15use iced_core::widget::{Tree, tree};
16use iced_core::{Border, Element, Event, Length, Shadow, Size, Vector, Widget, window};
17
18const ICON_SIZE: u16 = 16;
19const TOP_SPACING: u16 = 4;
20const VERTICAL_SPACING: f32 = 8.0;
21const PADDING: u16 = 16;
22const BG_CARD_VISIBLE_HEIGHT: f32 = 4.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,
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                    let guard = THEME.lock().unwrap();
341                    let radius_xs = guard.cosmic().radius_xs();
342
343                    let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP;
344                    let node = Node::new(Size::new(width - 2.0 * margin, radius_xs[0] * 2.0))
345                        .translate(Vector::new(
346                            margin,
347                            size.height - radius_xs[0] * 2.0 + BG_CARD_VISIBLE_HEIGHT,
348                        ));
349                    size.height += BG_CARD_VISIBLE_HEIGHT;
350                    children.push(node);
351                }
352                break;
353            }
354
355            if i + 1 < count {
356                size.height += VERTICAL_SPACING;
357            }
358        }
359
360        Node::with_children(size, children)
361    }
362
363    fn draw(
364        &self,
365        state: &iced_core::widget::Tree,
366        renderer: &mut Renderer,
367        theme: &crate::Theme,
368        style: &iced_core::renderer::Style,
369        layout: iced_core::Layout<'_>,
370        cursor: iced_core::mouse::Cursor,
371        viewport: &iced_core::Rectangle,
372    ) {
373        let my_state = state.state.downcast_ref::<State>();
374
375        // there are 4 cases for drawing
376        // 1. empty entries list
377        //      Nothing to draw
378        // 2. un-expanded
379        //      go through the layout, draw the card, the inner card, and the bg cards
380        // 3. expanding / unexpanding
381        //      go through the layout. draw each card and its inner card
382        // 4. expanded =>
383        //      go through the layout. draw the top bar, and do all of 3
384        // cards may be hovered
385        // any buttons may have a hover state as well
386        if self.elements.is_empty() {
387            return;
388        }
389
390        let t = my_state.anim.t(self.duration, self.expanded);
391        let fully_unexpanded = self.fully_unexpanded(t);
392        let fully_expanded = self.fully_expanded(t);
393
394        let mut layout = layout.children();
395        let mut tree_children = state.children.iter();
396
397        if fully_expanded {
398            let show_less = &self.show_less_button;
399            let clear_all = &self.clear_all_button;
400
401            let show_less_layout = layout.next().unwrap();
402            let clear_all_layout = layout.next().unwrap();
403
404            show_less.as_widget().draw(
405                tree_children.next().unwrap(),
406                renderer,
407                theme,
408                style,
409                show_less_layout,
410                cursor,
411                viewport,
412            );
413
414            clear_all.as_widget().draw(
415                tree_children.next().unwrap(),
416                renderer,
417                theme,
418                style,
419                clear_all_layout,
420                cursor,
421                viewport,
422            );
423        } else {
424            _ = tree_children.next();
425            _ = tree_children.next();
426        }
427
428        let radius_xs = theme.cosmic().radius_xs();
429        // Draw first to appear behind
430        if fully_unexpanded {
431            use crate::widget::card::style::Catalog as _;
432            let card_layout = layout.next().unwrap();
433            let appearance = theme.default();
434            let bg_layout = layout.collect::<Vec<_>>();
435            for (i, layout) in (0..2).zip(bg_layout).rev() {
436                renderer.fill_quad(
437                    Quad {
438                        bounds: layout.bounds(),
439                        border: Border {
440                            radius: Radius::from([0.0, 0.0, radius_xs[2], radius_xs[3]]),
441                            ..Default::default()
442                        },
443                        shadow: Shadow::default(),
444                        snap: true,
445                    },
446                    if i == 0 {
447                        appearance.card_1
448                    } else {
449                        appearance.card_2
450                    },
451                );
452            }
453            self.elements[0].as_widget().draw(
454                tree_children.next().unwrap(),
455                renderer,
456                theme,
457                style,
458                card_layout,
459                cursor,
460                viewport,
461            );
462        } else {
463            let layout = layout.collect::<Vec<_>>();
464            // draw in reverse order so later cards appear behind earlier cards
465            for ((inner, layout), c_state) in self
466                .elements
467                .iter()
468                .rev()
469                .zip(layout.into_iter().rev())
470                .zip(tree_children.rev())
471            {
472                inner
473                    .as_widget()
474                    .draw(c_state, renderer, theme, style, layout, cursor, viewport);
475            }
476        }
477    }
478
479    fn update(
480        &mut self,
481        state: &mut Tree,
482        event: &iced_core::Event,
483        layout: iced_core::Layout<'_>,
484        cursor: iced_core::mouse::Cursor,
485        renderer: &Renderer,
486        clipboard: &mut dyn iced_core::Clipboard,
487        shell: &mut iced_core::Shell<'_, Message>,
488        viewport: &iced_core::Rectangle,
489    ) {
490        if self.elements.is_empty() {
491            return;
492        }
493
494        if let Event::Window(window::Event::RedrawRequested(_)) = event {
495            let state = state.state.downcast_mut::<State>();
496
497            state.anim.anim_done(self.duration);
498            if state.anim.last_change.is_some() {
499                shell.request_redraw();
500                shell.invalidate_layout();
501            }
502        }
503
504        let my_state = state.state.downcast_ref::<State>();
505
506        let mut layout = layout.children();
507        let mut tree_children = state.children.iter_mut();
508        let t = my_state.anim.t(self.duration, self.expanded);
509        let fully_expanded = self.fully_expanded(t);
510        let fully_unexpanded = self.fully_unexpanded(t);
511        let show_less_state = tree_children.next();
512        let clear_all_state = tree_children.next();
513
514        if fully_expanded {
515            let c_layout = layout.next().unwrap();
516            let state = show_less_state.unwrap();
517            self.show_less_button.as_widget_mut().update(
518                state, event, c_layout, cursor, renderer, clipboard, shell, viewport,
519            );
520
521            if shell.is_event_captured() {
522                return;
523            }
524
525            let c_layout = layout.next().unwrap();
526            let state = clear_all_state.unwrap();
527            self.clear_all_button.as_widget_mut().update(
528                state, &event, c_layout, cursor, renderer, clipboard, shell, viewport,
529            );
530        }
531
532        if shell.is_event_captured() {
533            return;
534        }
535
536        for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) {
537            inner.as_widget_mut().update(
538                c_state, &event, layout, cursor, renderer, clipboard, shell, viewport,
539            );
540            if shell.is_event_captured() || fully_unexpanded {
541                break;
542            }
543        }
544    }
545
546    fn size(&self) -> Size<Length> {
547        Size::new(self.width, Length::Shrink)
548    }
549
550    fn tag(&self) -> tree::Tag {
551        tree::Tag::of::<State>()
552    }
553
554    fn state(&self) -> tree::State {
555        tree::State::new(State::default())
556    }
557
558    fn id(&self) -> Option<Id> {
559        Some(self.id.clone())
560    }
561
562    fn set_id(&mut self, id: Id) {
563        self.id = id;
564    }
565}
566
567impl<'a, Message> From<Cards<'a, Message>> for Element<'a, Message, crate::Theme, crate::Renderer>
568where
569    Message: Clone + 'a,
570{
571    fn from(cards: Cards<'a, Message>) -> Self {
572        Self::new(cards)
573    }
574}
575
576#[derive(Debug, Default)]
577pub struct State {
578    anim: anim::State,
579}