Skip to main content

boppo_core/
button.rs

1use serde::de::Error;
2use serde::{Deserialize, Deserializer};
3
4use crate::Lights;
5use crate::buttons::Buttons;
6use crate::internal::BUTTON_COUNTS;
7use crate::lights::LightDir;
8
9/// One of the 10 top buttons.
10///
11/// Button 0 is the top left. 4 is the top right. 5 is the bottom left and 9 is
12/// the bottom right. (English lexographical order).
13///
14/// Represented visually:
15///
16/// ```text
17/// 0 1 2 3 4
18/// 5 6 7 8 9
19/// ```
20#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
21#[repr(u8)]
22pub enum Button {
23    /// [`Row::Top`], [`Column::C0`]
24    B0 = 0,
25    /// [`Row::Top`], [`Column::C1`]
26    B1 = 1,
27    /// [`Row::Top`], [`Column::C2`]
28    B2 = 2,
29    /// [`Row::Top`], [`Column::C3`]
30    B3 = 3,
31    /// [`Row::Top`], [`Column::C4`]
32    B4 = 4,
33    /// [`Row::Bottom`], [`Column::C0`]
34    B5 = 5,
35    /// [`Row::Bottom`], [`Column::C1`]
36    B6 = 6,
37    /// [`Row::Bottom`], [`Column::C2`]
38    B7 = 7,
39    /// [`Row::Bottom`], [`Column::C3`]
40    B8 = 8,
41    /// [`Row::Bottom`], [`Column::C4`]
42    B9 = 9,
43}
44
45impl Button {
46    /// The number of light-up buttons on top of Boppo.
47    pub const COUNT: usize = 10;
48
49    #[track_caller]
50    #[must_use]
51    /// Converts an `index` to its corresponding [`Button`].
52    ///
53    /// # Panics
54    ///
55    /// This function will panic if `index >= Button::COUNT`.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// # use boppo_core::Button;
61    /// let button = Button::from_index(6);
62    /// assert_eq!(button, Button::B6);
63    /// ```
64    ///
65    /// ```should_panic
66    /// # use boppo_core::Button;
67    /// // Panic!
68    /// let button = Button::from_index(100);
69    /// ```
70    pub const fn from_index(index: usize) -> Button {
71        use Button::{B0, B1, B2, B3, B4, B5, B6, B7, B8, B9};
72        match index {
73            0 => B0,
74            1 => B1,
75            2 => B2,
76            3 => B3,
77            4 => B4,
78            5 => B5,
79            6 => B6,
80            7 => B7,
81            8 => B8,
82            9 => B9,
83            _ => panic!("index must be less than Button::COUNT"),
84        }
85    }
86
87    /// Returns the [`Button`] at `row`, `col`.
88    #[track_caller]
89    #[must_use]
90    pub const fn from_row_col(row: Row, col: Column) -> Button {
91        Button::from_index(row.index() * 5 + col.index())
92    }
93
94    /// Returns a random [`Button`].
95    ///
96    /// See also [`Buttons::choose_n_randomly()`] and [`Buttons::choose_one_randomly()`].
97    #[must_use]
98    pub fn random() -> Button {
99        Buttons::all().choose_one_randomly()
100    }
101
102    /// Returns this button's index.
103    #[must_use]
104    pub const fn index(self) -> usize {
105        self as usize
106    }
107
108    /// Returns this button's [`Row`].
109    #[must_use]
110    pub const fn row(self) -> Row {
111        if self.index() < 5 {
112            Row::Top
113        } else {
114            Row::Bottom
115        }
116    }
117
118    /// Returns this button's [`Column`].
119    #[must_use]
120    pub const fn col(self) -> Column {
121        if self.row().index() == 0 {
122            Column::from_index(self.index())
123        } else {
124            Column::from_index(self.index() - 5)
125        }
126    }
127
128    /// Return the button clockwise of this button
129    /// # Examples
130    ///
131    /// ```
132    /// # use boppo_core::Button;
133    /// assert_eq!(Button::B3.next_clockwise(), Button::B4);
134    /// assert_eq!(Button::B4.next_clockwise(), Button::B9);
135    /// assert_eq!(Button::B5.next_clockwise(), Button::B0);
136    /// assert_eq!(Button::B9.next_clockwise(), Button::B8);
137    /// ```
138    #[must_use]
139    pub const fn next_clockwise(self) -> Button {
140        use Button::{B0, B1, B2, B3, B4, B5, B6, B7, B8, B9};
141        #[allow(
142            clippy::missing_panics_doc,
143            reason = "These functions will never panic"
144        )]
145        match self {
146            B0 | B1 | B2 | B3 => self.right().unwrap(),
147            B4 => B9,
148            B5 => B0,
149            B6 | B7 | B8 | B9 => self.left().unwrap(),
150        }
151    }
152
153    /// Return the button counterclockwise of this button
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// # use boppo_core::Button;
159    /// assert_eq!(Button::B1.next_counterclockwise(), Button::B0);
160    /// assert_eq!(Button::B8.next_counterclockwise(), Button::B9);
161    /// assert_eq!(Button::B0.next_counterclockwise(), Button::B5);
162    /// assert_eq!(Button::B9.next_counterclockwise(), Button::B4);
163    /// ```
164    #[must_use]
165    pub const fn next_counterclockwise(self) -> Button {
166        use Button::{B0, B1, B2, B3, B4, B5, B6, B7, B8, B9};
167        #[allow(
168            clippy::missing_panics_doc,
169            reason = "These functions will never panic"
170        )]
171        match self {
172            B0 => B5,
173            B1 | B2 | B3 | B4 => self.left().unwrap(),
174            B5 | B6 | B7 | B8 => self.right().unwrap(),
175            B9 => B4,
176        }
177    }
178
179    /// The button above this one (if any)
180    #[must_use]
181    pub const fn above(self) -> Option<Button> {
182        if self.index() > 4 {
183            Some(Button::from_index(self.index() - 5))
184        } else {
185            None
186        }
187    }
188
189    /// The button below this one (if any)
190    #[must_use]
191    pub const fn below(self) -> Option<Button> {
192        if self.index() < 5 {
193            Some(Button::from_index(self.index() + 5))
194        } else {
195            None
196        }
197    }
198
199    /// The button to the left of this one (if any)
200    #[must_use]
201    pub const fn left(self) -> Option<Button> {
202        if matches!(self, Self::B0 | Self::B5) {
203            None
204        } else {
205            Some(Button::from_index(self.index() - 1))
206        }
207    }
208
209    /// The button to the right of this one (if any)
210    #[must_use]
211    pub const fn right(self) -> Option<Button> {
212        if matches!(self, Self::B4 | Self::B9) {
213            None
214        } else {
215            Some(Button::from_index(self.index() + 1))
216        }
217    }
218
219    /// Return the button where this button would be if Boppo is rotated 180 degrees around its center
220    #[must_use]
221    pub const fn rotate_180(self) -> Button {
222        Button::from_index(Button::COUNT - 1 - self.index())
223    }
224
225    /// Returns a [`Buttons`] with only this button selected.
226    #[must_use]
227    pub const fn to_buttons(self) -> Buttons {
228        Buttons::from_index(self.index())
229    }
230
231    /// Returns a [`Lights`] with only this button's lights selected.
232    #[must_use]
233    pub const fn to_lights(self) -> Lights {
234        Lights::all_from_button(self)
235    }
236
237    /// Sets this button's lights to `color`.
238    ///
239    /// This function sets the lights immediately, if you want to modify many lights' or buttons'
240    /// colors at once, consider using [`Framebuffer`][crate::Framebuffer].
241    pub fn set_color(self, color: crate::color::RGB) {
242        crate::MainFramebuffer::get().set_color(self.into(), color);
243    }
244
245    /// Sets this button's lights to [`color::OFF`][crate::color::OFF]. Shorthand for
246    /// `self.set_color(color::OFF)`.
247    pub fn set_off(self) {
248        self.set_color(crate::color::OFF);
249    }
250
251    /// Returns the light at `dir` on this button.
252    #[must_use]
253    pub const fn light_at(self, dir: LightDir) -> Lights {
254        self.to_lights().only(dir)
255    }
256
257    /// Returns true if the button is currently pressed.
258    ///
259    /// See also [`ButtonEvents`][crate::ButtonEvents] which is an alternative way to
260    /// receive button events.
261    #[must_use]
262    pub fn is_pressed(&self) -> bool {
263        Buttons::currently_pressed().contains(*self)
264    }
265
266    /// Wait for this button to be pressed. If the button is already pressed it returns
267    /// immediately.
268    pub async fn wait_for_press(&self) {
269        self.wait_for(true).await;
270    }
271
272    /// Wait for this button to be released. If the button is already released it returns
273    /// immediately.
274    pub async fn wait_for_release(&self) {
275        self.wait_for(false).await;
276    }
277
278    async fn wait_for(&self, press: bool) {
279        let _ = BUTTON_COUNTS
280            .get()
281            .unwrap()
282            .clone()
283            .wait_for(|counts| counts.is_pressed(*self) == press)
284            .await;
285    }
286}
287
288impl<'de> Deserialize<'de> for Button {
289    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290    where
291        D: Deserializer<'de>,
292    {
293        let idx = usize::deserialize(deserializer)?;
294        if idx >= Button::COUNT {
295            return Err(D::Error::custom(format!("Invalid button index: {idx}")));
296        }
297        Ok(Button::from_index(idx))
298    }
299}
300
301/// A row of [`Button`]s.
302///
303#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
304#[repr(u8)]
305pub enum Row {
306    /// [`Button::B0`] through [`Button::B4`]
307    Top = 0,
308    /// [`Button::B5`] through [`Button::B9`]
309    Bottom = 1,
310}
311
312impl Row {
313    /// The number of rows of top buttons on Boppo.
314    pub const COUNT: usize = 2;
315
316    /// Returns the index of this row, 0 ([`Top`][Row::Top]) or 1 ([`Bottom`][Row::Bottom]).
317    #[must_use]
318    pub const fn index(&self) -> usize {
319        *self as usize
320    }
321
322    /// Returns [`Row::Top`] if `idx == 0`, and [`Row::Bottom`] if `idx == 1`.
323    ///
324    /// # Panics
325    ///
326    /// This function will panic if `idx` is not `0` or `1`.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// # use boppo_core::Row;
332    /// let top_row = Row::from_index(0);
333    /// assert_eq!(top_row, Row::Top);
334    ///
335    /// let bottom_row = Row::from_index(1);
336    /// assert_eq!(bottom_row, Row::Bottom);
337    /// ```
338    #[must_use]
339    pub const fn from_index(idx: usize) -> Row {
340        match idx {
341            0 => Row::Top,
342            1 => Row::Bottom,
343            _ => panic!("Invalid row index"),
344        }
345    }
346
347    /// Returns the opposite row to this one.
348    #[must_use]
349    pub const fn opposite(&self) -> Row {
350        match self {
351            Row::Top => Row::Bottom,
352            Row::Bottom => Row::Top,
353        }
354    }
355}
356
357/// A column of [`Buttons`]. When the side buttons face the user,
358/// [`Column::C0`] is the leftmost column, and [`Column::C4`] is the rightmost column.
359///
360#[repr(u8)]
361#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
362#[expect(missing_docs, reason = "Variants documented in enum")]
363pub enum Column {
364    C0 = 0,
365    C1 = 1,
366    C2 = 2,
367    C3 = 3,
368    C4 = 4,
369}
370
371impl Column {
372    /// The number of columns of top buttons on Boppo.
373    pub const COUNT: usize = 5;
374
375    /// Returns the index of this column.
376    #[must_use]
377    pub const fn index(&self) -> usize {
378        *self as usize
379    }
380
381    /// Returns the corresponding column for the given `idx`.
382    ///
383    /// # Panics
384    ///
385    /// This function will panic if `idx` exceeds `4`.
386    ///
387    /// # Examples
388    ///
389    /// ```
390    /// # use boppo_core::Column;
391    /// let col = Column::from_index(2);
392    /// assert_eq!(col, Column::C2);
393    /// ```
394    ///
395    /// ```should_panic
396    /// # use boppo_core::Column;
397    /// // Panic! No fifth column exists!
398    /// let col = Column::from_index(5);
399    /// ```
400    #[must_use]
401    pub const fn from_index(idx: usize) -> Column {
402        match idx {
403            0 => Column::C0,
404            1 => Column::C1,
405            2 => Column::C2,
406            3 => Column::C3,
407            4 => Column::C4,
408            _ => panic!("Invalid column index"),
409        }
410    }
411
412    /// Returns an array containing all five [`Column`] variants, ordered left-to-right.
413    #[must_use]
414    pub const fn all() -> [Column; 5] {
415        [Column::C0, Column::C1, Column::C2, Column::C3, Column::C4]
416    }
417}
418
419#[cfg(test)]
420#[path = "./tests/button_test.rs"]
421mod test;