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