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 &mut 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_mut()
335 .layout(&mut tree.children[0], renderer, limits)
336 },
337 )
338 }
339
340 fn operate(
341 &mut self,
342 tree: &mut Tree,
343 layout: Layout<'_>,
344 renderer: &crate::Renderer,
345 operation: &mut dyn Operation<()>,
346 ) {
347 operation.container(None, layout.bounds());
348 operation.traverse(&mut |operation| {
349 self.content.as_widget_mut().operate(
350 &mut tree.children[0],
351 layout
352 .children()
353 .next()
354 .unwrap()
355 .with_virtual_offset(layout.virtual_offset()),
356 renderer,
357 operation,
358 );
359 });
360 let state = tree.state.downcast_mut::<State>();
361 operation.focusable(Some(&self.id), layout.bounds(), state);
362 }
363
364 fn update(
365 &mut self,
366 tree: &mut Tree,
367 event: &Event,
368 layout: Layout<'_>,
369 cursor: mouse::Cursor,
370 renderer: &crate::Renderer,
371 clipboard: &mut dyn Clipboard,
372 shell: &mut Shell<'_, Message>,
373 viewport: &Rectangle,
374 ) {
375 if let Variant::Image {
376 on_remove: Some(on_remove),
377 ..
378 } = &self.variant
379 {
380 match event {
382 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
383 | Event::Touch(touch::Event::FingerPressed { .. }) => {
384 if let Some(position) = cursor.position() {
385 if removal_bounds(layout.bounds(), 4.0).contains(position) {
386 shell.publish(on_remove.clone());
387 shell.capture_event();
388 return;
389 }
390 }
391 }
392
393 _ => (),
394 }
395 }
396 self.content.as_widget_mut().update(
397 &mut tree.children[0],
398 event,
399 layout
400 .children()
401 .next()
402 .unwrap()
403 .with_virtual_offset(layout.virtual_offset()),
404 cursor,
405 renderer,
406 clipboard,
407 shell,
408 viewport,
409 );
410 if shell.is_event_captured() {
411 return;
412 }
413
414 update(
415 self.id.clone(),
416 event,
417 layout,
418 cursor,
419 shell,
420 self.on_press.as_deref(),
421 self.on_press_down.as_deref(),
422 || tree.state.downcast_mut::<State>(),
423 )
424 }
425
426 #[allow(clippy::too_many_lines)]
427 fn draw(
428 &self,
429 tree: &Tree,
430 renderer: &mut crate::Renderer,
431 theme: &crate::Theme,
432 renderer_style: &renderer::Style,
433 layout: Layout<'_>,
434 cursor: mouse::Cursor,
435 viewport: &Rectangle,
436 ) {
437 let bounds = layout.bounds();
438 if !viewport.intersects(&bounds) {
439 return;
440 }
441
442 let Some(content_layout) = layout.children().next() else {
444 return;
445 };
446
447 let mut headerbar_alpha = None;
448
449 let is_enabled =
450 self.on_press.is_some() || self.on_press_down.is_some() || self.force_enabled;
451 let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p));
452
453 let state = tree.state.downcast_ref::<State>();
454
455 let styling = if !is_enabled {
456 theme.disabled(&self.style)
457 } else if is_mouse_over {
458 if state.is_pressed {
459 if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
460 headerbar_alpha = Some(0.8);
461 }
462
463 theme.pressed(state.is_focused, self.selected, &self.style)
464 } else {
465 if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
466 headerbar_alpha = Some(0.8);
467 }
468 theme.hovered(state.is_focused, self.selected, &self.style)
469 }
470 } else {
471 if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
472 headerbar_alpha = Some(0.75);
473 }
474
475 theme.active(state.is_focused, self.selected, &self.style)
476 };
477
478 let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
479
480 let mut text_color = if matches!(self.style, crate::theme::Button::MenuRoot) {
482 icon_color
483 } else {
484 styling.text_color.unwrap_or(renderer_style.text_color)
485 };
486
487 if let Some(alpha) = headerbar_alpha {
488 icon_color.a = alpha;
489 text_color.a = alpha;
490 }
491
492 draw::<_, crate::Theme>(
493 renderer,
494 bounds,
495 *viewport,
496 &styling,
497 |renderer, _styling| {
498 self.content.as_widget().draw(
499 &tree.children[0],
500 renderer,
501 theme,
502 &renderer::Style {
503 icon_color,
504 text_color,
505 scale_factor: renderer_style.scale_factor,
506 },
507 content_layout.with_virtual_offset(layout.virtual_offset()),
508 cursor,
509 &viewport.intersection(&bounds).unwrap_or_default(),
510 );
511 },
512 matches!(self.variant, Variant::Image { .. }),
513 );
514
515 if let Variant::Image {
516 close_icon,
517 on_remove,
518 } = &self.variant
519 {
520 renderer.with_layer(*viewport, |renderer| {
521 let selection_background = theme.selection_background();
522
523 let c_rad = THEME.lock().unwrap().cosmic().corner_radii;
524
525 if self.selected {
526 renderer.fill_quad(
527 Quad {
528 bounds: Rectangle {
529 width: 24.0,
530 height: 20.0,
531 x: bounds.x + styling.border_width,
532 y: bounds.y + (bounds.height - 20.0 - styling.border_width),
533 },
534 border: Border {
535 radius: [
536 c_rad.radius_0[0],
537 c_rad.radius_s[1],
538 c_rad.radius_0[2],
539 c_rad.radius_s[3],
540 ]
541 .into(),
542 ..Default::default()
543 },
544 shadow: Shadow::default(),
545 snap: true,
546 },
547 selection_background,
548 );
549
550 let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone())
551 .color(icon_color);
552 let bounds = Rectangle {
553 width: 16.0,
554 height: 16.0,
555 x: bounds.x + 5.0 + styling.border_width,
556 y: bounds.y + (bounds.height - 18.0 - styling.border_width),
557 };
558 if bounds.intersects(viewport) {
559 iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds);
560 }
561 }
562
563 if on_remove.is_some() {
564 if let Some(position) = cursor.position() {
565 if bounds.contains(position) {
566 let bounds = removal_bounds(layout.bounds(), 4.0);
567 renderer.fill_quad(
568 renderer::Quad {
569 bounds,
570 shadow: Shadow::default(),
571 border: Border {
572 radius: c_rad.radius_m.into(),
573 ..Default::default()
574 },
575 snap: true,
576 },
577 selection_background,
578 );
579 let svg_handle = svg::Svg::new(close_icon.clone()).color(icon_color);
580 iced_core::svg::Renderer::draw_svg(
581 renderer,
582 svg_handle,
583 Rectangle {
584 width: 16.0,
585 height: 16.0,
586 x: bounds.x + 4.0,
587 y: bounds.y + 4.0,
588 },
589 Rectangle {
590 width: 16.0,
591 height: 16.0,
592 x: bounds.x + 4.0,
593 y: bounds.y + 4.0,
594 },
595 );
596 }
597 }
598 }
599 });
600 }
601 }
602
603 fn mouse_interaction(
604 &self,
605 _tree: &Tree,
606 layout: Layout<'_>,
607 cursor: mouse::Cursor,
608 _viewport: &Rectangle,
609 _renderer: &crate::Renderer,
610 ) -> mouse::Interaction {
611 mouse_interaction(
612 layout.with_virtual_offset(layout.virtual_offset()),
613 cursor,
614 self.on_press.is_some(),
615 )
616 }
617
618 fn overlay<'b>(
619 &'b mut self,
620 tree: &'b mut Tree,
621 layout: Layout<'b>,
622 renderer: &crate::Renderer,
623 viewport: &Rectangle,
624 mut translation: Vector,
625 ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
626 let position = layout.bounds().position();
627 translation.x += position.x;
628 translation.y += position.y;
629 self.content.as_widget_mut().overlay(
630 &mut tree.children[0],
631 layout
632 .children()
633 .next()
634 .unwrap()
635 .with_virtual_offset(layout.virtual_offset()),
636 renderer,
637 viewport,
638 translation,
639 )
640 }
641
642 #[cfg(feature = "a11y")]
643 fn a11y_nodes(
645 &self,
646 layout: Layout<'_>,
647 state: &Tree,
648 p: mouse::Cursor,
649 ) -> iced_accessibility::A11yTree {
650 use iced_accessibility::{
651 A11yNode, A11yTree,
652 accesskit::{Action, Node, NodeId, Rect, Role},
653 };
654 if matches!(state.state, iced_core::widget::tree::State::None) {
656 tracing::info!("Button state is missing.");
657 return A11yTree::default();
658 }
659
660 let child_layout = layout.children().next().unwrap();
661 let child_tree = state.children.first();
662
663 let Rectangle {
664 x,
665 y,
666 width,
667 height,
668 } = layout.bounds();
669 let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64);
670 let is_hovered = state.state.downcast_ref::<State>().is_hovered;
671
672 let mut node = Node::new(Role::Button);
673 node.add_action(Action::Focus);
674 node.add_action(Action::Click);
675 node.set_bounds(bounds);
676 if let Some(name) = self.name.as_ref() {
677 node.set_label(name.clone());
678 }
679 match self.description.as_ref() {
680 Some(iced_accessibility::Description::Id(id)) => {
681 node.set_described_by(id.iter().cloned().map(NodeId::from).collect::<Vec<_>>());
682 }
683 Some(iced_accessibility::Description::Text(text)) => {
684 node.set_description(text.clone());
685 }
686 None => {}
687 }
688
689 if let Some(label) = self.label.as_ref() {
690 node.set_labelled_by(label.clone());
691 }
692
693 if self.on_press.is_none() {
694 node.set_disabled();
695 }
696 if let Some(child_tree) = child_tree.map(|child_tree| {
702 self.content.as_widget().a11y_nodes(
703 child_layout.with_virtual_offset(layout.virtual_offset()),
704 child_tree,
705 p,
706 )
707 }) {
708 A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree)
709 } else {
710 A11yTree::leaf(node, self.id.clone())
711 }
712 }
713
714 fn id(&self) -> Option<Id> {
715 Some(self.id.clone())
716 }
717
718 fn set_id(&mut self, id: Id) {
719 self.id = id;
720 }
721}
722
723impl<'a, Message: Clone + 'a> From<Button<'a, Message>> for crate::Element<'a, Message> {
724 fn from(button: Button<'a, Message>) -> Self {
725 Self::new(button)
726 }
727}
728
729#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
731#[allow(clippy::struct_field_names)]
732pub struct State {
733 is_hovered: bool,
734 is_pressed: bool,
735 is_focused: bool,
736}
737
738impl State {
739 #[inline]
741 pub fn new() -> Self {
742 Self::default()
743 }
744
745 #[inline]
747 pub fn is_focused(self) -> bool {
748 self.is_focused
749 }
750
751 #[inline]
753 pub fn is_hovered(self) -> bool {
754 self.is_hovered
755 }
756
757 #[inline]
759 pub fn focus(&mut self) {
760 self.is_focused = true;
761 }
762
763 #[inline]
765 pub fn unfocus(&mut self) {
766 self.is_focused = false;
767 }
768}
769
770#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
773pub fn update<'a, Message: Clone>(
774 _id: Id,
775 event: &Event,
776 layout: Layout<'_>,
777 cursor: mouse::Cursor,
778 shell: &mut Shell<'_, Message>,
779 on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>,
780 on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>,
781 state: impl FnOnce() -> &'a mut State,
782) {
783 match event {
784 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
785 | Event::Touch(touch::Event::FingerPressed { .. }) => {
786 let state = state();
788 state.unfocus();
789
790 if on_press.is_some() || on_press_down.is_some() {
791 let bounds = layout.bounds();
792
793 if cursor.is_over(bounds) {
794 state.is_pressed = true;
795
796 if let Some(on_press_down) = on_press_down {
797 let msg = (on_press_down)(layout.virtual_offset(), layout.bounds());
798 shell.publish(msg);
799 }
800
801 shell.capture_event();
802 return;
803 }
804 }
805 }
806 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
807 | Event::Touch(touch::Event::FingerLifted { .. }) => {
808 if let Some(on_press) = on_press {
809 let state = state();
810
811 if state.is_pressed {
812 state.is_pressed = false;
813
814 let bounds = layout.bounds();
815
816 if cursor.is_over(bounds) {
817 let msg = (on_press)(layout.virtual_offset(), layout.bounds());
818 shell.publish(msg);
819 }
820
821 shell.capture_event();
822 return;
823 }
824 } else if on_press_down.is_some() {
825 let state = state();
826 state.is_pressed = false;
827 }
828 }
829 #[cfg(feature = "a11y")]
830 Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => {
831 let state = state();
832 if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click)
833 .then_some(on_press)
834 .flatten()
835 {
836 state.is_pressed = false;
837 let msg = (on_press)(layout.virtual_offset(), layout.bounds());
838
839 shell.publish(msg);
840 }
841 shell.capture_event();
842 return;
843 }
844 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
845 if let Some(on_press) = on_press {
846 let state = state();
847 if state.is_focused && *key == keyboard::Key::Named(keyboard::key::Named::Enter) {
848 state.is_pressed = true;
849 let msg = (on_press)(layout.virtual_offset(), layout.bounds());
850
851 shell.publish(msg);
852 shell.capture_event();
853 return;
854 }
855 }
856 }
857 Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
858 let state = state();
859 state.is_hovered = false;
860 state.is_pressed = false;
861 }
862 _ => {}
863 }
864}
865
866#[allow(clippy::too_many_arguments)]
867pub fn draw<Renderer: iced_core::Renderer, Theme>(
868 renderer: &mut Renderer,
869 bounds: Rectangle,
870 viewport_bounds: Rectangle,
871 styling: &super::style::Style,
872 draw_contents: impl FnOnce(&mut Renderer, &Style),
873 is_image: bool,
874) where
875 Theme: super::style::Catalog,
876{
877 let doubled_border_width = styling.border_width * 2.0;
878 let doubled_outline_width = styling.outline_width * 2.0;
879
880 if styling.outline_width > 0.0 {
881 renderer.fill_quad(
882 renderer::Quad {
883 bounds: Rectangle {
884 x: bounds.x - styling.border_width - styling.outline_width,
885 y: bounds.y - styling.border_width - styling.outline_width,
886 width: bounds.width + doubled_border_width + doubled_outline_width,
887 height: bounds.height + doubled_border_width + doubled_outline_width,
888 },
889 border: Border {
890 width: styling.outline_width,
891 color: styling.outline_color,
892 radius: styling.border_radius,
893 },
894 shadow: Shadow::default(),
895 snap: true,
896 },
897 Color::TRANSPARENT,
898 );
899 }
900
901 if styling.background.is_some() || styling.border_width > 0.0 {
902 if styling.shadow_offset != Vector::default() {
903 renderer.fill_quad(
905 renderer::Quad {
906 bounds: Rectangle {
907 x: bounds.x + styling.shadow_offset.x,
908 y: bounds.y + styling.shadow_offset.y,
909 width: bounds.width,
910 height: bounds.height,
911 },
912 border: Border {
913 radius: styling.border_radius,
914 ..Default::default()
915 },
916 shadow: Shadow::default(),
917 snap: true,
918 },
919 Background::Color([0.0, 0.0, 0.0, 0.5].into()),
920 );
921 }
922
923 if let Some(background) = styling.background {
925 renderer.fill_quad(
926 renderer::Quad {
927 bounds,
928 border: Border {
929 radius: styling.border_radius,
930 ..Default::default()
931 },
932 shadow: Shadow::default(),
933 snap: true,
934 },
935 background,
936 );
937 }
938
939 if let Some(overlay) = styling.overlay {
941 renderer.fill_quad(
942 renderer::Quad {
943 bounds,
944 border: Border {
945 radius: styling.border_radius,
946 ..Default::default()
947 },
948 shadow: Shadow::default(),
949 snap: true,
950 },
951 overlay,
952 );
953 }
954
955 draw_contents(renderer, styling);
957
958 let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
959 clipped_bounds.height += styling.border_width;
960 clipped_bounds.width += 1.0;
961
962 renderer.with_layer(clipped_bounds, |renderer| {
964 renderer.fill_quad(
965 renderer::Quad {
966 bounds,
967 border: Border {
968 width: styling.border_width,
969 color: styling.border_color,
970 radius: styling.border_radius,
971 },
972 shadow: Shadow::default(),
973 snap: true,
974 },
975 Color::TRANSPARENT,
976 );
977 })
978 } else {
979 draw_contents(renderer, styling);
980 }
981}
982
983pub fn layout<Renderer>(
985 renderer: &Renderer,
986 limits: &layout::Limits,
987 width: Length,
988 height: Length,
989 padding: Padding,
990 layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
991) -> layout::Node {
992 let limits = limits.width(width).height(height);
993
994 let mut content = layout_content(renderer, &limits.shrink(padding));
995 let padding = padding.fit(content.size(), limits.max());
996 let size = limits
997 .shrink(padding)
998 .resolve(width, height, content.size())
999 .expand(padding);
1000
1001 content = content.move_to(Point::new(padding.left, padding.top));
1002
1003 layout::Node::with_children(size, vec![content])
1004}
1005
1006#[must_use]
1008pub fn mouse_interaction(
1009 layout: Layout<'_>,
1010 cursor: mouse::Cursor,
1011 is_enabled: bool,
1012) -> mouse::Interaction {
1013 let is_mouse_over = cursor.is_over(layout.bounds());
1014
1015 if is_mouse_over && is_enabled {
1016 mouse::Interaction::Pointer
1017 } else {
1018 mouse::Interaction::default()
1019 }
1020}
1021
1022pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
1024 task::effect(Action::Widget(Box::new(operation::focusable::focus(id))))
1025}
1026
1027impl operation::Focusable for State {
1028 #[inline]
1029 fn is_focused(&self) -> bool {
1030 Self::is_focused(*self)
1031 }
1032
1033 #[inline]
1034 fn focus(&mut self) {
1035 Self::focus(self);
1036 }
1037
1038 #[inline]
1039 fn unfocus(&mut self) {
1040 Self::unfocus(self);
1041 }
1042}
1043
1044fn removal_bounds(bounds: Rectangle, offset: f32) -> Rectangle {
1045 Rectangle {
1046 x: bounds.x + bounds.width - 12.0 - offset,
1047 y: bounds.y - 12.0 + offset,
1048 width: 24.0,
1049 height: 24.0,
1050 }
1051}