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;