1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use agui_core::{
    context::WidgetContext,
    layout::Layout,
    unit::{Callback, Color, Shape, Sizing},
    widget::{BuildResult, WidgetBuilder, WidgetRef},
    Ref,
};
use agui_macros::{build, Widget};
use agui_primitives::{Drawable, DrawableStyle};

use crate::state::{
    hovering::Hovering,
    mouse::{Mouse, MouseButtonState},
    theme::{Style, Theme},
};

#[derive(Clone)]
pub struct ButtonStyle {
    pub normal: DrawableStyle,
    pub hover: DrawableStyle,
    pub pressed: DrawableStyle,
}

impl Style for ButtonStyle {}

impl Default for ButtonStyle {
    fn default() -> Self {
        Self {
            normal: DrawableStyle {
                color: Color::White,
            },

            hover: DrawableStyle {
                color: Color::LightGray,
            },

            pressed: DrawableStyle {
                color: Color::DarkGray,
            },
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum ButtonState {
    Normal,
    Hover,
    Pressed,
}

#[derive(Default, Widget)]
pub struct Button {
    pub layout: Ref<Layout>,

    pub style: Option<ButtonStyle>,

    pub child: WidgetRef,

    pub on_pressed: Callback<()>,
}

impl WidgetBuilder for Button {
    fn build(&self, ctx: &WidgetContext) -> BuildResult {
        ctx.set_clipping(
            Shape::RoundedRect {
                top_left: 4.0,
                top_right: 4.0,
                bottom_right: 4.0,
                bottom_left: 4.0,
            }
            .into(),
        );

        ctx.set_layout(Ref::clone(&self.layout));

        let state = ctx.computed(|ctx| {
            if let Some(hovering) = ctx.try_use_global::<Hovering>() {
                if let Some(mouse) = ctx.try_use_global::<Mouse>() {
                    if hovering.read().is_hovering(ctx) {
                        if mouse.read().button.left == MouseButtonState::Pressed {
                            return ButtonState::Pressed;
                        } else {
                            return ButtonState::Hover;
                        }
                    }
                }
            }

            ButtonState::Normal
        });

        // We init the state, instead of using `use_state`, because we don't want to react to
        // these changes, only keep track of them.
        let last_state = ctx.init_state(|| state);

        if *last_state.read() == ButtonState::Pressed {
            if let ButtonState::Pressed = state {
            } else {
                self.on_pressed.emit(());
            }
        }

        if *last_state.read() != state {
            *last_state.write() = state;
        }

        let style = Theme::resolve(ctx, &self.style);

        build! {
            Drawable {
                // We need to pass through sizing parameters so that the Drawable can react to child size if necessary,
                // but also fill the Button if the button itself is set to a non-Auto size.
                layout: Layout {
                    sizing: self.layout.try_get().map_or(Sizing::default(), |layout| layout.sizing)
                },
                style: match state {
                    ButtonState::Normal => style.normal.into(),
                    ButtonState::Hover => style.hover.into(),
                    ButtonState::Pressed => style.pressed.into(),
                },
                child: &self.child
            }
        }
    }
}