1use iced_runtime::core::widget::Id;
10use iced_runtime::{Action, Task, keyboard, task};
11
12use iced_core::event::{self, Event};
13use iced_core::renderer::{self, Quad, Renderer};
14use iced_core::touch;
15use iced_core::widget::Operation;
16use iced_core::widget::tree::{self, Tree};
17use iced_core::{
18 Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget,
19};
20use iced_core::{Border, mouse};
21use iced_core::{Shadow, overlay};
22use iced_core::{layout, svg};
23use iced_renderer::core::widget::operation;
24
25use crate::theme::THEME;
26
27pub use super::style::{Catalog, Style};
28
29enum Variant<Message> {
31 Normal,
32 Image {
33 close_icon: svg::Handle,
34 on_remove: Option<Message>,
35 },
36}
37
38#[allow(missing_debug_implementations)]
40#[must_use]
41pub struct Button<'a, Message> {
42 id: Id,
43 #[cfg(feature = "a11y")]
44 name: Option<std::borrow::Cow<'a, str>>,
45 #[cfg(feature = "a11y")]
46 description: Option<iced_accessibility::Description<'a>>,
47 #[cfg(feature = "a11y")]
48 label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
49 content: crate::Element<'a, Message>,
50 on_press: Option<Box<dyn Fn(Vector, Rectangle) -> Message + 'a>>,
51 on_press_down: Option<Box<dyn Fn(Vector, Rectangle) -> Message + 'a>>,
52 width: Length,
53 height: Length,
54 padding: Padding,
55 selected: bool,
56 style: crate::theme::Button,
57 variant: Variant<Message>,
58 force_enabled: bool,
59}
60
61impl<'a, Message: Clone + 'a> Button<'a, Message> {
62 pub(super) fn new(content: impl Into<crate::Element<'a, Message>>) -> Self {
64 Self {
65 id: Id::unique(),
66 #[cfg(feature = "a11y")]
67 name: None,
68 #[cfg(feature = "a11y")]
69 description: None,
70 #[cfg(feature = "a11y")]
71 label: None,
72 content: content.into(),
73 on_press: None,
74 on_press_down: None,
75 width: Length::Shrink,
76 height: Length::Shrink,
77 padding: Padding::new(5.0),
78 selected: false,
79 style: crate::theme::Button::default(),
80 variant: Variant::Normal,
81 force_enabled: false,
82 }
83 }
84
85 pub fn new_image(
86 content: impl Into<crate::Element<'a, Message>>,
87 on_remove: Option<Message>,
88 ) -> Self {
89 Self {
90 id: Id::unique(),
91 #[cfg(feature = "a11y")]
92 name: None,
93 #[cfg(feature = "a11y")]
94 description: None,
95 force_enabled: false,
96 #[cfg(feature = "a11y")]
97 label: None,
98 content: content.into(),
99 on_press: None,
100 on_press_down: None,
101 width: Length::Shrink,
102 height: Length::Shrink,
103 padding: Padding::new(5.0),
104 selected: false,
105 style: crate::theme::Button::default(),
106 variant: Variant::Image {
107 on_remove,
108 close_icon: crate::widget::icon::from_name("window-close-symbolic")
109 .size(8)
110 .icon()
111 .into_svg_handle()
112 .unwrap_or_else(|| {
113 let bytes: &'static [u8] = &[];
114 iced_core::svg::Handle::from_memory(bytes)
115 }),
116 },
117 }
118 }
119
120 #[inline]
122 pub fn id(mut self, id: Id) -> Self {
123 self.id = id;
124 self
125 }
126
127 #[inline]
129 pub fn width(mut self, width: impl Into<Length>) -> Self {
130 self.width = width.into();
131 self
132 }
133
134 #[inline]
136 pub fn height(mut self, height: impl Into<Length>) -> Self {
137 self.height = height.into();
138 self
139 }
140
141 #[inline]
143 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
144 self.padding = padding.into();
145 self
146 }
147
148 #[inline]
152 pub fn on_press(mut self, on_press: Message) -> Self {
153 self.on_press = Some(Box::new(move |_, _| on_press.clone()));
154 self
155 }
156
157 #[inline]
161 pub fn on_press_with_rectangle(
162 mut self,
163 on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
164 ) -> Self {
165 self.on_press = Some(Box::new(on_press));
166 self
167 }
168
169 #[inline]
173 pub fn on_press_down(mut self, on_press: Message) -> Self {
174 self.on_press_down = Some(Box::new(move |_, _| on_press.clone()));
175 self
176 }
177
178 #[inline]
182 pub fn on_press_down_with_rectange(
183 mut self,
184 on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
185 ) -> Self {
186 self.on_press_down = Some(Box::new(on_press));
187 self
188 }
189
190 #[inline]
195 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
196 if let Some(m) = on_press {
197 self.on_press(m)
198 } else {
199 self.on_press = None;
200 self
201 }
202 }
203
204 #[inline]
208 pub fn on_press_maybe_with_rectangle(
209 mut self,
210 on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
211 ) -> Self {
212 self.on_press = Some(Box::new(on_press));
213 self
214 }
215
216 #[inline]
221 pub fn on_press_down_maybe(mut self, on_press: Option<Message>) -> Self {
222 if let Some(m) = on_press {
223 self.on_press(m)
224 } else {
225 self.on_press_down = None;
226 self
227 }
228 }
229
230 #[inline]
234 pub fn on_press_down_maybe_with_rectangle(
235 mut self,
236 on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
237 ) -> Self {
238 self.on_press_down = Some(Box::new(on_press));
239 self
240 }
241
242 #[inline]
244 pub fn force_enabled(mut self, enabled: bool) -> Self {
245 self.force_enabled = enabled;
246 self
247 }
248
249 #[inline]
253 pub fn selected(mut self, selected: bool) -> Self {
254 self.selected = selected;
255
256 self
257 }
258
259 #[inline]
261 pub fn class(mut self, style: crate::theme::Button) -> Self {
262 self.style = style;
263 self
264 }
265
266 #[cfg(feature = "a11y")]
267 pub fn name(mut self, name: impl Into<std::borrow::Cow<'a, str>>) -> Self {
269 self.name = Some(name.into());
270 self
271 }
272
273 #[cfg(feature = "a11y")]
274 pub fn description_widget<T: iced_accessibility::Describes>(mut self, description: &T) -> Self {
276 self.description = Some(iced_accessibility::Description::Id(
277 description.description(),
278 ));
279 self
280 }
281
282 #[cfg(feature = "a11y")]
283 pub fn description(mut self, description: impl Into<std::borrow::Cow<'a, str>>) -> Self {
285 self.description = Some(iced_accessibility::Description::Text(description.into()));
286 self
287 }
288
289 #[cfg(feature = "a11y")]
290 pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
292 self.label = Some(label.label().into_iter().map(|l| l.into()).collect());
293 self
294 }
295}
296
297impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
298 for Button<'a, Message>
299{
300 fn tag(&self) -> tree::Tag {
301 tree::Tag::of::<State>()
302 }
303
304 fn state(&self) -> tree::State {
305 tree::State::new(State::new())
306 }
307
308 fn children(&self) -> Vec<Tree> {
309 vec![Tree::new(&self.content)]
310 }
311
312 fn diff(&mut self, tree: &mut Tree) {
313 tree.diff_children(std::slice::from_mut(&mut self.content));
314 }
315
316 fn size(&self) -> iced_core::Size<Length> {
317 iced_core::Size::new(self.width, self.height)
318 }
319
320 fn layout(
321 &self,
322 tree: &mut Tree,
323 renderer: &crate::Renderer,
324 limits: &layout::Limits,
325 ) -> layout::Node {
326 layout(
327 renderer,
328 limits,
329 self.width,
330 self.height,
331 self.padding,
332 |renderer, limits| {
333 self.content
334 .as_widget()
335 .layout(&mut tree.children[0], renderer, limits)
336 },
337 )
338 }
339
340 fn operate(
341 &self,
342 tree: &mut Tree,
343 layout: Layout<'_>,
344 renderer: &crate::Renderer,
345 operation: &mut dyn Operation<()>,
346 ) {
347 operation.container(None, layout.bounds(), &mut |operation| {
348 self.content.as_widget().operate(
349 &mut tree.children[0],
350 layout
351 .children()
352 .next()
353 .unwrap()
354 .with_virtual_offset(layout.virtual_offset()),
355 renderer,
356 operation,
357 );
358 });
359 let state = tree.state.downcast_mut::<State>();
360 operation.focusable(state, Some(&self.id));
361 }
362
363 fn on_event(
364 &mut self,
365 tree: &mut Tree,
366 event: Event,
367 layout: Layout<'_>,
368 cursor: mouse::Cursor,
369 renderer: &crate::Renderer,
370 clipboard: &mut dyn Clipboard,
371 shell: &mut Shell<'_, Message>,
372 viewport: &Rectangle,
373 ) -> event::Status {
374 if let Variant::Image {
375 on_remove: Some(on_remove),
376 ..
377 } = &self.variant
378 {
379 match event {
381 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
382 | Event::Touch(touch::Event::FingerPressed { .. }) => {
383 if let Some(position) = cursor.position() {
384 if removal_bounds(layout.bounds(), 4.0).contains(position) {
385 shell.publish(on_remove.clone());
386 return event::Status::Captured;
387 }
388 }
389 }
390
391 _ => (),
392 }
393 }
394
395 if self.content.as_widget_mut().on_event(
396 &mut tree.children[0],
397 event.clone(),
398 layout
399 .children()
400 .next()
401 .unwrap()
402 .with_virtual_offset(layout.virtual_offset()),
403 cursor,
404 renderer,
405 clipboard,
406 shell,
407 viewport,
408 ) == event::Status::Captured
409 {
410 return event::Status::Captured;
411 }
412
413 update(
414 self.id.clone(),
415 event,
416 layout,
417 cursor,
418 shell,
419 self.on_press.as_deref(),
420 self.on_press_down.as_deref(),
421 || tree.state.downcast_mut::<State>(),
422 )
423 }
424
425 #[allow(clippy::too_many_lines)]
426 fn draw(
427 &self,
428 tree: &Tree,
429 renderer: &mut crate::Renderer,
430 theme: &crate::Theme,
431 renderer_style: &renderer::Style,
432 layout: Layout<'_>,
433 cursor: mouse::Cursor,
434 viewport: &Rectangle,
435 ) {
436 let bounds = layout.bounds();
437 if !viewport.intersects(&bounds) {
438 return;
439 }
440 let content_layout = layout.children().next().unwrap();
441
442 let mut headerbar_alpha = None;
443
444 let is_enabled =
445 self.on_press.is_some() || self.on_press_down.is_some() || self.force_enabled;
446 let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p));
447
448 let state = tree.state.downcast_ref::<State>();
449
450 let styling = if !is_enabled {
451 theme.disabled(&self.style)
452 } else if is_mouse_over {
453 if state.is_pressed {
454 if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
455 headerbar_alpha = Some(0.8);
456 }
457
458 theme.pressed(state.is_focused, self.selected, &self.style)
459 } else {
460 if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
461 headerbar_alpha = Some(0.8);
462 }
463
464 theme.hovered(state.is_focused, self.selected, &self.style)
465 }
466 } else {
467 if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
468 headerbar_alpha = Some(0.75);
469 }
470
471 theme.active(state.is_focused, self.selected, &self.style)
472 };
473
474 let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
475
476 let mut text_color = if matches!(self.style, crate::theme::Button::MenuRoot) {
478 icon_color
479 } else {
480 styling.text_color.unwrap_or(renderer_style.text_color)
481 };
482
483 if let Some(alpha) = headerbar_alpha {
484 icon_color.a = alpha;
485 text_color.a = alpha;
486 }
487
488 draw::<_, crate::Theme>(
489 renderer,
490 bounds,
491 *viewport,
492 &styling,
493 |renderer, _styling| {
494 self.content.as_widget().draw(
495 &tree.children[0],
496 renderer,
497 theme,
498 &renderer::Style {
499 icon_color,
500 text_color,
501 scale_factor: renderer_style.scale_factor,
502 },
503 content_layout.with_virtual_offset(layout.virtual_offset()),
504 cursor,
505 &viewport.intersection(&bounds).unwrap_or_default(),
506 );
507 },
508 matches!(self.variant, Variant::Image { .. }),
509 );
510
511 if let Variant::Image {
512 close_icon,
513 on_remove,
514 } = &self.variant
515 {
516 renderer.with_layer(*viewport, |renderer| {
517 let selection_background = theme.selection_background();
518
519 let c_rad = THEME.lock().unwrap().cosmic().corner_radii;
520
521 if !self.selected && !is_mouse_over {
523 let mut bounds = bounds;
524 bounds.x -= 2.0;
525 bounds.y -= 2.0;
526 bounds.width += 4.0;
527 bounds.height += 4.0;
528 renderer.fill_quad(
529 renderer::Quad {
530 bounds,
531 border: Border {
532 width: 2.0,
533 color: crate::theme::active().current_container().base.into(),
534 radius: 9.0.into(),
535 },
536 shadow: Shadow::default(),
537 },
538 Color::TRANSPARENT,
539 );
540 }
541
542 if self.selected {
543 renderer.fill_quad(
544 Quad {
545 bounds: Rectangle {
546 width: 24.0,
547 height: 20.0,
548 x: bounds.x + styling.border_width,
549 y: bounds.y + (bounds.height - 20.0 - styling.border_width),
550 },
551 border: Border {
552 radius: [
553 c_rad.radius_0[0],
554 c_rad.radius_s[1],
555 c_rad.radius_0[2],
556 c_rad.radius_s[3],
557 ]
558 .into(),
559 ..Default::default()
560 },
561 shadow: Shadow::default(),
562 },
563 selection_background,
564 );
565
566 let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone())
567 .color(icon_color);
568 let bounds = Rectangle {
569 width: 16.0,
570 height: 16.0,
571 x: bounds.x + 5.0 + styling.border_width,
572 y: bounds.y + (bounds.height - 18.0 - styling.border_width),
573 };
574 if bounds.intersects(viewport) {
575 iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds);
576 }
577 }
578
579 if on_remove.is_some() {
580 if let Some(position) = cursor.position() {
581 if bounds.contains(position) {
582 let bounds = removal_bounds(layout.bounds(), 4.0);
583 renderer.fill_quad(
584 renderer::Quad {
585 bounds,
586 shadow: Shadow::default(),
587 border: Border {
588 radius: c_rad.radius_m.into(),
589 ..Default::default()
590 },
591 },
592 selection_background,
593 );
594 let svg_handle = svg::Svg::new(close_icon.clone()).color(icon_color);
595 iced_core::svg::Renderer::draw_svg(
596 renderer,
597 svg_handle,
598 Rectangle {
599 width: 16.0,
600 height: 16.0,
601 x: bounds.x + 4.0,
602 y: bounds.y + 4.0,
603 },
604 );
605 }
606 }
607 }
608 });
609 }
610 }
611
612 fn mouse_interaction(
613 &self,
614 _tree: &Tree,
615 layout: Layout<'_>,
616 cursor: mouse::Cursor,
617 _viewport: &Rectangle,
618 _renderer: &crate::Renderer,
619 ) -> mouse::Interaction {
620 mouse_interaction(
621 layout.with_virtual_offset(layout.virtual_offset()),
622 cursor,
623 self.on_press.is_some(),
624 )
625 }
626
627 fn overlay<'b>(
628 &'b mut self,
629 tree: &'b mut Tree,
630 layout: Layout<'_>,
631 renderer: &crate::Renderer,
632 mut translation: Vector,
633 ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
634 let position = layout.bounds().position();
635 translation.x += position.x;
636 translation.y += position.y;
637 self.content.as_widget_mut().overlay(
638 &mut tree.children[0],
639 layout
640 .children()
641 .next()
642 .unwrap()
643 .with_virtual_offset(layout.virtual_offset()),
644 renderer,
645 translation,
646 )
647 }
648
649 #[cfg(feature = "a11y")]
650 fn a11y_nodes(
652 &self,
653 layout: Layout<'_>,
654 state: &Tree,
655 p: mouse::Cursor,
656 ) -> iced_accessibility::A11yTree {
657 use iced_accessibility::{
658 A11yNode, A11yTree,
659 accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role},
660 };
661 if matches!(state.state, iced_core::widget::tree::State::None) {
663 tracing::info!("Button state is missing.");
664 return A11yTree::default();
665 }
666
667 let child_layout = layout.children().next().unwrap();
668 let child_tree = state.children.first();
669
670 let Rectangle {
671 x,
672 y,
673 width,
674 height,
675 } = layout.bounds();
676 let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64);
677 let is_hovered = state.state.downcast_ref::<State>().is_hovered;
678
679 let mut node = NodeBuilder::new(Role::Button);
680 node.add_action(Action::Focus);
681 node.add_action(Action::Default);
682 node.set_bounds(bounds);
683 if let Some(name) = self.name.as_ref() {
684 node.set_name(name.clone());
685 }
686 match self.description.as_ref() {
687 Some(iced_accessibility::Description::Id(id)) => {
688 node.set_described_by(id.iter().cloned().map(NodeId::from).collect::<Vec<_>>());
689 }
690 Some(iced_accessibility::Description::Text(text)) => {
691 node.set_description(text.clone());
692 }
693 None => {}
694 }
695
696 if let Some(label) = self.label.as_ref() {
697 node.set_labelled_by(label.clone());
698 }
699
700 if self.on_press.is_none() {
701 node.set_disabled();
702 }
703 if is_hovered {
704 node.set_hovered();
705 }
706 node.set_default_action_verb(DefaultActionVerb::Click);
707
708 if let Some(child_tree) = child_tree.map(|child_tree| {
709 self.content.as_widget().a11y_nodes(
710 child_layout.with_virtual_offset(layout.virtual_offset()),
711 child_tree,
712 p,
713 )
714 }) {
715 A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree)
716 } else {
717 A11yTree::leaf(node, self.id.clone())
718 }
719 }
720
721 fn id(&self) -> Option<Id> {
722 Some(self.id.clone())
723 }
724
725 fn set_id(&mut self, id: Id) {
726 self.id = id;
727 }
728}
729
730impl<'a, Message: Clone + 'a> From<Button<'a, Message>> for crate::Element<'a, Message> {
731 fn from(button: Button<'a, Message>) -> Self {
732 Self::new(button)
733 }
734}
735
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
738#[allow(clippy::struct_field_names)]
739pub struct State {
740 is_hovered: bool,
741 is_pressed: bool,
742 is_focused: bool,
743}
744
745impl State {
746 #[inline]
748 pub fn new() -> Self {
749 Self::default()
750 }
751
752 #[inline]
754 pub fn is_focused(self) -> bool {
755 self.is_focused
756 }
757
758 #[inline]
760 pub fn is_hovered(self) -> bool {
761 self.is_hovered
762 }
763
764 #[inline]
766 pub fn focus(&mut self) {
767 self.is_focused = true;
768 }
769
770 #[inline]
772 pub fn unfocus(&mut self) {
773 self.is_focused = false;
774 }
775}
776
777#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
780pub fn update<'a, Message: Clone>(
781 _id: Id,
782 event: Event,
783 layout: Layout<'_>,
784 cursor: mouse::Cursor,
785 shell: &mut Shell<'_, Message>,
786 on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>,
787 on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>,
788 state: impl FnOnce() -> &'a mut State,
789) -> event::Status {
790 match event {
791 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
792 | Event::Touch(touch::Event::FingerPressed { .. }) => {
793 let state = state();
795 state.unfocus();
796
797 if on_press.is_some() || on_press_down.is_some() {
798 let bounds = layout.bounds();
799
800 if cursor.is_over(bounds) {
801 state.is_pressed = true;
802
803 if let Some(on_press_down) = on_press_down {
804 let msg = (on_press_down)(layout.virtual_offset(), layout.bounds());
805 shell.publish(msg);
806 }
807
808 return event::Status::Captured;
809 }
810 }
811 }
812 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
813 | Event::Touch(touch::Event::FingerLifted { .. }) => {
814 if let Some(on_press) = on_press.clone() {
815 let state = state();
816
817 if state.is_pressed {
818 state.is_pressed = false;
819
820 let bounds = layout.bounds();
821
822 if cursor.is_over(bounds) {
823 let msg = (on_press)(layout.virtual_offset(), layout.bounds());
824 shell.publish(msg);
825 }
826
827 return event::Status::Captured;
828 }
829 } else if on_press_down.is_some() {
830 let state = state();
831 state.is_pressed = false;
832 }
833 }
834 #[cfg(feature = "a11y")]
835 Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => {
836 let state = state();
837 if let Some(Some(on_press)) = (event_id == event_id
838 && matches!(action, iced_accessibility::accesskit::Action::Default))
839 .then(|| on_press.clone())
840 {
841 state.is_pressed = false;
842 let msg = (on_press)(layout.virtual_offset(), layout.bounds());
843
844 shell.publish(msg);
845 }
846 return event::Status::Captured;
847 }
848 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
849 if let Some(on_press) = on_press.clone() {
850 let state = state();
851 if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) {
852 state.is_pressed = true;
853 let msg = (on_press)(layout.virtual_offset(), layout.bounds());
854
855 shell.publish(msg);
856 return event::Status::Captured;
857 }
858 }
859 }
860 Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
861 let state = state();
862 state.is_hovered = false;
863 state.is_pressed = false;
864 }
865 _ => {}
866 }
867
868 event::Status::Ignored
869}
870
871#[allow(clippy::too_many_arguments)]
872pub fn draw<Renderer: iced_core::Renderer, Theme>(
873 renderer: &mut Renderer,
874 bounds: Rectangle,
875 viewport_bounds: Rectangle,
876 styling: &super::style::Style,
877 draw_contents: impl FnOnce(&mut Renderer, &Style),
878 is_image: bool,
879) where
880 Theme: super::style::Catalog,
881{
882 let doubled_border_width = styling.border_width * 2.0;
883 let doubled_outline_width = styling.outline_width * 2.0;
884
885 if styling.outline_width > 0.0 {
886 renderer.fill_quad(
887 renderer::Quad {
888 bounds: Rectangle {
889 x: bounds.x - styling.border_width - styling.outline_width,
890 y: bounds.y - styling.border_width - styling.outline_width,
891 width: bounds.width + doubled_border_width + doubled_outline_width,
892 height: bounds.height + doubled_border_width + doubled_outline_width,
893 },
894 border: Border {
895 width: styling.outline_width,
896 color: styling.outline_color,
897 radius: styling.border_radius,
898 },
899 shadow: Shadow::default(),
900 },
901 Color::TRANSPARENT,
902 );
903 }
904
905 if styling.background.is_some() || styling.border_width > 0.0 {
906 if styling.shadow_offset != Vector::default() {
907 renderer.fill_quad(
909 renderer::Quad {
910 bounds: Rectangle {
911 x: bounds.x + styling.shadow_offset.x,
912 y: bounds.y + styling.shadow_offset.y,
913 width: bounds.width,
914 height: bounds.height,
915 },
916 border: Border {
917 radius: styling.border_radius,
918 ..Default::default()
919 },
920 shadow: Shadow::default(),
921 },
922 Background::Color([0.0, 0.0, 0.0, 0.5].into()),
923 );
924 }
925
926 if let Some(background) = styling.background {
928 renderer.fill_quad(
929 renderer::Quad {
930 bounds,
931 border: Border {
932 radius: styling.border_radius,
933 ..Default::default()
934 },
935 shadow: Shadow::default(),
936 },
937 background,
938 );
939 }
940
941 if let Some(overlay) = styling.overlay {
943 renderer.fill_quad(
944 renderer::Quad {
945 bounds,
946 border: Border {
947 radius: styling.border_radius,
948 ..Default::default()
949 },
950 shadow: Shadow::default(),
951 },
952 overlay,
953 );
954 }
955
956 draw_contents(renderer, styling);
958
959 let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
960 clipped_bounds.height += styling.border_width;
961
962 renderer.with_layer(clipped_bounds, |renderer| {
963 if is_image {
965 renderer.fill_quad(
966 renderer::Quad {
967 bounds,
968 border: Border {
969 width: styling.border_width,
970 color: crate::theme::active().current_container().base.into(),
971 radius: 0.0.into(),
972 },
973 shadow: Shadow::default(),
974 },
975 Color::TRANSPARENT,
976 );
977 }
978
979 renderer.fill_quad(
981 renderer::Quad {
982 bounds,
983 border: Border {
984 width: styling.border_width,
985 color: styling.border_color,
986 radius: styling.border_radius,
987 },
988 shadow: Shadow::default(),
989 },
990 Color::TRANSPARENT,
991 );
992 });
993 } else {
994 draw_contents(renderer, styling);
995 }
996}
997
998pub fn layout<Renderer>(
1000 renderer: &Renderer,
1001 limits: &layout::Limits,
1002 width: Length,
1003 height: Length,
1004 padding: Padding,
1005 layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
1006) -> layout::Node {
1007 let limits = limits.width(width).height(height);
1008
1009 let mut content = layout_content(renderer, &limits.shrink(padding));
1010 let padding = padding.fit(content.size(), limits.max());
1011 let size = limits
1012 .shrink(padding)
1013 .resolve(width, height, content.size())
1014 .expand(padding);
1015
1016 content = content.move_to(Point::new(padding.left, padding.top));
1017
1018 layout::Node::with_children(size, vec![content])
1019}
1020
1021#[must_use]
1023pub fn mouse_interaction(
1024 layout: Layout<'_>,
1025 cursor: mouse::Cursor,
1026 is_enabled: bool,
1027) -> mouse::Interaction {
1028 let is_mouse_over = cursor.is_over(layout.bounds());
1029
1030 if is_mouse_over && is_enabled {
1031 mouse::Interaction::Pointer
1032 } else {
1033 mouse::Interaction::default()
1034 }
1035}
1036
1037pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
1039 task::effect(Action::Widget(Box::new(operation::focusable::focus(id))))
1040}
1041
1042impl operation::Focusable for State {
1043 #[inline]
1044 fn is_focused(&self) -> bool {
1045 Self::is_focused(*self)
1046 }
1047
1048 #[inline]
1049 fn focus(&mut self) {
1050 Self::focus(self);
1051 }
1052
1053 #[inline]
1054 fn unfocus(&mut self) {
1055 Self::unfocus(self);
1056 }
1057}
1058
1059fn removal_bounds(bounds: Rectangle, offset: f32) -> Rectangle {
1060 Rectangle {
1061 x: bounds.x + bounds.width - 12.0 - offset,
1062 y: bounds.y - 12.0 + offset,
1063 width: 24.0,
1064 height: 24.0,
1065 }
1066}