1use 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#[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#[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 #[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 .collect(),
183 width: Length::Shrink,
184 anim_multiplier: 1.0,
185 expanded,
186 duration: Duration::from_millis(200),
187 }
188 }
189
190 #[must_use]
192 pub fn width(mut self, width: Length) -> Self {
193 self.width = width;
194 self
195 }
196
197 #[must_use]
198 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 for i in 1..self.elements.len().min(3) {
343 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 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 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 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}