cosmic/widget/
cards.rs

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