1use iced_runtime::core::border::Radius;
21use iced_runtime::core::widget::Id;
22use iced_runtime::{keyboard, task, Task};
23#[cfg(feature = "a11y")]
24use std::borrow::Cow;
25
26use crate::core::border::{self, Border};
27use crate::core::event::{self, Event};
28use crate::core::layout;
29use crate::core::mouse;
30use crate::core::overlay;
31use crate::core::renderer;
32use crate::core::theme::palette;
33use crate::core::touch;
34use crate::core::widget::tree::{self, Tree};
35use crate::core::widget::Operation;
36use crate::core::{
37 Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle,
38 Shadow, Shell, Size, Theme, Vector, Widget,
39};
40
41use iced_renderer::core::widget::operation;
42
43#[allow(missing_debug_implementations)]
81pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
82where
83 Renderer: crate::core::Renderer,
84 Theme: Catalog,
85{
86 content: Element<'a, Message, Theme, Renderer>,
87 on_press: Option<OnPress<'a, Message>>,
88 id: Id,
89 #[cfg(feature = "a11y")]
90 name: Option<Cow<'a, str>>,
91 #[cfg(feature = "a11y")]
92 description: Option<iced_accessibility::Description<'a>>,
93 #[cfg(feature = "a11y")]
94 label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
95 width: Length,
96 height: Length,
97 padding: Padding,
98 clip: bool,
99 class: Theme::Class<'a>,
100}
101
102enum OnPress<'a, Message> {
103 Direct(Message),
104 Closure(Box<dyn Fn() -> Message + 'a>),
105}
106
107impl<'a, Message: Clone> OnPress<'a, Message> {
108 fn get(&self) -> Message {
109 match self {
110 OnPress::Direct(message) => message.clone(),
111 OnPress::Closure(f) => f(),
112 }
113 }
114}
115
116impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
117where
118 Renderer: crate::core::Renderer,
119 Theme: Catalog,
120{
121 pub fn new(
123 content: impl Into<Element<'a, Message, Theme, Renderer>>,
124 ) -> Self {
125 let content = content.into();
126 let size = content.as_widget().size_hint();
127
128 Button {
129 content,
130 id: Id::unique(),
131 #[cfg(feature = "a11y")]
132 name: None,
133 #[cfg(feature = "a11y")]
134 description: None,
135 #[cfg(feature = "a11y")]
136 label: None,
137 on_press: None,
138 width: size.width.fluid(),
139 height: size.height.fluid(),
140 padding: DEFAULT_PADDING,
141 clip: false,
142 class: Theme::default(),
143 }
144 }
145
146 pub fn width(mut self, width: impl Into<Length>) -> Self {
148 self.width = width.into();
149 self
150 }
151
152 pub fn height(mut self, height: impl Into<Length>) -> Self {
154 self.height = height.into();
155 self
156 }
157
158 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
160 self.padding = padding.into();
161 self
162 }
163
164 pub fn on_press(mut self, on_press: Message) -> Self {
168 self.on_press = Some(OnPress::Direct(on_press));
169 self
170 }
171
172 pub fn on_press_with(
181 mut self,
182 on_press: impl Fn() -> Message + 'a,
183 ) -> Self {
184 self.on_press = Some(OnPress::Closure(Box::new(on_press)));
185 self
186 }
187
188 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
193 self.on_press = on_press.map(OnPress::Direct);
194 self
195 }
196
197 pub fn clip(mut self, clip: bool) -> Self {
200 self.clip = clip;
201 self
202 }
203
204 #[must_use]
206 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
207 where
208 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
209 {
210 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
211 self
212 }
213
214 #[cfg(feature = "advanced")]
216 #[must_use]
217 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
218 self.class = class.into();
219 self
220 }
221
222 pub fn id(mut self, id: Id) -> Self {
224 self.id = id;
225 self
226 }
227
228 #[cfg(feature = "a11y")]
229 pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
231 self.name = Some(name.into());
232 self
233 }
234
235 #[cfg(feature = "a11y")]
236 pub fn description_widget<T: iced_accessibility::Describes>(
238 mut self,
239 description: &T,
240 ) -> Self {
241 self.description = Some(iced_accessibility::Description::Id(
242 description.description(),
243 ));
244 self
245 }
246
247 #[cfg(feature = "a11y")]
248 pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
250 self.description =
251 Some(iced_accessibility::Description::Text(description.into()));
252 self
253 }
254
255 #[cfg(feature = "a11y")]
256 pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
258 self.label =
259 Some(label.label().into_iter().map(|l| l.into()).collect());
260 self
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
265struct State {
266 is_hovered: bool,
267 is_pressed: bool,
268 is_focused: bool,
269}
270
271impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
272 for Button<'a, Message, Theme, Renderer>
273where
274 Message: 'a + Clone,
275 Renderer: 'a + crate::core::Renderer,
276 Theme: Catalog,
277{
278 fn tag(&self) -> tree::Tag {
279 tree::Tag::of::<State>()
280 }
281
282 fn state(&self) -> tree::State {
283 tree::State::new(State::default())
284 }
285
286 fn children(&self) -> Vec<Tree> {
287 vec![Tree::new(&self.content)]
288 }
289
290 fn diff(&mut self, tree: &mut Tree) {
291 tree.diff_children(std::slice::from_mut(&mut self.content));
292 }
293
294 fn size(&self) -> Size<Length> {
295 Size {
296 width: self.width,
297 height: self.height,
298 }
299 }
300
301 fn layout(
302 &self,
303 tree: &mut Tree,
304 renderer: &Renderer,
305 limits: &layout::Limits,
306 ) -> layout::Node {
307 layout::padded(
308 limits,
309 self.width,
310 self.height,
311 self.padding,
312 |limits| {
313 self.content.as_widget().layout(
314 &mut tree.children[0],
315 renderer,
316 limits,
317 )
318 },
319 )
320 }
321
322 fn operate(
323 &self,
324 tree: &mut Tree,
325 layout: Layout<'_>,
326 renderer: &Renderer,
327 operation: &mut dyn Operation,
328 ) {
329 operation.container(None, layout.bounds(), &mut |operation| {
330 self.content.as_widget().operate(
331 &mut tree.children[0],
332 layout
333 .children()
334 .next()
335 .unwrap()
336 .with_virtual_offset(layout.virtual_offset()),
337 renderer,
338 operation,
339 );
340 });
341 }
342
343 fn on_event(
344 &mut self,
345 tree: &mut Tree,
346 event: Event,
347 layout: Layout<'_>,
348 cursor: mouse::Cursor,
349 renderer: &Renderer,
350 clipboard: &mut dyn Clipboard,
351 shell: &mut Shell<'_, Message>,
352 viewport: &Rectangle,
353 ) -> event::Status {
354 if let event::Status::Captured = self.content.as_widget_mut().on_event(
355 &mut tree.children[0],
356 event.clone(),
357 layout
358 .children()
359 .next()
360 .unwrap()
361 .with_virtual_offset(layout.virtual_offset()),
362 cursor,
363 renderer,
364 clipboard,
365 shell,
366 viewport,
367 ) {
368 return event::Status::Captured;
369 }
370
371 match event {
372 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
373 | Event::Touch(touch::Event::FingerPressed { .. }) => {
374 if self.on_press.is_some() {
375 let bounds = layout.bounds();
376
377 if cursor.is_over(bounds) {
378 let state = tree.state.downcast_mut::<State>();
379
380 state.is_pressed = true;
381
382 return event::Status::Captured;
383 }
384 }
385 }
386 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
387 | Event::Touch(touch::Event::FingerLifted { .. }) => {
388 if let Some(on_press) = self.on_press.as_ref().map(OnPress::get)
389 {
390 let state = tree.state.downcast_mut::<State>();
391
392 if state.is_pressed {
393 state.is_pressed = false;
394
395 let bounds = layout.bounds();
396
397 if cursor.is_over(bounds) {
398 shell.publish(on_press);
399 }
400
401 return event::Status::Captured;
402 }
403 }
404 }
405 #[cfg(feature = "a11y")]
406 Event::A11y(
407 event_id,
408 iced_accessibility::accesskit::ActionRequest { action, .. },
409 ) => {
410 let state = tree.state.downcast_mut::<State>();
411 if let Some(Some(on_press)) = (self.id == event_id
412 && matches!(
413 action,
414 iced_accessibility::accesskit::Action::Default
415 ))
416 .then(|| self.on_press.as_ref())
417 {
418 state.is_pressed = false;
419 shell.publish(on_press.get());
420 }
421 return event::Status::Captured;
422 }
423 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
424 if let Some(on_press) = self.on_press.as_ref() {
425 let state = tree.state.downcast_mut::<State>();
426 if state.is_focused
427 && matches!(
428 key,
429 keyboard::Key::Named(keyboard::key::Named::Enter)
430 )
431 {
432 state.is_pressed = true;
433 shell.publish(on_press.get());
434 return event::Status::Captured;
435 }
436 }
437 }
438 Event::Touch(touch::Event::FingerLost { .. })
439 | Event::Mouse(mouse::Event::CursorLeft) => {
440 let state = tree.state.downcast_mut::<State>();
441 state.is_hovered = false;
442 state.is_pressed = false;
443 }
444 _ => {}
445 }
446
447 event::Status::Ignored
448 }
449
450 fn draw(
451 &self,
452 tree: &Tree,
453 renderer: &mut Renderer,
454 theme: &Theme,
455 renderer_style: &renderer::Style,
456 layout: Layout<'_>,
457 cursor: mouse::Cursor,
458 viewport: &Rectangle,
459 ) {
460 let bounds = layout.bounds();
461 let content_layout = layout
462 .children()
463 .next()
464 .unwrap()
465 .with_virtual_offset(layout.virtual_offset());
466 let is_mouse_over = cursor.is_over(bounds);
467
468 let status = if self.on_press.is_none() {
469 Status::Disabled
470 } else if is_mouse_over {
471 let state = tree.state.downcast_ref::<State>();
472
473 if state.is_pressed {
474 Status::Pressed
475 } else {
476 Status::Hovered
477 }
478 } else {
479 Status::Active
480 };
481
482 let style = theme.style(&self.class, status);
483
484 if style.background.is_some()
485 || style.border.width > 0.0
486 || style.shadow.color.a > 0.0
487 {
488 renderer.fill_quad(
489 renderer::Quad {
490 bounds,
491 border: style.border,
492 shadow: style.shadow,
493 },
494 style
495 .background
496 .unwrap_or(Background::Color(Color::TRANSPARENT)),
497 );
498 }
499
500 let viewport = if self.clip {
501 bounds.intersection(viewport).unwrap_or(*viewport)
502 } else {
503 *viewport
504 };
505
506 self.content.as_widget().draw(
507 &tree.children[0],
508 renderer,
509 theme,
510 &renderer::Style {
511 text_color: style.text_color,
512 icon_color: style
513 .icon_color
514 .unwrap_or(renderer_style.icon_color),
515 scale_factor: renderer_style.scale_factor,
516 },
517 content_layout,
518 cursor,
519 &viewport,
520 );
521 }
522
523 fn mouse_interaction(
524 &self,
525 _tree: &Tree,
526 layout: Layout<'_>,
527 cursor: mouse::Cursor,
528 _viewport: &Rectangle,
529 _renderer: &Renderer,
530 ) -> mouse::Interaction {
531 let is_mouse_over = cursor.is_over(layout.bounds());
532
533 if is_mouse_over && self.on_press.is_some() {
534 mouse::Interaction::Pointer
535 } else {
536 mouse::Interaction::default()
537 }
538 }
539
540 fn overlay<'b>(
541 &'b mut self,
542 tree: &'b mut Tree,
543 layout: Layout<'_>,
544 renderer: &Renderer,
545 translation: Vector,
546 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
547 self.content.as_widget_mut().overlay(
548 &mut tree.children[0],
549 layout
550 .children()
551 .next()
552 .unwrap()
553 .with_virtual_offset(layout.virtual_offset()),
554 renderer,
555 translation,
556 )
557 }
558
559 #[cfg(feature = "a11y")]
560 fn a11y_nodes(
562 &self,
563 layout: Layout<'_>,
564 state: &Tree,
565 p: mouse::Cursor,
566 ) -> iced_accessibility::A11yTree {
567 use iced_accessibility::{
568 accesskit::{
569 Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role,
570 },
571 A11yNode, A11yTree,
572 };
573
574 let child_layout = layout.children().next().unwrap();
575 let child_tree = &state.children[0];
576 let child_tree = self.content.as_widget().a11y_nodes(
577 child_layout.with_virtual_offset(layout.virtual_offset()),
578 child_tree,
579 p,
580 );
581
582 let Rectangle {
583 x,
584 y,
585 width,
586 height,
587 } = layout.bounds();
588 let bounds = Rect::new(
589 x as f64,
590 y as f64,
591 (x + width) as f64,
592 (y + height) as f64,
593 );
594 let is_hovered = state.state.downcast_ref::<State>().is_hovered;
595
596 let mut node = NodeBuilder::new(Role::Button);
597 node.add_action(Action::Focus);
598 node.add_action(Action::Default);
599 node.set_bounds(bounds);
600 if let Some(name) = self.name.as_ref() {
601 node.set_name(name.clone());
602 }
603 match self.description.as_ref() {
604 Some(iced_accessibility::Description::Id(id)) => {
605 node.set_described_by(
606 id.iter()
607 .cloned()
608 .map(|id| NodeId::from(id))
609 .collect::<Vec<_>>(),
610 );
611 }
612 Some(iced_accessibility::Description::Text(text)) => {
613 node.set_description(text.clone());
614 }
615 None => {}
616 }
617
618 if let Some(label) = self.label.as_ref() {
619 node.set_labelled_by(label.clone());
620 }
621
622 if self.on_press.is_none() {
623 node.set_disabled()
624 }
625 if is_hovered {
626 node.set_hovered()
627 }
628 node.set_default_action_verb(DefaultActionVerb::Click);
629
630 A11yTree::node_with_child_tree(
631 A11yNode::new(node, self.id.clone()),
632 child_tree,
633 )
634 }
635
636 fn id(&self) -> Option<Id> {
637 Some(self.id.clone())
638 }
639
640 fn set_id(&mut self, id: Id) {
641 self.id = id;
642 }
643}
644
645impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
646 for Element<'a, Message, Theme, Renderer>
647where
648 Message: Clone + 'a,
649 Theme: Catalog + 'a,
650 Renderer: crate::core::Renderer + 'a,
651{
652 fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
653 Self::new(button)
654 }
655}
656
657pub(crate) const DEFAULT_PADDING: Padding = Padding {
659 top: 5.0,
660 bottom: 5.0,
661 right: 10.0,
662 left: 10.0,
663};
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
667pub enum Status {
668 Active,
670 Hovered,
672 Pressed,
674 Disabled,
676}
677
678#[derive(Debug, Clone, Copy, PartialEq)]
683pub struct Style {
684 pub background: Option<Background>,
686 pub border_radius: Radius,
688 pub border_width: f32,
690 pub border_color: Color,
692 pub icon_color: Option<Color>,
694 pub text_color: Color,
696 pub border: Border,
698 pub shadow: Shadow,
700}
701
702impl Style {
703 pub fn with_background(self, background: impl Into<Background>) -> Self {
705 Self {
706 background: Some(background.into()),
707 ..self
708 }
709 }
710
711 }
731
732impl Default for Style {
733 fn default() -> Self {
734 Self {
735 background: None,
736 border_radius: 0.0.into(),
737 border_width: 0.0,
738 border_color: Color::TRANSPARENT,
739 icon_color: None,
740 text_color: Color::BLACK,
741 border: Border::default(),
742 shadow: Shadow::default(),
743 }
744 }
745}
746
747pub trait Catalog {
797 type Class<'a>;
799
800 fn default<'a>() -> Self::Class<'a>;
802
803 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
805}
806
807pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
809
810impl Catalog for Theme {
811 type Class<'a> = StyleFn<'a, Self>;
812
813 fn default<'a>() -> Self::Class<'a> {
814 Box::new(primary)
815 }
816
817 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
818 class(self, status)
819 }
820}
821
822pub fn primary(theme: &Theme, status: Status) -> Style {
824 let palette = theme.extended_palette();
825 let base = styled(palette.primary.strong);
826
827 match status {
828 Status::Active | Status::Pressed => base,
829 Status::Hovered => Style {
830 background: Some(Background::Color(palette.primary.base.color)),
831 ..base
832 },
833 Status::Disabled => disabled(base),
834 }
835}
836
837pub fn secondary(theme: &Theme, status: Status) -> Style {
839 let palette = theme.extended_palette();
840 let base = styled(palette.secondary.base);
841
842 match status {
843 Status::Active | Status::Pressed => base,
844 Status::Hovered => Style {
845 background: Some(Background::Color(palette.secondary.strong.color)),
846 ..base
847 },
848 Status::Disabled => disabled(base),
849 }
850}
851
852pub fn success(theme: &Theme, status: Status) -> Style {
854 let palette = theme.extended_palette();
855 let base = styled(palette.success.base);
856
857 match status {
858 Status::Active | Status::Pressed => base,
859 Status::Hovered => Style {
860 background: Some(Background::Color(palette.success.strong.color)),
861 ..base
862 },
863 Status::Disabled => disabled(base),
864 }
865}
866
867pub fn danger(theme: &Theme, status: Status) -> Style {
869 let palette = theme.extended_palette();
870 let base = styled(palette.danger.base);
871
872 match status {
873 Status::Active | Status::Pressed => base,
874 Status::Hovered => Style {
875 background: Some(Background::Color(palette.danger.strong.color)),
876 ..base
877 },
878 Status::Disabled => disabled(base),
879 }
880}
881
882pub fn text(theme: &Theme, status: Status) -> Style {
884 let palette = theme.extended_palette();
885
886 let base = Style {
887 text_color: palette.background.base.text,
888 ..Style::default()
889 };
890
891 match status {
892 Status::Active | Status::Pressed => base,
893 Status::Hovered => Style {
894 text_color: palette.background.base.text.scale_alpha(0.8),
895 ..base
896 },
897 Status::Disabled => disabled(base),
898 }
899}
900
901fn styled(pair: palette::Pair) -> Style {
902 Style {
903 background: Some(Background::Color(pair.color)),
904 text_color: pair.text,
905 border: border::rounded(2),
906 ..Style::default()
907 }
908}
909
910fn disabled(style: Style) -> Style {
911 Style {
912 background: style
913 .background
914 .map(|background| background.scale_alpha(0.5)),
915 text_color: style.text_color.scale_alpha(0.5),
916 ..style
917 }
918}
919
920pub fn focus<Message: 'static + Send>(id: Id) -> Task<Message> {
922 task::widget(operation::focusable::focus(id))
923}
924
925impl operation::Focusable for State {
926 fn is_focused(&self) -> bool {
927 self.is_focused
928 }
929
930 fn focus(&mut self) {
931 self.is_focused = true;
932 }
933
934 fn unfocus(&mut self) {
935 self.is_focused = false;
936 }
937}