1use 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#[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#[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 #[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 .collect(),
178 width: Length::Shrink,
179 anim_multiplier: 1.0,
180 expanded,
181 duration: Duration::from_millis(200),
182 }
183 }
184
185 #[must_use]
187 pub fn width(mut self, width: Length) -> Self {
188 self.width = width;
189 self
190 }
191
192 #[must_use]
193 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 for i in 1..self.elements.len().min(3) {
338 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 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 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 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}