Skip to main content

material_ui_rs/widget/component/
button.rs

1//! Material 3 button constructors with token-backed layout defaults.
2
3use 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
22/// A Material 3 button with Android-style bounded press ripples.
23pub 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    /// Creates a new [`Button`] with the given content.
72    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    /// Sets the width of the [`Button`].
89    pub fn width(mut self, width: impl Into<Length>) -> Self {
90        self.width = width.into();
91        self
92    }
93
94    /// Sets the height of the [`Button`].
95    pub fn height(mut self, height: impl Into<Length>) -> Self {
96        self.height = height.into();
97        self
98    }
99
100    /// Sets the [`Padding`] of the [`Button`].
101    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
102        self.padding = padding.into();
103        self
104    }
105
106    /// Sets the message produced when the [`Button`] is pressed.
107    pub fn on_press(mut self, on_press: Message) -> Self {
108        self.on_press = Some(OnPress::Direct(on_press));
109        self
110    }
111
112    /// Sets the message produced when the [`Button`] is pressed using a closure.
113    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    /// Sets the message produced when the [`Button`] is pressed, if any.
119    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    /// Sets whether the button content should be clipped on overflow.
125    pub fn clip(mut self, clip: bool) -> Self {
126        self.clip = clip;
127        self
128    }
129
130    /// Sets the style of the [`Button`].
131    #[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(button_hover_state_layer_target(self.is_hovered));
202    }
203
204    fn animate_state_layer(&mut self, now: Instant) {
205        self.state_layer_opacity.set_target(
206            button_hover_state_layer_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 should_snap_initial_redraw_hover =
333            button_should_snap_initial_redraw_hover(event, state, is_hovered);
334
335        if button_should_sync_hover(event, cursor) {
336            if state.sync_hover(is_hovered, now_or_current()) {
337                if should_snap_initial_redraw_hover {
338                    state.snap_state_layer_to_hover_target();
339                }
340
341                shell.request_redraw();
342            }
343        }
344
345        match event {
346            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
347            | Event::Touch(touch::Event::FingerPressed { .. }) => {
348                if self.on_press.is_some() {
349                    if let Some(origin) = press_origin(event, bounds, cursor) {
350                        state.press(origin, now_or_current());
351                        state.touch_press_position = touch_position(event, cursor);
352                        shell.capture_event();
353                        shell.request_redraw();
354                    }
355                }
356            }
357            Event::Touch(touch::Event::FingerMoved { .. }) => {
358                if state.is_pressed
359                    && touch_moved_beyond_click_slop(state.touch_press_position, event, cursor)
360                {
361                    state.cancel(now_or_current());
362                    shell.request_redraw();
363                }
364            }
365            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
366            | Event::Touch(touch::Event::FingerLifted { .. }) => {
367                if state.is_pressed {
368                    state.release(now_or_current());
369
370                    if release_is_over(event, bounds, cursor) {
371                        if let Some(on_press) = &self.on_press {
372                            shell.publish(on_press.get());
373                        }
374                    }
375
376                    shell.capture_event();
377                    shell.request_redraw();
378                }
379            }
380            Event::Touch(touch::Event::FingerLost { .. }) => {
381                if state.is_pressed {
382                    state.cancel(now_or_current());
383                    shell.request_redraw();
384                }
385            }
386            _ => {}
387        }
388
389        let current_status =
390            button_status(self.on_press.is_some(), state.is_pressed, bounds, cursor);
391
392        if let Some(now) = now {
393            if state.advance(now) {
394                shell.request_redraw();
395            }
396
397            self.status = Some(current_status);
398            state.last_status = Some(current_status);
399        } else if self.status.is_some_and(|status| status != current_status)
400            || state.state_layer_opacity.is_animating()
401            || state.has_visible_ripples(state.now.unwrap_or_else(Instant::now))
402        {
403            shell.request_redraw();
404        }
405    }
406
407    fn draw(
408        &self,
409        tree: &Tree,
410        renderer: &mut Renderer,
411        theme: &Theme,
412        _style: &renderer::Style,
413        layout: Layout<'_>,
414        cursor: mouse::Cursor,
415        viewport: &Rectangle,
416    ) {
417        let bounds = layout.bounds();
418
419        if bounds.width < 1.0 || bounds.height < 1.0 {
420            return;
421        }
422
423        let state = tree.state.downcast_ref::<ButtonState>();
424        let status = button_status(self.on_press.is_some(), state.is_pressed, bounds, cursor);
425        let now = state.now.unwrap_or_else(Instant::now);
426        let style = button_draw_style(theme, &self.class, status);
427        let content_layout = layout.children().next().unwrap();
428
429        if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
430            renderer.fill_quad(
431                renderer::Quad {
432                    bounds,
433                    border: style.border,
434                    shadow: style.shadow,
435                    snap: style.snap,
436                },
437                style
438                    .background
439                    .unwrap_or(Background::Color(Color::TRANSPARENT)),
440            );
441        }
442
443        let viewport = if self.clip {
444            bounds.intersection(viewport).unwrap_or(*viewport)
445        } else {
446            *viewport
447        };
448
449        self.content.as_widget().draw(
450            &tree.children[0],
451            renderer,
452            theme,
453            &renderer::Style {
454                text_color: style.text_color,
455            },
456            content_layout,
457            cursor,
458            &viewport,
459        );
460
461        draw_button_state_layer(
462            renderer,
463            bounds,
464            &style,
465            button_state_layer_opacity_for_draw(state, status),
466        );
467
468        draw_ripples(
469            renderer,
470            bounds,
471            &state.ripples,
472            style.text_color,
473            RippleConfig::bounded(style.border.radius),
474            now,
475        );
476    }
477
478    fn mouse_interaction(
479        &self,
480        _tree: &Tree,
481        layout: Layout<'_>,
482        cursor: mouse::Cursor,
483        _viewport: &Rectangle,
484        _renderer: &Renderer,
485    ) -> mouse::Interaction {
486        if cursor.is_over(layout.bounds()) && self.on_press.is_some() {
487            mouse::Interaction::Pointer
488        } else {
489            mouse::Interaction::default()
490        }
491    }
492
493    fn overlay<'b>(
494        &'b mut self,
495        tree: &'b mut Tree,
496        layout: Layout<'b>,
497        renderer: &Renderer,
498        viewport: &Rectangle,
499        translation: iced_widget::core::Vector,
500    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
501        self.content.as_widget_mut().overlay(
502            &mut tree.children[0],
503            layout.children().next().unwrap(),
504            renderer,
505            viewport,
506            translation,
507        )
508    }
509}
510
511impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
512    for Element<'a, Message, Theme, Renderer>
513where
514    Message: Clone + 'a,
515    Renderer: geometry::Renderer + primitive::Renderer + 'a,
516{
517    fn from(button: Button<'a, Message, Renderer>) -> Self {
518        Element::new(button)
519    }
520}
521
522fn button_hover_state_layer_target(is_hovered: bool) -> f32 {
523    if is_hovered {
524        tokens::state::HOVER_STATE_LAYER_OPACITY
525    } else {
526        0.0
527    }
528}
529
530fn button_should_sync_hover(event: &Event, cursor: mouse::Cursor) -> bool {
531    match event {
532        Event::Window(window::Event::RedrawRequested(_)) => {
533            !matches!(cursor, mouse::Cursor::Unavailable)
534        }
535        Event::Mouse(_) | Event::Touch(_) => true,
536        _ => false,
537    }
538}
539
540fn button_should_snap_initial_redraw_hover(
541    event: &Event,
542    state: &ButtonState,
543    is_hovered: bool,
544) -> bool {
545    matches!(event, Event::Window(window::Event::RedrawRequested(_)))
546        && state.last_status.is_none()
547        && is_hovered
548}
549
550fn button_state_layer_opacity_for_draw(state: &ButtonState, status: Status) -> f32 {
551    if matches!(status, Status::Hovered) && state.last_status.is_none() {
552        button_hover_state_layer_target(true)
553    } else {
554        state.state_layer_opacity()
555    }
556}
557
558fn button_status(
559    is_enabled: bool,
560    is_pressed: bool,
561    bounds: Rectangle,
562    cursor: mouse::Cursor,
563) -> Status {
564    if !is_enabled {
565        Status::Disabled
566    } else if cursor.is_over(bounds) {
567        if is_pressed {
568            Status::Pressed
569        } else {
570            Status::Hovered
571        }
572    } else {
573        Status::Active
574    }
575}
576
577fn button_draw_style(
578    theme: &Theme,
579    class: &<Theme as Catalog>::Class<'_>,
580    status: Status,
581) -> Style {
582    if matches!(status, Status::Pressed | Status::Hovered) {
583        return theme.style(class, Status::Active);
584    }
585
586    theme.style(class, status)
587}
588
589fn draw_button_state_layer<Renderer>(
590    renderer: &mut Renderer,
591    bounds: Rectangle,
592    style: &Style,
593    opacity: f32,
594) where
595    Renderer: geometry::Renderer,
596{
597    if opacity <= 0.0 {
598        return;
599    }
600
601    renderer.fill_quad(
602        renderer::Quad {
603            bounds,
604            border: Border {
605                radius: style.border.radius,
606                ..Border::default()
607            },
608            snap: style.snap,
609            ..renderer::Quad::default()
610        },
611        state_layer(style.text_color, opacity),
612    );
613}
614
615fn press_origin(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> Option<Point> {
616    if cursor.position().is_some() {
617        return cursor.position_in(bounds);
618    }
619
620    if cursor.is_levitating() {
621        return None;
622    }
623
624    match event {
625        Event::Touch(touch::Event::FingerPressed { position, .. }) => {
626            relative_position(*position, bounds)
627        }
628        _ => cursor.position_in(bounds),
629    }
630}
631
632fn release_is_over(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> bool {
633    if cursor.position().is_some() {
634        return cursor.is_over(bounds);
635    }
636
637    if cursor.is_levitating() {
638        return false;
639    }
640
641    match event {
642        Event::Touch(touch::Event::FingerLifted { position, .. }) => bounds.contains(*position),
643        _ => cursor.is_over(bounds),
644    }
645}
646
647fn touch_position(event: &Event, cursor: mouse::Cursor) -> Option<Point> {
648    if cursor.position().is_some() {
649        return cursor.position();
650    }
651
652    if cursor.is_levitating() {
653        return None;
654    }
655
656    match event {
657        Event::Touch(
658            touch::Event::FingerPressed { position, .. }
659            | touch::Event::FingerMoved { position, .. }
660            | touch::Event::FingerLifted { position, .. }
661            | touch::Event::FingerLost { position, .. },
662        ) => Some(*position),
663        _ => None,
664    }
665}
666
667fn touch_moved_beyond_click_slop(
668    press_position: Option<Point>,
669    event: &Event,
670    cursor: mouse::Cursor,
671) -> bool {
672    let Some(press_position) = press_position else {
673        return false;
674    };
675    let Some(position) = touch_position(event, cursor) else {
676        return false;
677    };
678    let dx = position.x - press_position.x;
679    let dy = position.y - press_position.y;
680
681    dx * dx + dy * dy > TOUCH_CLICK_SLOP * TOUCH_CLICK_SLOP
682}
683
684fn relative_position(position: Point, bounds: Rectangle) -> Option<Point> {
685    bounds
686        .contains(position)
687        .then(|| position - iced_widget::core::Vector::new(bounds.x, bounds.y))
688}
689
690#[cfg(test)]
691#[path = "../../../tests/widget/component/button.rs"]
692mod tests;
693
694fn standard<'a, Message, Renderer>(
695    label: impl text::IntoFragment<'a>,
696    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
697) -> Button<'a, Message, Renderer>
698where
699    Message: Clone + 'a,
700    Renderer: geometry::Renderer + core_text::Renderer + 'a,
701{
702    Button::new(text_button_content(
703        label,
704        tokens::component::button::LABEL_TEXT_SIZE,
705        tokens::component::button::LABEL_TEXT_LINE_HEIGHT,
706        tokens::component::button::CONTAINER_HEIGHT,
707        tokens::component::button::LEADING_SPACE,
708    ))
709    .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
710    .padding(Padding::ZERO)
711    .style(style)
712}
713
714fn chip<'a, Message, Renderer>(
715    label: impl text::IntoFragment<'a>,
716    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
717) -> Button<'a, Message, Renderer>
718where
719    Message: Clone + 'a,
720    Renderer: geometry::Renderer + core_text::Renderer + 'a,
721{
722    Button::new(text_button_content(
723        label,
724        tokens::component::chip::LABEL_TEXT_SIZE,
725        tokens::component::chip::LABEL_TEXT_LINE_HEIGHT,
726        tokens::component::chip::CONTAINER_HEIGHT,
727        tokens::component::chip::LEADING_SPACE,
728    ))
729    .height(Length::Fixed(tokens::component::chip::CONTAINER_HEIGHT))
730    .padding(Padding::ZERO)
731    .style(style)
732}
733
734fn icon<'a, Message, Renderer>(
735    icon: impl text::IntoFragment<'a>,
736    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
737) -> Button<'a, Message, Renderer>
738where
739    Message: Clone + 'a,
740    Renderer: geometry::Renderer + core_text::Renderer + 'a,
741    iced_widget::core::Font: Into<Renderer::Font>,
742{
743    Button::new(icon_button_content(icon))
744        .width(Length::Fixed(
745            tokens::component::icon_button::CONTAINER_WIDTH,
746        ))
747        .height(Length::Fixed(
748            tokens::component::icon_button::CONTAINER_HEIGHT,
749        ))
750        .padding(Padding::ZERO)
751        .style(style)
752}
753
754fn sized_fab<'a, Message, Renderer>(
755    icon_content: impl text::IntoFragment<'a>,
756    width: f32,
757    height: f32,
758    icon_size: f32,
759    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
760) -> Button<'a, Message, Renderer>
761where
762    Message: Clone + 'a,
763    Renderer: geometry::Renderer + core_text::Renderer + 'a,
764    iced_widget::core::Font: Into<Renderer::Font>,
765{
766    Button::new(sized_fab_content(icon_content, width, height, icon_size))
767        .width(Length::Fixed(width))
768        .height(Length::Fixed(height))
769        .padding(Padding::ZERO)
770        .style(style)
771}
772
773fn fab<'a, Message, Renderer>(
774    icon_content: impl text::IntoFragment<'a>,
775    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
776) -> Button<'a, Message, Renderer>
777where
778    Message: Clone + 'a,
779    Renderer: geometry::Renderer + core_text::Renderer + 'a,
780    iced_widget::core::Font: Into<Renderer::Font>,
781{
782    Button::new(fab_content(icon_content))
783        .width(Length::Fixed(tokens::component::fab::CONTAINER_WIDTH))
784        .height(Length::Fixed(tokens::component::fab::CONTAINER_HEIGHT))
785        .padding(Padding::ZERO)
786        .style(style)
787}
788
789fn small_fab<'a, Message, Renderer>(
790    icon_content: impl text::IntoFragment<'a>,
791    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
792) -> Button<'a, Message, Renderer>
793where
794    Message: Clone + 'a,
795    Renderer: geometry::Renderer + core_text::Renderer + 'a,
796    iced_widget::core::Font: Into<Renderer::Font>,
797{
798    sized_fab(
799        icon_content,
800        tokens::component::fab::SMALL_CONTAINER_WIDTH,
801        tokens::component::fab::SMALL_CONTAINER_HEIGHT,
802        tokens::component::fab::SMALL_ICON_SIZE,
803        style,
804    )
805}
806
807fn large_fab<'a, Message, Renderer>(
808    icon_content: impl text::IntoFragment<'a>,
809    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
810) -> Button<'a, Message, Renderer>
811where
812    Message: Clone + 'a,
813    Renderer: geometry::Renderer + core_text::Renderer + 'a,
814    iced_widget::core::Font: Into<Renderer::Font>,
815{
816    sized_fab(
817        icon_content,
818        tokens::component::fab::LARGE_CONTAINER_WIDTH,
819        tokens::component::fab::LARGE_CONTAINER_HEIGHT,
820        tokens::component::fab::LARGE_ICON_SIZE,
821        style,
822    )
823}
824
825fn extended_fab<'a, Message, Renderer>(
826    label: impl text::IntoFragment<'a>,
827    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
828) -> Button<'a, Message, Renderer>
829where
830    Message: Clone + 'a,
831    Renderer: geometry::Renderer + core_text::Renderer + 'a,
832{
833    Button::new(extended_fab_content(label))
834        .height(Length::Fixed(
835            tokens::component::fab::EXTENDED_CONTAINER_HEIGHT,
836        ))
837        .padding(Padding::ZERO)
838        .style(style)
839}
840
841fn extended_fab_with_icon<'a, Message, Renderer>(
842    icon_content: impl text::IntoFragment<'a>,
843    label: impl text::IntoFragment<'a>,
844    style: fn(&Theme, iced_widget::button::Status) -> iced_widget::button::Style,
845) -> Button<'a, Message, Renderer>
846where
847    Message: Clone + 'a,
848    Renderer: geometry::Renderer + core_text::Renderer + 'a,
849    iced_widget::core::Font: Into<Renderer::Font>,
850{
851    Button::new(extended_fab_icon_content(icon_content, label))
852        .height(Length::Fixed(
853            tokens::component::fab::EXTENDED_CONTAINER_HEIGHT,
854        ))
855        .padding(Padding::ZERO)
856        .style(style)
857}
858
859pub fn elevated<'a, Message, Renderer>(
860    label: impl text::IntoFragment<'a>,
861) -> Button<'a, Message, Renderer>
862where
863    Message: Clone + 'a,
864    Renderer: geometry::Renderer + core_text::Renderer + 'a,
865{
866    standard(label, button_style::elevated)
867}
868
869pub fn filled<'a, Message, Renderer>(
870    label: impl text::IntoFragment<'a>,
871) -> Button<'a, Message, Renderer>
872where
873    Message: Clone + 'a,
874    Renderer: geometry::Renderer + core_text::Renderer + 'a,
875{
876    standard(label, button_style::filled)
877}
878
879pub fn filled_action<'a, Message, Renderer>(
880    label: impl text::IntoFragment<'a>,
881    on_press: Message,
882) -> Element<'a, Message, Theme, Renderer>
883where
884    Message: Clone + 'a,
885    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
886{
887    filled(label).on_press(on_press).into()
888}
889
890/// Converts a Material button into an element with an optional action.
891pub fn maybe_action<'a, Message, Renderer>(
892    button: Button<'a, Message, Renderer>,
893    enabled: bool,
894    on_press: Message,
895) -> Element<'a, Message, Theme, Renderer>
896where
897    Message: Clone + 'a,
898    Renderer: geometry::Renderer + primitive::Renderer + 'a,
899{
900    button.on_press_maybe(enabled.then_some(on_press)).into()
901}
902
903/// Converts a group of Material buttons into elements sharing an enabled action.
904pub fn enabled_actions<'a, Message, Renderer>(
905    enabled: bool,
906    on_press: Message,
907    buttons: impl IntoIterator<Item = Button<'a, Message, Renderer>>,
908) -> Vec<Element<'a, Message, Theme, Renderer>>
909where
910    Message: Clone + 'a,
911    Renderer: geometry::Renderer + primitive::Renderer + 'a,
912{
913    buttons
914        .into_iter()
915        .map(|button| maybe_action(button, enabled, on_press.clone()))
916        .collect()
917}
918
919pub fn filled_tonal<'a, Message, Renderer>(
920    label: impl text::IntoFragment<'a>,
921) -> Button<'a, Message, Renderer>
922where
923    Message: Clone + 'a,
924    Renderer: geometry::Renderer + core_text::Renderer + 'a,
925{
926    standard(label, button_style::filled_tonal)
927}
928
929pub fn outlined<'a, Message, Renderer>(
930    label: impl text::IntoFragment<'a>,
931) -> Button<'a, Message, Renderer>
932where
933    Message: Clone + 'a,
934    Renderer: geometry::Renderer + core_text::Renderer + 'a,
935{
936    standard(label, button_style::outlined)
937}
938
939pub fn outlined_action<'a, Message, Renderer>(
940    label: impl text::IntoFragment<'a>,
941    on_press: Message,
942) -> Element<'a, Message, Theme, Renderer>
943where
944    Message: Clone + 'a,
945    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
946{
947    outlined(label).on_press(on_press).into()
948}
949
950pub fn text<'a, Message, Renderer>(
951    label: impl text::IntoFragment<'a>,
952) -> Button<'a, Message, Renderer>
953where
954    Message: Clone + 'a,
955    Renderer: geometry::Renderer + core_text::Renderer + 'a,
956{
957    standard(label, button_style::text)
958}
959
960pub fn text_action<'a, Message, Renderer>(
961    label: impl text::IntoFragment<'a>,
962    on_press: Message,
963) -> Element<'a, Message, Theme, Renderer>
964where
965    Message: Clone + 'a,
966    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
967{
968    text(label).on_press(on_press).into()
969}
970
971pub fn icon_button<'a, Message, Renderer>(
972    icon_content: impl text::IntoFragment<'a>,
973) -> Button<'a, Message, Renderer>
974where
975    Message: Clone + 'a,
976    Renderer: geometry::Renderer + core_text::Renderer + 'a,
977    iced_widget::core::Font: Into<Renderer::Font>,
978{
979    icon(icon_content, button_style::icon)
980}
981
982pub fn filled_icon<'a, Message, Renderer>(
983    icon_content: impl text::IntoFragment<'a>,
984) -> Button<'a, Message, Renderer>
985where
986    Message: Clone + 'a,
987    Renderer: geometry::Renderer + core_text::Renderer + 'a,
988    iced_widget::core::Font: Into<Renderer::Font>,
989{
990    icon(icon_content, button_style::filled_icon)
991}
992
993pub fn filled_tonal_icon<'a, Message, Renderer>(
994    icon_content: impl text::IntoFragment<'a>,
995) -> Button<'a, Message, Renderer>
996where
997    Message: Clone + 'a,
998    Renderer: geometry::Renderer + core_text::Renderer + 'a,
999    iced_widget::core::Font: Into<Renderer::Font>,
1000{
1001    icon(icon_content, button_style::filled_tonal_icon)
1002}
1003
1004pub fn outlined_icon<'a, Message, Renderer>(
1005    icon_content: impl text::IntoFragment<'a>,
1006) -> Button<'a, Message, Renderer>
1007where
1008    Message: Clone + 'a,
1009    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1010    iced_widget::core::Font: Into<Renderer::Font>,
1011{
1012    icon(icon_content, button_style::outlined_icon)
1013}
1014
1015pub fn primary_fab<'a, Message, Renderer>(
1016    icon_content: impl text::IntoFragment<'a>,
1017) -> Button<'a, Message, Renderer>
1018where
1019    Message: Clone + 'a,
1020    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1021    iced_widget::core::Font: Into<Renderer::Font>,
1022{
1023    fab(icon_content, button_style::fab_primary)
1024}
1025
1026pub fn primary_fab_action<'a, Message, Renderer>(
1027    icon_content: impl text::IntoFragment<'a>,
1028    on_press: Message,
1029) -> Element<'a, Message, Theme, Renderer>
1030where
1031    Message: Clone + 'a,
1032    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1033    iced_widget::core::Font: Into<Renderer::Font>,
1034{
1035    primary_fab(icon_content).on_press(on_press).into()
1036}
1037
1038pub fn primary_small_fab<'a, Message, Renderer>(
1039    icon_content: impl text::IntoFragment<'a>,
1040) -> Button<'a, Message, Renderer>
1041where
1042    Message: Clone + 'a,
1043    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1044    iced_widget::core::Font: Into<Renderer::Font>,
1045{
1046    small_fab(icon_content, button_style::fab_primary_small)
1047}
1048
1049pub fn primary_large_fab<'a, Message, Renderer>(
1050    icon_content: impl text::IntoFragment<'a>,
1051) -> Button<'a, Message, Renderer>
1052where
1053    Message: Clone + 'a,
1054    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1055    iced_widget::core::Font: Into<Renderer::Font>,
1056{
1057    large_fab(icon_content, button_style::fab_primary_large)
1058}
1059
1060pub fn secondary_fab<'a, Message, Renderer>(
1061    icon_content: impl text::IntoFragment<'a>,
1062) -> Button<'a, Message, Renderer>
1063where
1064    Message: Clone + 'a,
1065    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1066    iced_widget::core::Font: Into<Renderer::Font>,
1067{
1068    fab(icon_content, button_style::fab_secondary)
1069}
1070
1071pub fn secondary_small_fab<'a, Message, Renderer>(
1072    icon_content: impl text::IntoFragment<'a>,
1073) -> Button<'a, Message, Renderer>
1074where
1075    Message: Clone + 'a,
1076    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1077    iced_widget::core::Font: Into<Renderer::Font>,
1078{
1079    small_fab(icon_content, button_style::fab_secondary_small)
1080}
1081
1082pub fn secondary_large_fab<'a, Message, Renderer>(
1083    icon_content: impl text::IntoFragment<'a>,
1084) -> Button<'a, Message, Renderer>
1085where
1086    Message: Clone + 'a,
1087    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1088    iced_widget::core::Font: Into<Renderer::Font>,
1089{
1090    large_fab(icon_content, button_style::fab_secondary_large)
1091}
1092
1093pub fn tertiary_fab<'a, Message, Renderer>(
1094    icon_content: impl text::IntoFragment<'a>,
1095) -> Button<'a, Message, Renderer>
1096where
1097    Message: Clone + 'a,
1098    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1099    iced_widget::core::Font: Into<Renderer::Font>,
1100{
1101    fab(icon_content, button_style::fab_tertiary)
1102}
1103
1104pub fn tertiary_small_fab<'a, Message, Renderer>(
1105    icon_content: impl text::IntoFragment<'a>,
1106) -> Button<'a, Message, Renderer>
1107where
1108    Message: Clone + 'a,
1109    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1110    iced_widget::core::Font: Into<Renderer::Font>,
1111{
1112    small_fab(icon_content, button_style::fab_tertiary_small)
1113}
1114
1115pub fn tertiary_large_fab<'a, Message, Renderer>(
1116    icon_content: impl text::IntoFragment<'a>,
1117) -> Button<'a, Message, Renderer>
1118where
1119    Message: Clone + 'a,
1120    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1121    iced_widget::core::Font: Into<Renderer::Font>,
1122{
1123    large_fab(icon_content, button_style::fab_tertiary_large)
1124}
1125
1126pub fn surface_fab<'a, Message, Renderer>(
1127    icon_content: impl text::IntoFragment<'a>,
1128) -> Button<'a, Message, Renderer>
1129where
1130    Message: Clone + 'a,
1131    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1132    iced_widget::core::Font: Into<Renderer::Font>,
1133{
1134    fab(icon_content, button_style::fab_surface)
1135}
1136
1137pub fn surface_small_fab<'a, Message, Renderer>(
1138    icon_content: impl text::IntoFragment<'a>,
1139) -> Button<'a, Message, Renderer>
1140where
1141    Message: Clone + 'a,
1142    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1143    iced_widget::core::Font: Into<Renderer::Font>,
1144{
1145    small_fab(icon_content, button_style::fab_surface_small)
1146}
1147
1148pub fn surface_large_fab<'a, Message, Renderer>(
1149    icon_content: impl text::IntoFragment<'a>,
1150) -> Button<'a, Message, Renderer>
1151where
1152    Message: Clone + 'a,
1153    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1154    iced_widget::core::Font: Into<Renderer::Font>,
1155{
1156    large_fab(icon_content, button_style::fab_surface_large)
1157}
1158
1159pub fn primary_extended_fab<'a, Message, Renderer>(
1160    label: impl text::IntoFragment<'a>,
1161) -> Button<'a, Message, Renderer>
1162where
1163    Message: Clone + 'a,
1164    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1165{
1166    extended_fab(label, button_style::extended_fab_primary)
1167}
1168
1169pub fn primary_extended_fab_with_icon<'a, Message, Renderer>(
1170    icon_content: impl text::IntoFragment<'a>,
1171    label: impl text::IntoFragment<'a>,
1172) -> Button<'a, Message, Renderer>
1173where
1174    Message: Clone + 'a,
1175    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1176    iced_widget::core::Font: Into<Renderer::Font>,
1177{
1178    extended_fab_with_icon(icon_content, label, button_style::extended_fab_primary)
1179}
1180
1181pub fn secondary_extended_fab<'a, Message, Renderer>(
1182    label: impl text::IntoFragment<'a>,
1183) -> Button<'a, Message, Renderer>
1184where
1185    Message: Clone + 'a,
1186    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1187{
1188    extended_fab(label, button_style::extended_fab_secondary)
1189}
1190
1191pub fn secondary_extended_fab_with_icon<'a, Message, Renderer>(
1192    icon_content: impl text::IntoFragment<'a>,
1193    label: impl text::IntoFragment<'a>,
1194) -> Button<'a, Message, Renderer>
1195where
1196    Message: Clone + 'a,
1197    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1198    iced_widget::core::Font: Into<Renderer::Font>,
1199{
1200    extended_fab_with_icon(icon_content, label, button_style::extended_fab_secondary)
1201}
1202
1203pub fn tertiary_extended_fab<'a, Message, Renderer>(
1204    label: impl text::IntoFragment<'a>,
1205) -> Button<'a, Message, Renderer>
1206where
1207    Message: Clone + 'a,
1208    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1209{
1210    extended_fab(label, button_style::extended_fab_tertiary)
1211}
1212
1213pub fn tertiary_extended_fab_with_icon<'a, Message, Renderer>(
1214    icon_content: impl text::IntoFragment<'a>,
1215    label: impl text::IntoFragment<'a>,
1216) -> Button<'a, Message, Renderer>
1217where
1218    Message: Clone + 'a,
1219    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1220    iced_widget::core::Font: Into<Renderer::Font>,
1221{
1222    extended_fab_with_icon(icon_content, label, button_style::extended_fab_tertiary)
1223}
1224
1225pub fn surface_extended_fab<'a, Message, Renderer>(
1226    label: impl text::IntoFragment<'a>,
1227) -> Button<'a, Message, Renderer>
1228where
1229    Message: Clone + 'a,
1230    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1231{
1232    extended_fab(label, button_style::extended_fab_surface)
1233}
1234
1235pub fn surface_extended_fab_with_icon<'a, Message, Renderer>(
1236    icon_content: impl text::IntoFragment<'a>,
1237    label: impl text::IntoFragment<'a>,
1238) -> Button<'a, Message, Renderer>
1239where
1240    Message: Clone + 'a,
1241    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1242    iced_widget::core::Font: Into<Renderer::Font>,
1243{
1244    extended_fab_with_icon(icon_content, label, button_style::extended_fab_surface)
1245}
1246
1247pub fn assist_chip<'a, Message, Renderer>(
1248    label: impl text::IntoFragment<'a>,
1249) -> Button<'a, Message, Renderer>
1250where
1251    Message: Clone + 'a,
1252    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1253{
1254    chip(label, button_style::assist_chip)
1255}
1256
1257pub fn elevated_assist_chip<'a, Message, Renderer>(
1258    label: impl text::IntoFragment<'a>,
1259) -> Button<'a, Message, Renderer>
1260where
1261    Message: Clone + 'a,
1262    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1263{
1264    chip(label, button_style::elevated_assist_chip)
1265}
1266
1267pub fn suggestion_chip<'a, Message, Renderer>(
1268    label: impl text::IntoFragment<'a>,
1269) -> Button<'a, Message, Renderer>
1270where
1271    Message: Clone + 'a,
1272    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1273{
1274    chip(label, button_style::suggestion_chip)
1275}
1276
1277pub fn elevated_suggestion_chip<'a, Message, Renderer>(
1278    label: impl text::IntoFragment<'a>,
1279) -> Button<'a, Message, Renderer>
1280where
1281    Message: Clone + 'a,
1282    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1283{
1284    chip(label, button_style::elevated_suggestion_chip)
1285}
1286
1287pub fn filter_chip<'a, Message, Renderer>(
1288    label: impl text::IntoFragment<'a>,
1289) -> Button<'a, Message, Renderer>
1290where
1291    Message: Clone + 'a,
1292    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1293{
1294    chip(label, button_style::filter_chip)
1295}
1296
1297pub fn selected_filter_chip<'a, Message, Renderer>(
1298    label: impl text::IntoFragment<'a>,
1299) -> Button<'a, Message, Renderer>
1300where
1301    Message: Clone + 'a,
1302    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1303{
1304    chip(label, button_style::selected_filter_chip)
1305}
1306
1307pub fn input_chip<'a, Message, Renderer>(
1308    label: impl text::IntoFragment<'a>,
1309) -> Button<'a, Message, Renderer>
1310where
1311    Message: Clone + 'a,
1312    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1313{
1314    chip(label, button_style::input_chip)
1315}
1316
1317pub fn selected_input_chip<'a, Message, Renderer>(
1318    label: impl text::IntoFragment<'a>,
1319) -> Button<'a, Message, Renderer>
1320where
1321    Message: Clone + 'a,
1322    Renderer: geometry::Renderer + core_text::Renderer + 'a,
1323{
1324    chip(label, button_style::selected_input_chip)
1325}