1use 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#[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: 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 .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 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 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 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 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}