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