pixcil/widget/
button.rs

1use super::{FixedSizeWidget, Widget};
2use crate::{
3    app::App,
4    asset::{ButtonKind, IconId},
5    canvas_ext::CanvasExt,
6    event::{Event, MouseAction},
7};
8use pagurus::image::{Canvas, Sprite};
9use pagurus::{
10    Result,
11    spatial::{Contains, Position, Region, Size},
12};
13
14const DISABLED_ALPHA: u8 = 100;
15
16pub struct ButtonWidget {
17    region: Region,
18    kind: ButtonKind,
19    icon: IconId,
20    state: ButtonState,
21    disabled: Option<fn(&App) -> bool>,
22    number: Option<fn(&App) -> u32>,
23    number_margin: u32,
24    prev_state: ButtonState,
25    prev_disabled: bool,
26    prev_number: u32,
27}
28
29impl ButtonWidget {
30    pub fn new(kind: ButtonKind, icon: IconId) -> Self {
31        Self {
32            region: Region::default(),
33            kind,
34            icon,
35            state: ButtonState::default(),
36            disabled: None,
37            number: None,
38            number_margin: 0,
39            prev_state: ButtonState::default(),
40            prev_disabled: false,
41            prev_number: 0,
42        }
43    }
44
45    pub fn icon(&self) -> IconId {
46        self.icon
47    }
48
49    pub fn set_icon(&mut self, app: &mut App, icon: IconId) {
50        self.icon = icon;
51        app.request_redraw(self.region);
52    }
53
54    pub fn kind(&self) -> ButtonKind {
55        self.kind
56    }
57
58    pub fn state(&self) -> ButtonState {
59        self.state
60    }
61
62    pub fn set_kind(&mut self, kind: ButtonKind) {
63        self.kind = kind;
64    }
65
66    pub fn is_clicked(&self) -> bool {
67        self.state == ButtonState::Clicked
68    }
69
70    // TODO: remove
71    pub fn take_clicked(&mut self, app: &mut App) -> bool {
72        if self.state == ButtonState::Clicked {
73            app.request_redraw(self.region);
74            self.state = ButtonState::Focused;
75            true
76        } else {
77            false
78        }
79    }
80
81    pub fn with_disabled_callback(mut self, f: fn(&App) -> bool) -> Self {
82        self.set_disabled_callback(f);
83        self
84    }
85
86    pub fn set_disabled_callback(&mut self, f: fn(&App) -> bool) {
87        self.disabled = Some(f);
88    }
89
90    pub fn set_number_callback(&mut self, margin: u32, f: fn(&App) -> u32) {
91        self.number = Some(f);
92        self.number_margin = margin;
93    }
94
95    pub fn is_disabled(&self, app: &App) -> bool {
96        self.disabled.is_some_and(|f| f(app))
97    }
98
99    pub fn number(&self, app: &App) -> u32 {
100        self.number.map_or(0, |f| f(app))
101    }
102
103    fn render_number(&self, app: &App, canvas: &mut Canvas) {
104        let disabled = self.is_disabled(app);
105        let mut number = self.number(app);
106        let mut offset = Position::from_xy(
107            self.region.size.width as i32 - 18 - self.number_margin as i32,
108            self.region.size.height as i32 - 28,
109        );
110        let margin = 2;
111        loop {
112            let digit = (number % 10) as usize;
113            let sprite = &app.assets().digits_10x14[digit];
114            if disabled {
115                canvas
116                    .offset(offset)
117                    .draw_sprite_with_alpha(sprite, DISABLED_ALPHA);
118            } else {
119                canvas.offset(offset).draw_sprite(sprite);
120            }
121            offset.x -= sprite.size().width as i32 + margin;
122            number /= 10;
123            if number == 0 {
124                break;
125            }
126        }
127    }
128}
129
130impl std::fmt::Debug for ButtonWidget {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(f, "ButtonWidget {{ .. }}")
133    }
134}
135
136impl Widget for ButtonWidget {
137    fn region(&self) -> Region {
138        self.region
139    }
140
141    fn render(&self, app: &App, canvas: &mut Canvas) {
142        let mut canvas = canvas.offset(self.region.position);
143        let disabled = self.is_disabled(app);
144
145        let button = self.state.get_sprite(app, self.kind);
146        if disabled {
147            canvas.draw_sprite_with_alpha(button, DISABLED_ALPHA);
148        } else {
149            canvas.draw_sprite(button);
150        }
151
152        let mut canvas = canvas.offset(self.state.offset(self.kind));
153        let icon = app.assets().get_icon(self.icon);
154        if disabled {
155            canvas.draw_sprite_with_alpha(icon, DISABLED_ALPHA);
156        } else {
157            canvas.draw_sprite(icon);
158        }
159
160        if self.number.is_some() {
161            self.render_number(app, &mut canvas);
162        }
163    }
164
165    fn handle_event_before(&mut self, app: &mut App) -> Result<()> {
166        self.prev_disabled = self.is_disabled(app);
167        self.prev_state = self.state;
168        self.prev_number = self.number(app);
169        Ok(())
170    }
171
172    fn handle_event_after(&mut self, app: &mut App) -> Result<()> {
173        let disabled = self.is_disabled(app);
174        if disabled {
175            self.state = ButtonState::Neutral;
176        }
177        let number = self.number(app);
178        if self.prev_disabled != disabled
179            || self.prev_state != self.state
180            || self.prev_number != number
181        {
182            self.prev_disabled = disabled;
183            self.prev_state = self.state;
184            self.prev_number = number;
185            app.request_redraw(self.region);
186        }
187
188        Ok(())
189    }
190
191    fn handle_event(&mut self, app: &mut App, event: &mut Event) -> Result<()> {
192        match event {
193            Event::Mouse {
194                consumed: false,
195                action,
196                position,
197                ..
198            } => {
199                let disabled = self.is_disabled(app);
200                if !disabled && self.region.contains(position) {
201                    match action {
202                        MouseAction::Move if self.state == ButtonState::Neutral => {
203                            self.state = ButtonState::Focused;
204                        }
205                        MouseAction::Down => {
206                            self.state = ButtonState::Pressed;
207                        }
208                        MouseAction::Up if self.state == ButtonState::Pressed => {
209                            self.state = ButtonState::Clicked;
210                        }
211                        _ => {}
212                    }
213                    event.consume();
214                } else {
215                    self.state = ButtonState::Neutral;
216                }
217            }
218            Event::Mouse { position, .. } => {
219                if !self.region.contains(position) {
220                    self.state = ButtonState::Neutral;
221                }
222            }
223            _ => {}
224        }
225
226        Ok(())
227    }
228
229    fn children(&mut self) -> Vec<&mut dyn Widget> {
230        Vec::new()
231    }
232}
233
234impl FixedSizeWidget for ButtonWidget {
235    fn requiring_size(&self, _app: &App) -> Size {
236        self.kind.size()
237    }
238
239    fn set_position(&mut self, app: &App, position: Position) {
240        self.region = Region::new(position, self.requiring_size(app));
241    }
242}
243
244#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
245pub enum ButtonState {
246    #[default]
247    Neutral,
248    Focused,
249    Pressed,
250    Clicked,
251}
252
253impl ButtonState {
254    fn get_sprite(self, app: &App, kind: ButtonKind) -> &Sprite {
255        let button = app.assets().get_button(kind);
256        match self {
257            ButtonState::Neutral => &button.neutral,
258            ButtonState::Focused => &button.focused,
259            ButtonState::Pressed => &button.pressed,
260            ButtonState::Clicked => &button.pressed,
261        }
262    }
263
264    pub fn offset(self, kind: ButtonKind) -> Position {
265        let offset = Position::ORIGIN;
266        match kind {
267            ButtonKind::Basic => match self {
268                ButtonState::Neutral => offset,
269                ButtonState::Focused => offset.move_y(4),
270                ButtonState::Pressed => offset.move_y(8),
271                ButtonState::Clicked => offset.move_y(8),
272            },
273            ButtonKind::BasicPressed => offset.move_y(8),
274            ButtonKind::SliderLeft => match self {
275                ButtonState::Neutral => offset,
276                ButtonState::Focused => offset.move_y(2),
277                ButtonState::Pressed => offset.move_y(4),
278                ButtonState::Clicked => offset.move_y(4),
279            },
280            ButtonKind::SliderRight => match self {
281                ButtonState::Neutral => offset,
282                ButtonState::Focused => offset.move_y(2),
283                ButtonState::Pressed => offset.move_y(4),
284                ButtonState::Clicked => offset.move_y(4),
285            },
286            ButtonKind::Middle => match self {
287                ButtonState::Neutral => offset,
288                ButtonState::Focused => offset.move_y(2),
289                ButtonState::Pressed => offset.move_y(4),
290                ButtonState::Clicked => offset.move_y(4),
291            },
292        }
293    }
294}