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(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}