cosmic_time/widget/
button.rs

1//! Allow your users to perform actions by pressing a button.
2//!
3//! A [`Button`] has some local [`State`].
4use iced_native::color;
5use iced_native::event::{self, Event};
6use iced_native::layout;
7use iced_native::mouse;
8use iced_native::overlay;
9use iced_native::renderer;
10use iced_native::touch;
11use iced_native::widget::tree::{self, Tree};
12use iced_native::widget::Operation;
13use iced_native::{
14    Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell,
15    Vector, Widget,
16};
17
18use crate::widget::StyleType;
19
20pub use iced_style::button::{Appearance, StyleSheet};
21
22/// A generic widget that produces a message when pressed.
23///
24/// ```
25/// # type Button<'a, Message> =
26/// #     iced_native::widget::Button<'a, Message, iced_native::renderer::Null>;
27/// #
28/// #[derive(Clone)]
29/// enum Message {
30///     ButtonPressed,
31/// }
32///
33/// let button = Button::new("Press me!").on_press(Message::ButtonPressed);
34/// ```
35///
36/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
37/// be disabled:
38///
39/// ```
40/// # type Button<'a, Message> =
41/// #     iced_native::widget::Button<'a, Message, iced_native::renderer::Null>;
42/// #
43/// #[derive(Clone)]
44/// enum Message {
45///     ButtonPressed,
46/// }
47///
48/// fn disabled_button<'a>() -> Button<'a, Message> {
49///     Button::new("I'm disabled!")
50/// }
51///
52/// fn enabled_button<'a>() -> Button<'a, Message> {
53///     disabled_button().on_press(Message::ButtonPressed)
54/// }
55/// ```
56#[allow(missing_debug_implementations)]
57pub struct Button<'a, Message, Renderer>
58where
59    Renderer: iced_native::Renderer,
60    Renderer::Theme: StyleSheet,
61{
62    content: Element<'a, Message, Renderer>,
63    on_press: Option<Message>,
64    width: Length,
65    height: Length,
66    padding: Padding,
67    style: StyleType<<Renderer::Theme as StyleSheet>::Style>,
68}
69
70impl<'a, Message, Renderer> Button<'a, Message, Renderer>
71where
72    Renderer: iced_native::Renderer,
73    Renderer::Theme: StyleSheet,
74{
75    /// Creates a new [`Button`] with the given content.
76    pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
77        Button {
78            content: content.into(),
79            on_press: None,
80            width: Length::Shrink,
81            height: Length::Shrink,
82            padding: Padding::new(5.0),
83            style: StyleType::Static(<Renderer::Theme as StyleSheet>::Style::default()),
84        }
85    }
86
87    /// Sets the width of the [`Button`].
88    pub fn width(mut self, width: impl Into<Length>) -> Self {
89        self.width = width.into();
90        self
91    }
92
93    /// Sets the height of the [`Button`].
94    pub fn height(mut self, height: impl Into<Length>) -> Self {
95        self.height = height.into();
96        self
97    }
98
99    /// Sets the [`Padding`] of the [`Button`].
100    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
101        self.padding = padding.into();
102        self
103    }
104
105    /// Sets the message that will be produced when the [`Button`] is pressed.
106    ///
107    /// Unless `on_press` is called, the [`Button`] will be disabled.
108    pub fn on_press(mut self, msg: Message) -> Self {
109        self.on_press = Some(msg);
110        self
111    }
112
113    /// Sets the style variant of this [`Button`].
114    pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self {
115        self.style = StyleType::Static(style);
116        self
117    }
118
119    /// Sets the animatable style variant of this [`Button`].
120    pub fn blend_style(
121        mut self,
122        style1: <Renderer::Theme as StyleSheet>::Style,
123        style2: <Renderer::Theme as StyleSheet>::Style,
124        percent: f32,
125    ) -> Self {
126        self.style = StyleType::Blend(style1, style2, percent);
127        self
128    }
129}
130
131impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer>
132where
133    Message: 'a + Clone,
134    Renderer: 'a + iced_native::Renderer,
135    Renderer::Theme: StyleSheet,
136{
137    fn tag(&self) -> tree::Tag {
138        tree::Tag::of::<State>()
139    }
140
141    fn state(&self) -> tree::State {
142        tree::State::new(State::new())
143    }
144
145    fn children(&self) -> Vec<Tree> {
146        vec![Tree::new(&self.content)]
147    }
148
149    fn diff(&self, tree: &mut Tree) {
150        tree.diff_children(std::slice::from_ref(&self.content))
151    }
152
153    fn width(&self) -> Length {
154        self.width
155    }
156
157    fn height(&self) -> Length {
158        self.height
159    }
160
161    fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
162        layout(
163            renderer,
164            limits,
165            self.width,
166            self.height,
167            self.padding,
168            |renderer, limits| self.content.as_widget().layout(renderer, limits),
169        )
170    }
171
172    fn operate(
173        &self,
174        tree: &mut Tree,
175        layout: Layout<'_>,
176        renderer: &Renderer,
177        operation: &mut dyn Operation<Message>,
178    ) {
179        operation.container(None, &mut |operation| {
180            self.content.as_widget().operate(
181                &mut tree.children[0],
182                layout.children().next().unwrap(),
183                renderer,
184                operation,
185            );
186        });
187    }
188
189    fn on_event(
190        &mut self,
191        tree: &mut Tree,
192        event: Event,
193        layout: Layout<'_>,
194        cursor_position: Point,
195        renderer: &Renderer,
196        clipboard: &mut dyn Clipboard,
197        shell: &mut Shell<'_, Message>,
198    ) -> event::Status {
199        if let event::Status::Captured = self.content.as_widget_mut().on_event(
200            &mut tree.children[0],
201            event.clone(),
202            layout.children().next().unwrap(),
203            cursor_position,
204            renderer,
205            clipboard,
206            shell,
207        ) {
208            return event::Status::Captured;
209        }
210
211        update(
212            event,
213            layout,
214            cursor_position,
215            shell,
216            &self.on_press,
217            || tree.state.downcast_mut::<State>(),
218        )
219    }
220
221    fn draw(
222        &self,
223        tree: &Tree,
224        renderer: &mut Renderer,
225        theme: &Renderer::Theme,
226        _style: &renderer::Style,
227        layout: Layout<'_>,
228        cursor_position: Point,
229        _viewport: &Rectangle,
230    ) {
231        let bounds = layout.bounds();
232        let content_layout = layout.children().next().unwrap();
233
234        let styling = draw(
235            renderer,
236            bounds,
237            cursor_position,
238            self.on_press.is_some(),
239            theme,
240            &self.style,
241            || tree.state.downcast_ref::<State>(),
242        );
243
244        self.content.as_widget().draw(
245            &tree.children[0],
246            renderer,
247            theme,
248            &renderer::Style {
249                text_color: styling.text_color,
250            },
251            content_layout,
252            cursor_position,
253            &bounds,
254        );
255    }
256
257    fn mouse_interaction(
258        &self,
259        _tree: &Tree,
260        layout: Layout<'_>,
261        cursor_position: Point,
262        _viewport: &Rectangle,
263        _renderer: &Renderer,
264    ) -> mouse::Interaction {
265        mouse_interaction(layout, cursor_position, self.on_press.is_some())
266    }
267
268    fn overlay<'b>(
269        &'b mut self,
270        tree: &'b mut Tree,
271        layout: Layout<'_>,
272        renderer: &Renderer,
273    ) -> Option<overlay::Element<'b, Message, Renderer>> {
274        self.content.as_widget_mut().overlay(
275            &mut tree.children[0],
276            layout.children().next().unwrap(),
277            renderer,
278        )
279    }
280}
281
282impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer>
283where
284    Message: Clone + 'a,
285    Renderer: iced_native::Renderer + 'a,
286    Renderer::Theme: StyleSheet,
287{
288    fn from(button: Button<'a, Message, Renderer>) -> Self {
289        Self::new(button)
290    }
291}
292
293/// The local state of a [`Button`].
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
295pub struct State {
296    is_pressed: bool,
297}
298
299impl State {
300    /// Creates a new [`State`].
301    pub fn new() -> State {
302        State::default()
303    }
304}
305
306/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
307/// accordingly.
308pub fn update<'a, Message: Clone>(
309    event: Event,
310    layout: Layout<'_>,
311    cursor_position: Point,
312    shell: &mut Shell<'_, Message>,
313    on_press: &Option<Message>,
314    state: impl FnOnce() -> &'a mut State,
315) -> event::Status {
316    match event {
317        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
318        | Event::Touch(touch::Event::FingerPressed { .. }) => {
319            if on_press.is_some() {
320                let bounds = layout.bounds();
321
322                if bounds.contains(cursor_position) {
323                    let state = state();
324
325                    state.is_pressed = true;
326
327                    return event::Status::Captured;
328                }
329            }
330        }
331        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
332        | Event::Touch(touch::Event::FingerLifted { .. }) => {
333            if let Some(on_press) = on_press.clone() {
334                let state = state();
335
336                if state.is_pressed {
337                    state.is_pressed = false;
338
339                    let bounds = layout.bounds();
340
341                    if bounds.contains(cursor_position) {
342                        shell.publish(on_press);
343                    }
344
345                    return event::Status::Captured;
346                }
347            }
348        }
349        Event::Touch(touch::Event::FingerLost { .. }) => {
350            let state = state();
351
352            state.is_pressed = false;
353        }
354        _ => {}
355    }
356
357    event::Status::Ignored
358}
359
360/// Draws a [`Button`].
361pub fn draw<'a, Renderer: iced_native::Renderer>(
362    renderer: &mut Renderer,
363    bounds: Rectangle,
364    cursor_position: Point,
365    is_enabled: bool,
366    style_sheet: &dyn StyleSheet<Style = <Renderer::Theme as StyleSheet>::Style>,
367    style: &StyleType<<Renderer::Theme as StyleSheet>::Style>,
368    state: impl FnOnce() -> &'a State,
369) -> Appearance
370where
371    Renderer::Theme: StyleSheet,
372{
373    let is_mouse_over = bounds.contains(cursor_position);
374
375    // todo disable blend if user has applied style.
376    let styling = match style {
377        StyleType::Static(style) => {
378            if !is_enabled {
379                style_sheet.disabled(style)
380            } else if is_mouse_over {
381                let state = state();
382
383                if state.is_pressed {
384                    style_sheet.pressed(style)
385                } else {
386                    style_sheet.hovered(style)
387                }
388            } else {
389                style_sheet.active(style)
390            }
391        }
392        StyleType::Blend(style1, style2, percent) => {
393            let (one, two) = if !is_enabled {
394                (style_sheet.disabled(style1), style_sheet.disabled(style2))
395            } else if is_mouse_over {
396                let state = state();
397
398                if state.is_pressed {
399                    (style_sheet.pressed(style1), style_sheet.pressed(style2))
400                } else {
401                    (style_sheet.hovered(style1), style_sheet.hovered(style2))
402                }
403            } else {
404                (style_sheet.active(style1), style_sheet.active(style2))
405            };
406
407            blend_appearances(one, two, *percent)
408        }
409    };
410
411    if styling.background.is_some() || styling.border_width > 0.0 {
412        if styling.shadow_offset != Vector::default() {
413            // TODO: Implement proper shadow support
414            renderer.fill_quad(
415                renderer::Quad {
416                    bounds: Rectangle {
417                        x: bounds.x + styling.shadow_offset.x,
418                        y: bounds.y + styling.shadow_offset.y,
419                        ..bounds
420                    },
421                    border_radius: styling.border_radius.into(),
422                    border_width: 0.0,
423                    border_color: Color::TRANSPARENT,
424                },
425                Background::Color([0.0, 0.0, 0.0, 0.5].into()),
426            );
427        }
428
429        renderer.fill_quad(
430            renderer::Quad {
431                bounds,
432                border_radius: styling.border_radius.into(),
433                border_width: styling.border_width,
434                border_color: styling.border_color,
435            },
436            styling
437                .background
438                .unwrap_or(Background::Color(Color::TRANSPARENT)),
439        );
440    }
441
442    styling
443}
444
445/// Computes the layout of a [`Button`].
446pub fn layout<Renderer>(
447    renderer: &Renderer,
448    limits: &layout::Limits,
449    width: Length,
450    height: Length,
451    padding: Padding,
452    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
453) -> layout::Node {
454    let limits = limits.width(width).height(height);
455
456    let mut content = layout_content(renderer, &limits.pad(padding));
457    let padding = padding.fit(content.size(), limits.max());
458    let size = limits.pad(padding).resolve(content.size()).pad(padding);
459
460    content.move_to(Point::new(padding.left, padding.top));
461
462    layout::Node::with_children(size, vec![content])
463}
464
465/// Returns the [`mouse::Interaction`] of a [`Button`].
466pub fn mouse_interaction(
467    layout: Layout<'_>,
468    cursor_position: Point,
469    is_enabled: bool,
470) -> mouse::Interaction {
471    let is_mouse_over = layout.bounds().contains(cursor_position);
472
473    if is_mouse_over && is_enabled {
474        mouse::Interaction::Pointer
475    } else {
476        mouse::Interaction::default()
477    }
478}
479
480fn blend_appearances(
481    one: iced_style::button::Appearance,
482    mut two: iced_style::button::Appearance,
483    percent: f32,
484) -> iced_style::button::Appearance {
485    use crate::lerp;
486
487    // shadow offet
488    let x1 = one.shadow_offset.x;
489    let y1 = one.shadow_offset.y;
490    let x2 = two.shadow_offset.x;
491    let y2 = two.shadow_offset.y;
492
493    // background
494    let background_one: Color = one
495        .background
496        .map(|b| match b {
497            Background::Color(c) => c,
498        })
499        .unwrap_or(color!(0, 0, 0));
500    let background_two: Color = two
501        .background
502        .map(|b| match b {
503            Background::Color(c) => c,
504        })
505        .unwrap_or(color!(0, 0, 0));
506    let background_mix: [f32; 4] = background_one
507        .into_linear()
508        .iter()
509        .zip(background_two.into_linear().iter())
510        .map(|(o, t)| lerp(*o, *t, percent))
511        .collect::<Vec<f32>>()
512        .try_into()
513        .unwrap();
514    let new_background_color: Color = background_mix.into();
515
516    // boarder color
517    let border_color: [f32; 4] = one
518        .border_color
519        .into_linear()
520        .iter()
521        .zip(two.border_color.into_linear().iter())
522        .map(|(o, t)| lerp(*o, *t, percent))
523        .collect::<Vec<f32>>()
524        .try_into()
525        .unwrap();
526
527    // text
528    let text: [f32; 4] = one
529        .text_color
530        .into_linear()
531        .iter()
532        .zip(two.text_color.into_linear().iter())
533        .map(|(o, t)| lerp(*o, *t, percent))
534        .collect::<Vec<f32>>()
535        .try_into()
536        .unwrap();
537
538    two.shadow_offset = Vector::new(lerp(x1, x2, percent), lerp(y1, y2, percent));
539    two.background = Some(new_background_color.into());
540    two.border_radius = lerp(one.border_radius, two.border_radius, percent);
541    two.border_width = lerp(one.border_width, two.border_width, percent);
542    two.border_color = border_color.into();
543    two.text_color = text.into();
544    two
545}