1use iced::{
6 advanced::{
7 layout::{Limits, Node},
8 renderer,
9 text::LineHeight,
10 widget::{Operation, Tree},
11 Clipboard, Layout, Shell, Widget,
12 },
13 alignment::{Horizontal, Vertical},
14 event,
15 mouse::{self, Cursor},
16 touch,
17 widget::text::Wrapping,
18 Alignment, Border, Color, Element, Event, Length, Padding, Pixels, Point, Rectangle, Shadow,
19 Size, Vector,
20};
21use iced_fonts::{
22 required::{icon_to_string, RequiredIcons},
23 REQUIRED_FONT,
24};
25
26pub use crate::style::{
27 card::{Catalog, Style},
28 status::{Status, StyleFn},
29};
30
31const DEFAULT_PADDING: Padding = Padding::new(10.0);
33
34#[allow(missing_debug_implementations)]
55pub struct Card<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer>
56where
57 Renderer: renderer::Renderer,
58 Theme: Catalog,
59{
60 width: Length,
62 height: Length,
64 max_width: f32,
66 max_height: f32,
68 padding_head: Padding,
70 padding_body: Padding,
72 padding_foot: Padding,
74 close_size: Option<f32>,
76 on_close: Option<Message>,
78 head: Element<'a, Message, Theme, Renderer>,
80 body: Element<'a, Message, Theme, Renderer>,
82 foot: Option<Element<'a, Message, Theme, Renderer>>,
84 class: Theme::Class<'a>,
86}
87
88impl<'a, Message, Theme, Renderer> Card<'a, Message, Theme, Renderer>
89where
90 Renderer: renderer::Renderer,
91 Theme: Catalog,
92{
93 pub fn new<H, B>(head: H, body: B) -> Self
99 where
100 H: Into<Element<'a, Message, Theme, Renderer>>,
101 B: Into<Element<'a, Message, Theme, Renderer>>,
102 {
103 Card {
104 width: Length::Fill,
105 height: Length::Shrink,
106 max_width: u32::MAX as f32,
107 max_height: u32::MAX as f32,
108 padding_head: DEFAULT_PADDING,
109 padding_body: DEFAULT_PADDING,
110 padding_foot: DEFAULT_PADDING,
111 close_size: None,
112 on_close: None,
113 head: head.into(),
114 body: body.into(),
115 foot: None,
116 class: Theme::default(),
117 }
118 }
119
120 #[must_use]
122 pub fn foot<F>(mut self, foot: F) -> Self
123 where
124 F: Into<Element<'a, Message, Theme, Renderer>>,
125 {
126 self.foot = Some(foot.into());
127 self
128 }
129
130 #[must_use]
132 pub fn close_size(mut self, size: f32) -> Self {
133 self.close_size = Some(size);
134 self
135 }
136
137 #[must_use]
139 pub fn height(mut self, height: impl Into<Length>) -> Self {
140 self.height = height.into();
141 self
142 }
143
144 #[must_use]
146 pub fn max_height(mut self, height: f32) -> Self {
147 self.max_height = height;
148 self
149 }
150
151 #[must_use]
153 pub fn max_width(mut self, width: f32) -> Self {
154 self.max_width = width;
155 self
156 }
157
158 #[must_use]
163 pub fn on_close(mut self, msg: Message) -> Self {
164 self.on_close = Some(msg);
165 self
166 }
167
168 #[must_use]
173 pub fn padding(mut self, padding: Padding) -> Self {
174 self.padding_head = padding;
175 self.padding_body = padding;
176 self.padding_foot = padding;
177 self
178 }
179
180 #[must_use]
182 pub fn padding_head(mut self, padding: Padding) -> Self {
183 self.padding_head = padding;
184 self
185 }
186
187 #[must_use]
189 pub fn padding_body(mut self, padding: Padding) -> Self {
190 self.padding_body = padding;
191 self
192 }
193
194 #[must_use]
196 pub fn padding_foot(mut self, padding: Padding) -> Self {
197 self.padding_foot = padding;
198 self
199 }
200
201 #[must_use]
203 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
204 where
205 Theme::Class<'a>: From<StyleFn<'a, Theme, Style>>,
206 {
207 self.class = (Box::new(style) as StyleFn<'a, Theme, Style>).into();
208 self
209 }
210
211 #[must_use]
213 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
214 self.class = class.into();
215 self
216 }
217
218 #[must_use]
220 pub fn width(mut self, width: impl Into<Length>) -> Self {
221 self.width = width.into();
222 self
223 }
224}
225
226impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
227 for Card<'a, Message, Theme, Renderer>
228where
229 Message: 'a + Clone,
230 Renderer: 'a + renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font>,
231 Theme: Catalog,
232{
233 fn children(&self) -> Vec<Tree> {
234 self.foot.as_ref().map_or_else(
235 || vec![Tree::new(&self.head), Tree::new(&self.body)],
236 |foot| {
237 vec![
238 Tree::new(&self.head),
239 Tree::new(&self.body),
240 Tree::new(foot),
241 ]
242 },
243 )
244 }
245
246 fn diff(&self, tree: &mut Tree) {
247 if let Some(foot) = self.foot.as_ref() {
248 tree.diff_children(&[&self.head, &self.body, foot]);
249 } else {
250 tree.diff_children(&[&self.head, &self.body]);
251 }
252 }
253
254 fn size(&self) -> Size<Length> {
255 Size {
256 width: self.width,
257 height: self.height,
258 }
259 }
260
261 fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
262 let limits = limits.max_width(self.max_width).max_height(self.max_height);
263
264 let head_node = head_node(
265 renderer,
266 &limits,
267 &self.head,
268 self.padding_head,
269 self.width,
270 self.on_close.is_some(),
271 self.close_size,
272 tree,
273 );
274
275 let limits = limits.shrink(Size::new(0.0, head_node.size().height));
276
277 let mut foot_node = self.foot.as_ref().map_or_else(Node::default, |foot| {
278 foot_node(renderer, &limits, foot, self.padding_foot, self.width, tree)
279 });
280 let limits = limits.shrink(Size::new(0.0, foot_node.size().height));
281 let mut body_node = body_node(
282 renderer,
283 &limits,
284 &self.body,
285 self.padding_body,
286 self.width,
287 tree,
288 );
289 let body_bounds = body_node.bounds();
290 body_node = body_node.move_to(Point::new(
291 body_bounds.x,
292 body_bounds.y + head_node.bounds().height,
293 ));
294
295 let foot_bounds = foot_node.bounds();
296
297 foot_node = foot_node.move_to(Point::new(
298 foot_bounds.x,
299 foot_bounds.y + head_node.bounds().height + body_node.bounds().height,
300 ));
301
302 Node::with_children(
303 Size::new(
304 body_node.size().width,
305 head_node.size().height + body_node.size().height + foot_node.size().height,
306 ),
307 vec![head_node, body_node, foot_node],
308 )
309 }
310
311 fn on_event(
312 &mut self,
313 state: &mut Tree,
314 event: Event,
315 layout: Layout<'_>,
316 cursor: Cursor,
317 renderer: &Renderer,
318 clipboard: &mut dyn Clipboard,
319 shell: &mut Shell<'_, Message>,
320 viewport: &Rectangle,
321 ) -> event::Status {
322 let mut children = layout.children();
323
324 let head_layout = children
325 .next()
326 .expect("widget: Layout should have a head layout");
327 let mut head_children = head_layout.children();
328 let head_status = self.head.as_widget_mut().on_event(
329 &mut state.children[0],
330 event.clone(),
331 head_children
332 .next()
333 .expect("widget: Layout should have a head content layout"),
334 cursor,
335 renderer,
336 clipboard,
337 shell,
338 viewport,
339 );
340
341 let close_status = head_children
342 .next()
343 .map_or(event::Status::Ignored, |close_layout| {
344 match event {
345 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
346 | Event::Touch(touch::Event::FingerPressed { .. }) => self
347 .on_close
348 .clone()
349 .filter(|_| {
352 close_layout
353 .bounds()
354 .contains(cursor.position().unwrap_or_default())
355 })
356 .map_or(event::Status::Ignored, |on_close| {
357 shell.publish(on_close);
358 event::Status::Captured
359 }),
360 _ => event::Status::Ignored,
361 }
362 });
363
364 let body_layout = children
365 .next()
366 .expect("widget: Layout should have a body layout");
367 let mut body_children = body_layout.children();
368 let body_status = self.body.as_widget_mut().on_event(
369 &mut state.children[1],
370 event.clone(),
371 body_children
372 .next()
373 .expect("widget: Layout should have a body content layout"),
374 cursor,
375 renderer,
376 clipboard,
377 shell,
378 viewport,
379 );
380
381 let foot_layout = children
382 .next()
383 .expect("widget: Layout should have a foot layout");
384 let mut foot_children = foot_layout.children();
385 let foot_status = self.foot.as_mut().map_or(event::Status::Ignored, |foot| {
386 foot.as_widget_mut().on_event(
387 &mut state.children[2],
388 event,
389 foot_children
390 .next()
391 .expect("widget: Layout should have a foot content layout"),
392 cursor,
393 renderer,
394 clipboard,
395 shell,
396 viewport,
397 )
398 });
399
400 head_status
401 .merge(close_status)
402 .merge(body_status)
403 .merge(foot_status)
404 }
405
406 fn mouse_interaction(
407 &self,
408 state: &Tree,
409 layout: Layout<'_>,
410 cursor: Cursor,
411 viewport: &Rectangle,
412 renderer: &Renderer,
413 ) -> mouse::Interaction {
414 let mut children = layout.children();
415
416 let head_layout = children
417 .next()
418 .expect("widget: Layout should have a head layout");
419 let mut head_children = head_layout.children();
420
421 let head = head_children
422 .next()
423 .expect("widget: Layout should have a head layout");
424 let close_layout = head_children.next();
425
426 let is_mouse_over_close = close_layout.is_some_and(|layout| {
427 let bounds = layout.bounds();
428 bounds.contains(cursor.position().unwrap_or_default())
429 });
430
431 let mouse_interaction = if is_mouse_over_close {
432 mouse::Interaction::Pointer
433 } else {
434 mouse::Interaction::default()
435 };
436
437 let body_layout = children
438 .next()
439 .expect("widget: Layout should have a body layout");
440 let mut body_children = body_layout.children();
441
442 let foot_layout = children
443 .next()
444 .expect("widget: Layout should have a foot layout");
445 let mut foot_children = foot_layout.children();
446
447 mouse_interaction
448 .max(self.head.as_widget().mouse_interaction(
449 &state.children[0],
450 head,
451 cursor,
452 viewport,
453 renderer,
454 ))
455 .max(
456 self.body.as_widget().mouse_interaction(
457 &state.children[1],
458 body_children
459 .next()
460 .expect("widget: Layout should have a body content layout"),
461 cursor,
462 viewport,
463 renderer,
464 ),
465 )
466 .max(
467 self.foot
468 .as_ref()
469 .map_or_else(mouse::Interaction::default, |foot| {
470 foot.as_widget().mouse_interaction(
471 &state.children[2],
472 foot_children
473 .next()
474 .expect("widget: Layout should have a foot content layout"),
475 cursor,
476 viewport,
477 renderer,
478 )
479 }),
480 )
481 }
482
483 fn operate<'b>(
484 &'b self,
485 state: &'b mut Tree,
486 layout: Layout<'_>,
487 renderer: &Renderer,
488 operation: &mut dyn Operation<()>,
489 ) {
490 let mut children = layout.children();
491 let head_layout = children.next().expect("Missing Head Layout");
492 let body_layout = children.next().expect("Missing Body Layout");
493 let foot_layout = children.next().expect("Missing Footer Layout");
494
495 self.head
496 .as_widget()
497 .operate(&mut state.children[0], head_layout, renderer, operation);
498 self.body
499 .as_widget()
500 .operate(&mut state.children[1], body_layout, renderer, operation);
501
502 if let Some(footer) = &self.foot {
503 footer
504 .as_widget()
505 .operate(&mut state.children[2], foot_layout, renderer, operation);
506 };
507 }
508
509 fn draw(
510 &self,
511 state: &Tree,
512 renderer: &mut Renderer,
513 theme: &Theme,
514 _style: &renderer::Style,
515 layout: Layout<'_>,
516 cursor: Cursor,
517 viewport: &Rectangle,
518 ) {
519 let bounds = layout.bounds();
520 let mut children = layout.children();
521 let style_sheet = theme.style(&self.class, Status::Active);
522
523 if bounds.intersects(viewport) {
524 renderer.fill_quad(
526 renderer::Quad {
527 bounds,
528 border: Border {
529 radius: style_sheet.border_radius.into(),
530 width: style_sheet.border_width,
531 color: style_sheet.border_color,
532 },
533 shadow: Shadow::default(),
534 },
535 style_sheet.background,
536 );
537
538 renderer.fill_quad(
540 renderer::Quad {
542 bounds,
543 border: Border {
544 radius: style_sheet.border_radius.into(),
545 width: style_sheet.border_width,
546 color: style_sheet.border_color,
547 },
548 shadow: Shadow::default(),
549 },
550 Color::TRANSPARENT,
551 );
552 }
553
554 let head_layout = children
556 .next()
557 .expect("Graphics: Layout should have a head layout");
558 draw_head(
559 &state.children[0],
560 renderer,
561 &self.head,
562 head_layout,
563 cursor,
564 viewport,
565 theme,
566 &style_sheet,
567 self.close_size,
568 );
569
570 let body_layout = children
572 .next()
573 .expect("Graphics: Layout should have a body layout");
574 draw_body(
575 &state.children[1],
576 renderer,
577 &self.body,
578 body_layout,
579 cursor,
580 viewport,
581 theme,
582 &style_sheet,
583 );
584
585 let foot_layout = children
587 .next()
588 .expect("Graphics: Layout should have a foot layout");
589 draw_foot(
590 state.children.get(2),
591 renderer,
592 self.foot.as_ref(),
593 foot_layout,
594 cursor,
595 viewport,
596 theme,
597 &style_sheet,
598 );
599 }
600
601 fn overlay<'b>(
602 &'b mut self,
603 tree: &'b mut Tree,
604 layout: Layout<'_>,
605 renderer: &Renderer,
606 translation: Vector,
607 ) -> Option<iced::advanced::overlay::Element<'b, Message, Theme, Renderer>> {
608 let mut children = vec![&mut self.head, &mut self.body];
609 if let Some(foot) = &mut self.foot {
610 children.push(foot);
611 }
612 let children = children
613 .into_iter()
614 .zip(&mut tree.children)
615 .zip(layout.children())
616 .filter_map(|((child, state), layout)| {
617 layout.children().next().and_then(|child_layout| {
618 child
619 .as_widget_mut()
620 .overlay(state, child_layout, renderer, translation)
621 })
622 })
623 .collect::<Vec<_>>();
624
625 (!children.is_empty())
626 .then(|| iced::advanced::overlay::Group::with_children(children).overlay())
627 }
628}
629
630#[allow(clippy::too_many_arguments)]
632fn head_node<Message, Theme, Renderer>(
633 renderer: &Renderer,
634 limits: &Limits,
635 head: &Element<'_, Message, Theme, Renderer>,
636 padding: Padding,
637 width: Length,
638 on_close: bool,
639 close_size: Option<f32>,
640 tree: &mut Tree,
641) -> Node
642where
643 Renderer: renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font>,
644{
645 let header_size = head.as_widget().size();
646
647 let mut limits = limits
648 .loose()
649 .width(width)
650 .height(header_size.height)
651 .shrink(padding);
652
653 let close_size = close_size.unwrap_or_else(|| renderer.default_size().0);
654
655 if on_close {
656 limits = limits.shrink(Size::new(close_size, 0.0));
657 }
658
659 let mut head = head
660 .as_widget()
661 .layout(&mut tree.children[0], renderer, &limits);
662 let mut size = limits.resolve(width, header_size.height, head.size());
663
664 head = head.move_to(Point::new(padding.left, padding.top));
665 let head_size = head.size();
666 head = head.align(Alignment::Start, Alignment::Center, head_size);
667
668 let close = if on_close {
669 let node = Node::new(Size::new(close_size + 1.0, close_size + 1.0));
670 let node_size = node.size();
671
672 size = Size::new(size.width + close_size, size.height);
673
674 Some(
675 node.move_to(Point::new(size.width - padding.right, padding.top))
676 .align(Alignment::End, Alignment::Center, node_size),
677 )
678 } else {
679 None
680 };
681
682 Node::with_children(
683 size.expand(padding),
684 match close {
685 Some(node) => vec![head, node],
686 None => vec![head],
687 },
688 )
689}
690
691fn body_node<Message, Theme, Renderer>(
693 renderer: &Renderer,
694 limits: &Limits,
695 body: &Element<'_, Message, Theme, Renderer>,
696 padding: Padding,
697 width: Length,
698 tree: &mut Tree,
699) -> Node
700where
701 Renderer: renderer::Renderer,
702{
703 let body_size = body.as_widget().size();
704
705 let limits = limits
706 .loose()
707 .width(width)
708 .height(body_size.height)
709 .shrink(padding);
710
711 let mut body = body
712 .as_widget()
713 .layout(&mut tree.children[1], renderer, &limits);
714 let size = limits.resolve(width, body_size.height, body.size());
715
716 body = body.move_to(Point::new(padding.left, padding.top)).align(
717 Alignment::Start,
718 Alignment::Start,
719 size,
720 );
721
722 Node::with_children(size.expand(padding), vec![body])
723}
724
725fn foot_node<Message, Theme, Renderer>(
727 renderer: &Renderer,
728 limits: &Limits,
729 foot: &Element<'_, Message, Theme, Renderer>,
730 padding: Padding,
731 width: Length,
732 tree: &mut Tree,
733) -> Node
734where
735 Renderer: renderer::Renderer,
736{
737 let foot_size = foot.as_widget().size();
738
739 let limits = limits
740 .loose()
741 .width(width)
742 .height(foot_size.height)
743 .shrink(padding);
744
745 let mut foot = foot
746 .as_widget()
747 .layout(&mut tree.children[2], renderer, &limits);
748 let size = limits.resolve(width, foot_size.height, foot.size());
749
750 foot = foot.move_to(Point::new(padding.left, padding.right)).align(
751 Alignment::Start,
752 Alignment::Center,
753 size,
754 );
755
756 Node::with_children(size.expand(padding), vec![foot])
757}
758
759#[allow(clippy::too_many_arguments)]
761fn draw_head<Message, Theme, Renderer>(
762 state: &Tree,
763 renderer: &mut Renderer,
764 head: &Element<'_, Message, Theme, Renderer>,
765 layout: Layout<'_>,
766 cursor: Cursor,
767 viewport: &Rectangle,
768 theme: &Theme,
769 style: &Style,
770 close_size: Option<f32>,
771) where
772 Renderer: renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font>,
773 Theme: Catalog,
774{
775 let mut head_children = layout.children();
776 let bounds = layout.bounds();
777 let border_radius = style.border_radius;
778
779 if bounds.intersects(viewport) {
781 renderer.fill_quad(
782 renderer::Quad {
783 bounds,
784 border: Border {
785 radius: border_radius.into(),
786 width: 0.0,
787 color: Color::TRANSPARENT,
788 },
789 shadow: Shadow::default(),
790 },
791 style.head_background,
792 );
793 }
794
795 let button_bounds = Rectangle {
797 x: bounds.x,
798 y: bounds.y + bounds.height - border_radius,
799 width: bounds.width,
800 height: border_radius,
801 };
802 if button_bounds.intersects(viewport) {
803 renderer.fill_quad(
804 renderer::Quad {
805 bounds: button_bounds,
806 border: Border {
807 radius: (0.0).into(),
808 width: 0.0,
809 color: Color::TRANSPARENT,
810 },
811 shadow: Shadow::default(),
812 },
813 style.head_background,
814 );
815 }
816
817 head.as_widget().draw(
818 state,
819 renderer,
820 theme,
821 &renderer::Style {
822 text_color: style.head_text_color,
823 },
824 head_children
825 .next()
826 .expect("Graphics: Layout should have a head content layout"),
827 cursor,
828 viewport,
829 );
830
831 if let Some(close_layout) = head_children.next() {
832 let close_bounds = close_layout.bounds();
833 let is_mouse_over_close = close_bounds.contains(cursor.position().unwrap_or_default());
834
835 renderer.fill_text(
836 iced::advanced::text::Text {
837 content: icon_to_string(RequiredIcons::X),
838 bounds: Size::new(close_bounds.width, close_bounds.height),
839 size: Pixels(
840 close_size.unwrap_or_else(|| renderer.default_size().0)
841 + if is_mouse_over_close { 1.0 } else { 0.0 },
842 ),
843 font: REQUIRED_FONT,
844 horizontal_alignment: Horizontal::Center,
845 vertical_alignment: Vertical::Center,
846 line_height: LineHeight::Relative(1.3),
847 shaping: iced::advanced::text::Shaping::Advanced,
848 wrapping: Wrapping::default(),
849 },
850 Point::new(close_bounds.center_x(), close_bounds.center_y()),
851 style.close_color,
852 close_bounds,
853 );
854 }
855}
856
857#[allow(clippy::too_many_arguments)]
859fn draw_body<Message, Theme, Renderer>(
860 state: &Tree,
861 renderer: &mut Renderer,
862 body: &Element<'_, Message, Theme, Renderer>,
863 layout: Layout<'_>,
864 cursor: Cursor,
865 viewport: &Rectangle,
866 theme: &Theme,
867 style: &Style,
868) where
869 Renderer: renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font>,
870 Theme: Catalog,
871{
872 let mut body_children = layout.children();
873 let bounds = layout.bounds();
874
875 if bounds.intersects(viewport) {
877 renderer.fill_quad(
878 renderer::Quad {
879 bounds,
880 border: Border {
881 radius: (0.0).into(),
882 width: 0.0,
883 color: Color::TRANSPARENT,
884 },
885 shadow: Shadow::default(),
886 },
887 style.body_background,
888 );
889 }
890
891 body.as_widget().draw(
892 state,
893 renderer,
894 theme,
895 &renderer::Style {
896 text_color: style.body_text_color,
897 },
898 body_children
899 .next()
900 .expect("Graphics: Layout should have a body content layout"),
901 cursor,
902 viewport,
903 );
904}
905
906#[allow(clippy::too_many_arguments)]
908fn draw_foot<Message, Theme, Renderer>(
909 state: Option<&Tree>,
910 renderer: &mut Renderer,
911 foot: Option<&Element<'_, Message, Theme, Renderer>>,
912 layout: Layout<'_>,
913 cursor: Cursor,
914 viewport: &Rectangle,
915 theme: &Theme,
916 style: &Style,
917) where
918 Renderer: renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font>,
919 Theme: Catalog,
920{
921 let mut foot_children = layout.children();
922 let bounds = layout.bounds();
923
924 if bounds.intersects(viewport) {
926 renderer.fill_quad(
927 renderer::Quad {
928 bounds,
929 border: Border {
930 radius: style.border_radius.into(),
931 width: 0.0,
932 color: Color::TRANSPARENT,
933 },
934 shadow: Shadow::default(),
935 },
936 style.foot_background,
937 );
938 }
939
940 if let Some((foot, state)) = foot.as_ref().zip(state) {
941 foot.as_widget().draw(
942 state,
943 renderer,
944 theme,
945 &renderer::Style {
946 text_color: style.foot_text_color,
947 },
948 foot_children
949 .next()
950 .expect("Graphics: Layout should have a foot content layout"),
951 cursor,
952 viewport,
953 );
954 }
955}
956
957impl<'a, Message, Theme, Renderer> From<Card<'a, Message, Theme, Renderer>>
958 for Element<'a, Message, Theme, Renderer>
959where
960 Renderer: 'a + renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font>,
961 Theme: 'a + Catalog,
962 Message: Clone + 'a,
963{
964 fn from(card: Card<'a, Message, Theme, Renderer>) -> Self {
965 Element::new(card)
966 }
967}