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