1use super::*;
4use iced_widget::button::{Catalog, Status, Style, StyleFn};
5use iced_widget::core::overlay;
6use iced_widget::graphics::geometry;
7use iced_widget::renderer::wgpu::primitive;
8
9use super::ripple::{PressRippleState, RippleConfig, RippleStart, RippleStyle, draw_ripples};
10use super::support::{AnimatedScalar, duration_ms};
11use crate::utils::state_layer;
12
13#[cfg(test)]
14use super::ripple::{
15 PressRipple as Ripple, RIPPLE_CLIP_MAX_SAMPLES, RIPPLE_CLIP_MIN_SAMPLES, clamped_ripple_origin,
16 get_ripple_start_radius, ripple_clip_sample_count, ripple_noise_phases, ripple_target_radius,
17 rounded_rect_span_at_y, unbounded_ripple_target_radius,
18};
19
20const TOUCH_CLICK_SLOP: f32 = 8.0;
21
22pub struct Button<'a, Message, Renderer = iced_widget::Renderer>
24where
25 Renderer: geometry::Renderer,
26{
27 content: Element<'a, Message, Theme, Renderer>,
28 on_press: Option<OnPress<'a, Message>>,
29 width: Length,
30 height: Length,
31 padding: Padding,
32 clip: bool,
33 class: <Theme as Catalog>::Class<'a>,
34 status: Option<Status>,
35}
36
37enum OnPress<'a, Message> {
38 Direct(Message),
39 Closure(Box<dyn Fn() -> Message + 'a>),
40}
41
42impl<Message, Renderer> std::fmt::Debug for Button<'_, Message, Renderer>
43where
44 Renderer: geometry::Renderer,
45{
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("Button")
48 .field("width", &self.width)
49 .field("height", &self.height)
50 .field("padding", &self.padding)
51 .field("clip", &self.clip)
52 .field("status", &self.status)
53 .finish_non_exhaustive()
54 }
55}
56
57impl<Message: Clone> OnPress<'_, Message> {
58 fn get(&self) -> Message {
59 match self {
60 OnPress::Direct(message) => message.clone(),
61 OnPress::Closure(f) => f(),
62 }
63 }
64}
65
66impl<'a, Message, Renderer> Button<'a, Message, Renderer>
67where
68 Message: Clone + 'a,
69 Renderer: geometry::Renderer + 'a,
70{
71 pub fn new(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
73 let content = content.into();
74 let size = content.as_widget().size_hint();
75
76 Self {
77 content,
78 on_press: None,
79 width: size.width.fluid(),
80 height: size.height.fluid(),
81 padding: iced_widget::button::DEFAULT_PADDING,
82 clip: false,
83 class: <Theme as Catalog>::default(),
84 status: None,
85 }
86 }
87
88 pub fn width(mut self, width: impl Into<Length>) -> Self {
90 self.width = width.into();
91 self
92 }
93
94 pub fn height(mut self, height: impl Into<Length>) -> Self {
96 self.height = height.into();
97 self
98 }
99
100 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
102 self.padding = padding.into();
103 self
104 }
105
106 pub fn on_press(mut self, on_press: Message) -> Self {
108 self.on_press = Some(OnPress::Direct(on_press));
109 self
110 }
111
112 pub fn on_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
114 self.on_press = Some(OnPress::Closure(Box::new(on_press)));
115 self
116 }
117
118 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
120 self.on_press = on_press.map(OnPress::Direct);
121 self
122 }
123
124 pub fn clip(mut self, clip: bool) -> Self {
126 self.clip = clip;
127 self
128 }
129
130 #[must_use]
132 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self {
133 self.class = Box::new(style) as StyleFn<'a, Theme>;
134 self
135 }
136}
137
138#[derive(Debug, Clone)]
139struct ButtonState {
140 is_pressed: bool,
141 is_hovered: bool,
142 state_layer_opacity: AnimatedScalar,
143 touch_press_position: Option<Point>,
144 ripples: PressRippleState,
145 last_status: Option<Status>,
146 now: Option<Instant>,
147}
148
149impl Default for ButtonState {
150 fn default() -> Self {
151 Self {
152 is_pressed: false,
153 is_hovered: false,
154 state_layer_opacity: AnimatedScalar::new(0.0),
155 touch_press_position: None,
156 ripples: PressRippleState::default(),
157 last_status: None,
158 now: None,
159 }
160 }
161}
162
163impl ButtonState {
164 fn press(&mut self, origin: Point, now: Instant) {
165 self.is_pressed = true;
166 self.ripples.press(
167 origin,
168 now,
169 RippleStart::Additive,
170 RippleStyle::material_patterned(),
171 );
172 self.now = Some(now);
173 }
174
175 fn release(&mut self, now: Instant) {
176 self.is_pressed = false;
177 self.touch_press_position = None;
178
179 self.ripples.release(now);
180
181 self.now = Some(now);
182 }
183
184 fn cancel(&mut self, now: Instant) {
185 self.release(now);
186 }
187
188 fn sync_hover(&mut self, is_hovered: bool, now: Instant) -> bool {
189 if self.is_hovered == is_hovered {
190 return false;
191 }
192
193 self.is_hovered = is_hovered;
194 self.animate_state_layer(now);
195
196 true
197 }
198
199 fn snap_state_layer_to_hover_target(&mut self) {
200 self.state_layer_opacity
201 .snap_to(ButtonDrawState::hover_target(self.is_hovered));
202 }
203
204 fn animate_state_layer(&mut self, now: Instant) {
205 self.state_layer_opacity.set_target(
206 ButtonDrawState::hover_target(self.is_hovered),
207 now,
208 duration_ms(tokens::state::STATE_LAYER_TRANSITION_DURATION_MS),
209 tokens::motion::EASING_LINEAR,
210 );
211 }
212
213 fn advance(&mut self, now: Instant) -> bool {
214 self.now = Some(now);
215 self.prune(now);
216
217 self.state_layer_opacity.advance(now) || self.has_visible_ripples(now)
218 }
219
220 fn state_layer_opacity(&self) -> f32 {
221 self.state_layer_opacity.value
222 }
223
224 fn prune(&mut self, now: Instant) {
225 self.ripples.prune(now);
226 }
227
228 fn has_visible_ripples(&self, now: Instant) -> bool {
229 self.ripples.has_visible_ripples(now)
230 }
231
232 #[cfg(test)]
233 fn ripple_opacity(&self, now: Instant) -> f32 {
234 self.ripples.ripple_opacity(now)
235 }
236}
237
238impl<Message, Renderer> Widget<Message, Theme, Renderer> for Button<'_, Message, Renderer>
239where
240 Message: Clone,
241 Renderer: geometry::Renderer + primitive::Renderer,
242{
243 fn tag(&self) -> tree::Tag {
244 tree::Tag::of::<ButtonState>()
245 }
246
247 fn state(&self) -> tree::State {
248 tree::State::new(ButtonState::default())
249 }
250
251 fn children(&self) -> Vec<Tree> {
252 vec![Tree::new(&self.content)]
253 }
254
255 fn diff(&self, tree: &mut Tree) {
256 tree.diff_children(std::slice::from_ref(&self.content));
257 }
258
259 fn size(&self) -> Size<Length> {
260 Size {
261 width: self.width,
262 height: self.height,
263 }
264 }
265
266 fn layout(
267 &mut self,
268 tree: &mut Tree,
269 renderer: &Renderer,
270 limits: &layout::Limits,
271 ) -> layout::Node {
272 layout::padded(limits, self.width, self.height, self.padding, |limits| {
273 self.content
274 .as_widget_mut()
275 .layout(&mut tree.children[0], renderer, limits)
276 })
277 }
278
279 fn operate(
280 &mut self,
281 tree: &mut Tree,
282 layout: Layout<'_>,
283 renderer: &Renderer,
284 operation: &mut dyn core_widget::Operation,
285 ) {
286 operation.container(None, layout.bounds());
287 operation.traverse(&mut |operation| {
288 self.content.as_widget_mut().operate(
289 &mut tree.children[0],
290 layout.children().next().unwrap(),
291 renderer,
292 operation,
293 );
294 });
295 }
296
297 fn update(
298 &mut self,
299 tree: &mut Tree,
300 event: &Event,
301 layout: Layout<'_>,
302 cursor: mouse::Cursor,
303 renderer: &Renderer,
304 clipboard: &mut dyn Clipboard,
305 shell: &mut Shell<'_, Message>,
306 viewport: &Rectangle,
307 ) {
308 self.content.as_widget_mut().update(
309 &mut tree.children[0],
310 event,
311 layout.children().next().unwrap(),
312 cursor,
313 renderer,
314 clipboard,
315 shell,
316 viewport,
317 );
318
319 if shell.is_event_captured() {
320 return;
321 }
322
323 let bounds = layout.bounds();
324 let now = match event {
325 Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
326 _ => None,
327 };
328 let now_or_current = || now.unwrap_or_else(Instant::now);
329 let state = tree.state.downcast_mut::<ButtonState>();
330 let is_touch_event = matches!(event, Event::Touch(_));
331 let is_hovered = self.on_press.is_some() && !is_touch_event && cursor.is_over(bounds);
332 let interaction = ButtonInteraction {
333 event,
334 cursor,
335 is_hovered,
336 };
337 let should_snap_initial_redraw_hover = interaction.should_snap_initial_redraw(state);
338
339 if interaction.should_sync_hover() && state.sync_hover(is_hovered, now_or_current()) {
340 if should_snap_initial_redraw_hover {
341 state.snap_state_layer_to_hover_target();
342 }
343
344 shell.request_redraw();
345 }
346
347 match event {
348 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
349 | Event::Touch(touch::Event::FingerPressed { .. })
350 if self.on_press.is_some() =>
351 {
352 if let Some(origin) = press_origin(event, bounds, cursor) {
353 state.press(origin, now_or_current());
354 state.touch_press_position = touch_position(event, cursor);
355 shell.capture_event();
356 shell.request_redraw();
357 }
358 }
359 Event::Touch(touch::Event::FingerMoved { .. })
360 if state.is_pressed
361 && TouchClick {
362 press_position: state.touch_press_position,
363 event,
364 cursor,
365 }
366 .moved_beyond_slop() =>
367 {
368 state.cancel(now_or_current());
369 shell.request_redraw();
370 }
371 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
372 | Event::Touch(touch::Event::FingerLifted { .. })
373 if state.is_pressed =>
374 {
375 state.release(now_or_current());
376
377 if release_is_over(event, bounds, cursor)
378 && let Some(on_press) = &self.on_press
379 {
380 shell.publish(on_press.get());
381 }
382
383 shell.capture_event();
384 shell.request_redraw();
385 }
386 Event::Touch(touch::Event::FingerLost { .. }) if state.is_pressed => {
387 state.cancel(now_or_current());
388 shell.request_redraw();
389 }
390 _ => {}
391 }
392
393 let current_status =
394 button_status(self.on_press.is_some(), state.is_pressed, bounds, cursor);
395
396 if let Some(now) = now {
397 if state.advance(now) {
398 shell.request_redraw();
399 }
400
401 self.status = Some(current_status);
402 state.last_status = Some(current_status);
403 } else if self.status.is_some_and(|status| status != current_status)
404 || state.state_layer_opacity.is_animating()
405 || state.has_visible_ripples(state.now.unwrap_or_else(Instant::now))
406 {
407 shell.request_redraw();
408 }
409 }
410
411 fn draw(
412 &self,
413 tree: &Tree,
414 renderer: &mut Renderer,
415 theme: &Theme,
416 _style: &renderer::Style,
417 layout: Layout<'_>,
418 cursor: mouse::Cursor,
419 viewport: &Rectangle,
420 ) {
421 let bounds = layout.bounds();
422
423 if bounds.width < 1.0 || bounds.height < 1.0 {
424 return;
425 }
426
427 let state = tree.state.downcast_ref::<ButtonState>();
428 let status = button_status(self.on_press.is_some(), state.is_pressed, bounds, cursor);
429 let now = state.now.unwrap_or_else(Instant::now);
430 let style = button_draw_style(theme, &self.class, status);
431 let content_layout = layout.children().next().unwrap();
432
433 if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
434 renderer.fill_quad(
435 renderer::Quad {
436 bounds,
437 border: style.border,
438 shadow: style.shadow,
439 snap: style.snap,
440 },
441 style
442 .background
443 .unwrap_or(Background::Color(Color::TRANSPARENT)),
444 );
445 }
446
447 let viewport = if self.clip {
448 bounds.intersection(viewport).unwrap_or(*viewport)
449 } else {
450 *viewport
451 };
452
453 self.content.as_widget().draw(
454 &tree.children[0],
455 renderer,
456 theme,
457 &renderer::Style {
458 text_color: style.text_color,
459 },
460 content_layout,
461 cursor,
462 &viewport,
463 );
464
465 draw_button_state_layer(
466 renderer,
467 bounds,
468 &style,
469 ButtonDrawState { state, status }.layer_opacity(),
470 );
471
472 draw_ripples(
473 renderer,
474 bounds,
475 &state.ripples,
476 style.text_color,
477 RippleConfig::bounded(style.border.radius),
478 now,
479 );
480 }
481
482 fn mouse_interaction(
483 &self,
484 _tree: &Tree,
485 layout: Layout<'_>,
486 cursor: mouse::Cursor,
487 _viewport: &Rectangle,
488 _renderer: &Renderer,
489 ) -> mouse::Interaction {
490 if cursor.is_over(layout.bounds()) && self.on_press.is_some() {
491 mouse::Interaction::Pointer
492 } else {
493 mouse::Interaction::default()
494 }
495 }
496
497 fn overlay<'b>(
498 &'b mut self,
499 tree: &'b mut Tree,
500 layout: Layout<'b>,
501 renderer: &Renderer,
502 viewport: &Rectangle,
503 translation: iced_widget::core::Vector,
504 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
505 self.content.as_widget_mut().overlay(
506 &mut tree.children[0],
507 layout.children().next().unwrap(),
508 renderer,
509 viewport,
510 translation,
511 )
512 }
513}
514
515impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
516 for Element<'a, Message, Theme, Renderer>
517where
518 Message: Clone + 'a,
519 Renderer: geometry::Renderer + primitive::Renderer + 'a,
520{
521 fn from(button: Button<'a, Message, Renderer>) -> Self {
522 Element::new(button)
523 }
524}
525
526#[derive(Debug, Clone, Copy)]
527struct ButtonInteraction<'a> {
528 event: &'a Event,
529 cursor: mouse::Cursor,
530 is_hovered: bool,
531}
532
533impl ButtonInteraction<'_> {
534 fn should_sync_hover(self) -> bool {
535 match self.event {
536 Event::Window(window::Event::RedrawRequested(_)) => {
537 !matches!(self.cursor, mouse::Cursor::Unavailable)
538 }
539 Event::Mouse(_) | Event::Touch(_) => true,
540 _ => false,
541 }
542 }
543
544 fn should_snap_initial_redraw(self, state: &ButtonState) -> bool {
545 matches!(self.event, Event::Window(window::Event::RedrawRequested(_)))
546 && state.last_status.is_none()
547 && self.is_hovered
548 }
549}
550
551#[derive(Debug, Clone, Copy)]
552struct ButtonDrawState<'a> {
553 state: &'a ButtonState,
554 status: Status,
555}
556
557impl ButtonDrawState<'_> {
558 fn layer_opacity(self) -> f32 {
559 if matches!(self.status, Status::Hovered) && self.state.last_status.is_none() {
560 Self::hover_target(true)
561 } else {
562 self.state.state_layer_opacity()
563 }
564 }
565
566 fn hover_target(is_hovered: bool) -> f32 {
567 if is_hovered {
568 tokens::state::HOVER_STATE_LAYER_OPACITY
569 } else {
570 0.0
571 }
572 }
573}
574
575fn button_status(
576 is_enabled: bool,
577 is_pressed: bool,
578 bounds: Rectangle,
579 cursor: mouse::Cursor,
580) -> Status {
581 if !is_enabled {
582 Status::Disabled
583 } else if cursor.is_over(bounds) {
584 if is_pressed {
585 Status::Pressed
586 } else {
587 Status::Hovered
588 }
589 } else {
590 Status::Active
591 }
592}
593
594fn button_draw_style(
595 theme: &Theme,
596 class: &<Theme as Catalog>::Class<'_>,
597 status: Status,
598) -> Style {
599 if matches!(status, Status::Pressed | Status::Hovered) {
600 return theme.style(class, Status::Active);
601 }
602
603 theme.style(class, status)
604}
605
606fn draw_button_state_layer<Renderer>(
607 renderer: &mut Renderer,
608 bounds: Rectangle,
609 style: &Style,
610 opacity: f32,
611) where
612 Renderer: geometry::Renderer,
613{
614 if opacity <= 0.0 {
615 return;
616 }
617
618 renderer.fill_quad(
619 renderer::Quad {
620 bounds,
621 border: Border {
622 radius: style.border.radius,
623 ..Border::default()
624 },
625 snap: style.snap,
626 ..renderer::Quad::default()
627 },
628 state_layer(style.text_color, opacity),
629 );
630}
631
632fn press_origin(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> Option<Point> {
633 if cursor.position().is_some() {
634 return cursor.position_in(bounds);
635 }
636
637 if cursor.is_levitating() {
638 return None;
639 }
640
641 match event {
642 Event::Touch(touch::Event::FingerPressed { position, .. }) => {
643 relative_position(*position, bounds)
644 }
645 _ => cursor.position_in(bounds),
646 }
647}
648
649fn release_is_over(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> bool {
650 if cursor.position().is_some() {
651 return cursor.is_over(bounds);
652 }
653
654 if cursor.is_levitating() {
655 return false;
656 }
657
658 match event {
659 Event::Touch(touch::Event::FingerLifted { position, .. }) => bounds.contains(*position),
660 _ => cursor.is_over(bounds),
661 }
662}
663
664fn touch_position(event: &Event, cursor: mouse::Cursor) -> Option<Point> {
665 if cursor.position().is_some() {
666 return cursor.position();
667 }
668
669 if cursor.is_levitating() {
670 return None;
671 }
672
673 match event {
674 Event::Touch(
675 touch::Event::FingerPressed { position, .. }
676 | touch::Event::FingerMoved { position, .. }
677 | touch::Event::FingerLifted { position, .. }
678 | touch::Event::FingerLost { position, .. },
679 ) => Some(*position),
680 _ => None,
681 }
682}
683
684#[derive(Debug, Clone, Copy)]
685struct TouchClick<'a> {
686 press_position: Option<Point>,
687 event: &'a Event,
688 cursor: mouse::Cursor,
689}
690
691impl TouchClick<'_> {
692 fn moved_beyond_slop(self) -> bool {
693 let Some(press_position) = self.press_position else {
694 return false;
695 };
696 let Some(position) = touch_position(self.event, self.cursor) else {
697 return false;
698 };
699 let dx = position.x - press_position.x;
700 let dy = position.y - press_position.y;
701
702 dx * dx + dy * dy > TOUCH_CLICK_SLOP * TOUCH_CLICK_SLOP
703 }
704}
705
706fn relative_position(position: Point, bounds: Rectangle) -> Option<Point> {
707 bounds
708 .contains(position)
709 .then(|| position - iced_widget::core::Vector::new(bounds.x, bounds.y))
710}
711
712#[cfg(test)]
713#[path = "../../../tests/widget/component/button.rs"]
714mod tests;
715
716type ButtonStyle = fn(&Theme, Status) -> Style;
717
718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
719pub enum ButtonVariant {
720 Elevated,
721 Filled,
722 FilledTonal,
723 Outlined,
724 Text,
725}
726
727impl ButtonVariant {
728 const fn style(self) -> ButtonStyle {
729 match self {
730 Self::Elevated => button_style::elevated,
731 Self::Filled => button_style::filled,
732 Self::FilledTonal => button_style::filled_tonal,
733 Self::Outlined => button_style::outlined,
734 Self::Text => button_style::text,
735 }
736 }
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, Eq)]
740pub enum IconButtonVariant {
741 Standard,
742 Filled,
743 FilledTonal,
744 Outlined,
745}
746
747impl IconButtonVariant {
748 const fn style(self) -> ButtonStyle {
749 match self {
750 Self::Standard => button_style::icon,
751 Self::Filled => button_style::filled_icon,
752 Self::FilledTonal => button_style::filled_tonal_icon,
753 Self::Outlined => button_style::outlined_icon,
754 }
755 }
756}
757
758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
759pub enum FabVariant {
760 Primary,
761 Secondary,
762 Tertiary,
763 Surface,
764}
765
766impl FabVariant {
767 const fn standard_style(self) -> ButtonStyle {
768 match self {
769 Self::Primary => button_style::fab_primary,
770 Self::Secondary => button_style::fab_secondary,
771 Self::Tertiary => button_style::fab_tertiary,
772 Self::Surface => button_style::fab_surface,
773 }
774 }
775
776 const fn small_style(self) -> ButtonStyle {
777 match self {
778 Self::Primary => button_style::fab_primary_small,
779 Self::Secondary => button_style::fab_secondary_small,
780 Self::Tertiary => button_style::fab_tertiary_small,
781 Self::Surface => button_style::fab_surface_small,
782 }
783 }
784
785 const fn large_style(self) -> ButtonStyle {
786 match self {
787 Self::Primary => button_style::fab_primary_large,
788 Self::Secondary => button_style::fab_secondary_large,
789 Self::Tertiary => button_style::fab_tertiary_large,
790 Self::Surface => button_style::fab_surface_large,
791 }
792 }
793
794 const fn extended_style(self) -> ButtonStyle {
795 match self {
796 Self::Primary => button_style::extended_fab_primary,
797 Self::Secondary => button_style::extended_fab_secondary,
798 Self::Tertiary => button_style::extended_fab_tertiary,
799 Self::Surface => button_style::extended_fab_surface,
800 }
801 }
802}
803
804#[derive(Debug, Clone, Copy, PartialEq, Eq)]
805pub enum FabSize {
806 Small,
807 Standard,
808 Large,
809}
810
811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
812pub enum ChipVariant {
813 Assist,
814 ElevatedAssist,
815 Suggestion,
816 ElevatedSuggestion,
817 Filter,
818 SelectedFilter,
819 Input,
820 SelectedInput,
821}
822
823impl ChipVariant {
824 const fn style(self) -> ButtonStyle {
825 match self {
826 Self::Assist => button_style::assist_chip,
827 Self::ElevatedAssist => button_style::elevated_assist_chip,
828 Self::Suggestion => button_style::suggestion_chip,
829 Self::ElevatedSuggestion => button_style::elevated_suggestion_chip,
830 Self::Filter => button_style::filter_chip,
831 Self::SelectedFilter => button_style::selected_filter_chip,
832 Self::Input => button_style::input_chip,
833 Self::SelectedInput => button_style::selected_input_chip,
834 }
835 }
836}
837
838fn text_button_content<'a, Message, Renderer>(
839 label: impl text::IntoFragment<'a>,
840 label_size: f32,
841 label_line_height: f32,
842 height: f32,
843 horizontal_padding: f32,
844) -> Container<'a, Message, Theme, Renderer>
845where
846 Message: 'a,
847 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
848{
849 Container::new(text_with_metrics(label, label_size, label_line_height))
850 .height(Length::Fixed(height))
851 .padding(Padding::from([0.0, horizontal_padding]))
852 .align_y(alignment::Vertical::Center)
853}
854
855fn icon_button_content<'a, Message, Renderer>(
856 icon: impl text::IntoFragment<'a>,
857) -> Container<'a, Message, Theme, Renderer>
858where
859 Message: 'a,
860 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
861 iced_widget::core::Font: Into<Renderer::Font>,
862{
863 let icon = centered_icon_text(icon, tokens::component::icon_button::ICON_SIZE);
864
865 Container::new(icon)
866 .center_x(Length::Fixed(
867 tokens::component::icon_button::CONTAINER_WIDTH,
868 ))
869 .center_y(Length::Fixed(
870 tokens::component::icon_button::CONTAINER_HEIGHT,
871 ))
872}
873
874fn fab_content<'a, Message, Renderer>(
875 icon: impl text::IntoFragment<'a>,
876) -> Container<'a, Message, Theme, Renderer>
877where
878 Message: 'a,
879 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
880 iced_widget::core::Font: Into<Renderer::Font>,
881{
882 sized_fab_content(
883 icon,
884 tokens::component::fab::CONTAINER_WIDTH,
885 tokens::component::fab::CONTAINER_HEIGHT,
886 tokens::component::fab::ICON_SIZE,
887 )
888}
889
890fn sized_fab_content<'a, Message, Renderer>(
891 icon: impl text::IntoFragment<'a>,
892 width: f32,
893 height: f32,
894 icon_size: f32,
895) -> Container<'a, Message, Theme, Renderer>
896where
897 Message: 'a,
898 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
899 iced_widget::core::Font: Into<Renderer::Font>,
900{
901 let icon = centered_icon_text(icon, icon_size);
902
903 Container::new(icon)
904 .center_x(Length::Fixed(width))
905 .center_y(Length::Fixed(height))
906}
907
908fn extended_fab_content<'a, Message, Renderer>(
909 label: impl text::IntoFragment<'a>,
910) -> Container<'a, Message, Theme, Renderer>
911where
912 Message: 'a,
913 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
914{
915 let label_text = tokens::component::fab::EXTENDED_LABEL_TEXT;
916
917 Container::new(text_with_metrics(
918 label,
919 label_text.size,
920 label_text.line_height,
921 ))
922 .height(Length::Fixed(
923 tokens::component::fab::EXTENDED_CONTAINER_HEIGHT,
924 ))
925 .padding(Padding {
926 top: 0.0,
927 right: tokens::component::fab::EXTENDED_TRAILING_SPACE,
928 bottom: 0.0,
929 left: tokens::component::fab::EXTENDED_LEADING_SPACE,
930 })
931 .align_y(alignment::Vertical::Center)
932}
933
934fn extended_fab_icon_content<'a, Message, Renderer>(
935 icon: impl text::IntoFragment<'a>,
936 label: impl text::IntoFragment<'a>,
937) -> Container<'a, Message, Theme, Renderer>
938where
939 Message: 'a,
940 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
941 iced_widget::core::Font: Into<Renderer::Font>,
942{
943 let label_text = tokens::component::fab::EXTENDED_LABEL_TEXT;
944 let content = Row::new()
945 .push(centered_icon_text(
946 icon,
947 tokens::component::fab::EXTENDED_ICON_SIZE,
948 ))
949 .push(text_with_metrics(
950 label,
951 label_text.size,
952 label_text.line_height,
953 ))
954 .spacing(tokens::component::fab::EXTENDED_ICON_LABEL_SPACE)
955 .align_y(alignment::Vertical::Center);
956
957 Container::new(content)
958 .height(Length::Fixed(
959 tokens::component::fab::EXTENDED_CONTAINER_HEIGHT,
960 ))
961 .padding(Padding {
962 top: 0.0,
963 right: tokens::component::fab::EXTENDED_TRAILING_SPACE,
964 bottom: 0.0,
965 left: tokens::component::fab::EXTENDED_LEADING_SPACE,
966 })
967 .align_y(alignment::Vertical::Center)
968}
969
970fn standard_button<'a, Message, Renderer>(
971 label: impl text::IntoFragment<'a>,
972 style: ButtonStyle,
973) -> Button<'a, Message, Renderer>
974where
975 Message: Clone + 'a,
976 Renderer: geometry::Renderer + core_text::Renderer + 'a,
977{
978 Button::new(text_button_content(
979 label,
980 tokens::component::button::LABEL_TEXT_SIZE,
981 tokens::component::button::LABEL_TEXT_LINE_HEIGHT,
982 tokens::component::button::CONTAINER_HEIGHT,
983 tokens::component::button::LEADING_SPACE,
984 ))
985 .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
986 .padding(Padding::ZERO)
987 .style(style)
988}
989
990fn chip_button<'a, Message, Renderer>(
991 label: impl text::IntoFragment<'a>,
992 style: ButtonStyle,
993) -> Button<'a, Message, Renderer>
994where
995 Message: Clone + 'a,
996 Renderer: geometry::Renderer + core_text::Renderer + 'a,
997{
998 Button::new(text_button_content(
999 label,
1000 tokens::component::chip::LABEL_TEXT_SIZE,
1001 tokens::component::chip::LABEL_TEXT_LINE_HEIGHT,
1002 tokens::component::chip::CONTAINER_HEIGHT,
1003 tokens::component::chip::LEADING_SPACE,
1004 ))
1005 .height(Length::Fixed(tokens::component::chip::CONTAINER_HEIGHT))
1006 .padding(Padding::ZERO)
1007 .style(style)
1008}
1009
1010fn icon_button_with_style<'a, Message, Renderer>(
1011 icon: impl text::IntoFragment<'a>,
1012 style: ButtonStyle,
1013) -> Button<'a, Message, Renderer>
1014where
1015 Message: Clone + 'a,
1016 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1017 iced_widget::core::Font: Into<Renderer::Font>,
1018{
1019 Button::new(icon_button_content(icon))
1020 .width(Length::Fixed(
1021 tokens::component::icon_button::CONTAINER_WIDTH,
1022 ))
1023 .height(Length::Fixed(
1024 tokens::component::icon_button::CONTAINER_HEIGHT,
1025 ))
1026 .padding(Padding::ZERO)
1027 .style(style)
1028}
1029
1030fn sized_fab<'a, Message, Renderer>(
1031 icon_content: impl text::IntoFragment<'a>,
1032 width: f32,
1033 height: f32,
1034 icon_size: f32,
1035 style: ButtonStyle,
1036) -> Button<'a, Message, Renderer>
1037where
1038 Message: Clone + 'a,
1039 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1040 iced_widget::core::Font: Into<Renderer::Font>,
1041{
1042 Button::new(sized_fab_content(icon_content, width, height, icon_size))
1043 .width(Length::Fixed(width))
1044 .height(Length::Fixed(height))
1045 .padding(Padding::ZERO)
1046 .style(style)
1047}
1048
1049fn standard_fab<'a, Message, Renderer>(
1050 icon_content: impl text::IntoFragment<'a>,
1051 style: ButtonStyle,
1052) -> Button<'a, Message, Renderer>
1053where
1054 Message: Clone + 'a,
1055 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1056 iced_widget::core::Font: Into<Renderer::Font>,
1057{
1058 Button::new(fab_content(icon_content))
1059 .width(Length::Fixed(tokens::component::fab::CONTAINER_WIDTH))
1060 .height(Length::Fixed(tokens::component::fab::CONTAINER_HEIGHT))
1061 .padding(Padding::ZERO)
1062 .style(style)
1063}
1064
1065fn small_fab<'a, Message, Renderer>(
1066 icon_content: impl text::IntoFragment<'a>,
1067 style: ButtonStyle,
1068) -> Button<'a, Message, Renderer>
1069where
1070 Message: Clone + 'a,
1071 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1072 iced_widget::core::Font: Into<Renderer::Font>,
1073{
1074 sized_fab(
1075 icon_content,
1076 tokens::component::fab::SMALL_CONTAINER_WIDTH,
1077 tokens::component::fab::SMALL_CONTAINER_HEIGHT,
1078 tokens::component::fab::SMALL_ICON_SIZE,
1079 style,
1080 )
1081}
1082
1083fn large_fab<'a, Message, Renderer>(
1084 icon_content: impl text::IntoFragment<'a>,
1085 style: ButtonStyle,
1086) -> Button<'a, Message, Renderer>
1087where
1088 Message: Clone + 'a,
1089 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1090 iced_widget::core::Font: Into<Renderer::Font>,
1091{
1092 sized_fab(
1093 icon_content,
1094 tokens::component::fab::LARGE_CONTAINER_WIDTH,
1095 tokens::component::fab::LARGE_CONTAINER_HEIGHT,
1096 tokens::component::fab::LARGE_ICON_SIZE,
1097 style,
1098 )
1099}
1100
1101fn extended_fab_button<'a, Message, Renderer>(
1102 label: impl text::IntoFragment<'a>,
1103 style: ButtonStyle,
1104) -> Button<'a, Message, Renderer>
1105where
1106 Message: Clone + 'a,
1107 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1108{
1109 Button::new(extended_fab_content(label))
1110 .height(Length::Fixed(
1111 tokens::component::fab::EXTENDED_CONTAINER_HEIGHT,
1112 ))
1113 .padding(Padding::ZERO)
1114 .style(style)
1115}
1116
1117fn extended_fab_button_with_icon<'a, Message, Renderer>(
1118 icon_content: impl text::IntoFragment<'a>,
1119 label: impl text::IntoFragment<'a>,
1120 style: ButtonStyle,
1121) -> Button<'a, Message, Renderer>
1122where
1123 Message: Clone + 'a,
1124 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1125 iced_widget::core::Font: Into<Renderer::Font>,
1126{
1127 Button::new(extended_fab_icon_content(icon_content, label))
1128 .height(Length::Fixed(
1129 tokens::component::fab::EXTENDED_CONTAINER_HEIGHT,
1130 ))
1131 .padding(Padding::ZERO)
1132 .style(style)
1133}
1134
1135pub fn button<'a, Message, Renderer>(
1136 label: impl text::IntoFragment<'a>,
1137 variant: ButtonVariant,
1138) -> Button<'a, Message, Renderer>
1139where
1140 Message: Clone + 'a,
1141 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1142{
1143 standard_button(label, variant.style())
1144}
1145
1146pub fn action<'a, Message, Renderer>(
1147 button: Button<'a, Message, Renderer>,
1148 on_press: Message,
1149) -> Element<'a, Message, Theme, Renderer>
1150where
1151 Message: Clone + 'a,
1152 Renderer: geometry::Renderer + primitive::Renderer + 'a,
1153{
1154 button.on_press(on_press).into()
1155}
1156
1157pub fn optional_action<'a, Message, Renderer>(
1158 button: Button<'a, Message, Renderer>,
1159 on_press: Option<Message>,
1160) -> Element<'a, Message, Theme, Renderer>
1161where
1162 Message: Clone + 'a,
1163 Renderer: geometry::Renderer + primitive::Renderer + 'a,
1164{
1165 button.on_press_maybe(on_press).into()
1166}
1167
1168pub fn enabled_actions<'a, Message, Renderer>(
1169 enabled: bool,
1170 on_press: Message,
1171 buttons: impl IntoIterator<Item = Button<'a, Message, Renderer>>,
1172) -> Vec<Element<'a, Message, Theme, Renderer>>
1173where
1174 Message: Clone + 'a,
1175 Renderer: geometry::Renderer + primitive::Renderer + 'a,
1176{
1177 buttons
1178 .into_iter()
1179 .map(|button| optional_action(button, enabled.then_some(on_press.clone())))
1180 .collect()
1181}
1182
1183pub fn icon_button<'a, Message, Renderer>(
1184 icon_content: impl text::IntoFragment<'a>,
1185 variant: IconButtonVariant,
1186) -> Button<'a, Message, Renderer>
1187where
1188 Message: Clone + 'a,
1189 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1190 iced_widget::core::Font: Into<Renderer::Font>,
1191{
1192 icon_button_with_style(icon_content, variant.style())
1193}
1194
1195pub fn fab<'a, Message, Renderer>(
1196 icon_content: impl text::IntoFragment<'a>,
1197 variant: FabVariant,
1198 size: FabSize,
1199) -> Button<'a, Message, Renderer>
1200where
1201 Message: Clone + 'a,
1202 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1203 iced_widget::core::Font: Into<Renderer::Font>,
1204{
1205 match size {
1206 FabSize::Small => small_fab(icon_content, variant.small_style()),
1207 FabSize::Standard => standard_fab(icon_content, variant.standard_style()),
1208 FabSize::Large => large_fab(icon_content, variant.large_style()),
1209 }
1210}
1211
1212pub fn extended_fab<'a, Message, Renderer>(
1213 label: impl text::IntoFragment<'a>,
1214 variant: FabVariant,
1215) -> Button<'a, Message, Renderer>
1216where
1217 Message: Clone + 'a,
1218 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1219{
1220 extended_fab_button(label, variant.extended_style())
1221}
1222
1223pub fn extended_fab_with_icon<'a, Message, Renderer>(
1224 icon_content: impl text::IntoFragment<'a>,
1225 label: impl text::IntoFragment<'a>,
1226 variant: FabVariant,
1227) -> Button<'a, Message, Renderer>
1228where
1229 Message: Clone + 'a,
1230 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1231 iced_widget::core::Font: Into<Renderer::Font>,
1232{
1233 extended_fab_button_with_icon(icon_content, label, variant.extended_style())
1234}
1235
1236pub fn chip<'a, Message, Renderer>(
1237 label: impl text::IntoFragment<'a>,
1238 variant: ChipVariant,
1239) -> Button<'a, Message, Renderer>
1240where
1241 Message: Clone + 'a,
1242 Renderer: geometry::Renderer + core_text::Renderer + 'a,
1243{
1244 chip_button(label, variant.style())
1245}