streamdeck_oxide/view/
customizable.rs

1//! customizable view implementation for the view system.
2//!
3//! This module provides a customizable view implementation for the view system.
4//! customizable views allow for programmatic creation of views with custom buttons.
5
6use std::{
7    future::Future,
8    marker::PhantomData,
9    pin::Pin,
10    sync::{
11        atomic::{AtomicBool, Ordering},
12        Arc,
13    },
14};
15
16use generic_array::{sequence::GenericSequence, GenericArray, ArrayLength};
17use tokio::sync::mpsc;
18
19use crate::navigation::NavigationEntry;
20
21use super::{button::Button, button::ButtonState, matrix::ButtonMatrix, View};
22
23type Matrix<W, H, C, N> = GenericArray<GenericArray<Option<CustomizableViewButton<W, H, C, N>>, W>, H>;
24
25/// A customizable view.
26///
27/// This struct represents a customizable view in the view system.
28/// It allows for programmatic creation of views with custom buttons.
29pub struct CustomizableView<W, H, C, N>
30where
31    W: ArrayLength,
32    H: ArrayLength,
33    C: Send + Clone + Sync + 'static,
34    N: NavigationEntry<W, H, C>,
35{
36    /// The matrix of buttons.
37    pub(crate) matrix: Matrix<W, H, C, N>,
38    /// Phantom data for the navigation type.
39    pub(crate) _marker: PhantomData<N>,
40}
41
42/// A button in a customizable view.
43///
44/// This enum represents a button in a customizable view.
45/// It can be either a navigation button or a custom button.
46pub enum CustomizableViewButton<W, H, C, N>
47where
48    W: ArrayLength,
49    H: ArrayLength,
50    C: Send + Clone + Sync + 'static,
51    N: NavigationEntry<W, H, C>,
52{
53    /// A navigation button.
54    ///
55    /// This button navigates to a different view when clicked.
56    Navigation {
57        /// The navigation entry to navigate to.
58        navigation: N,
59        /// The button to display.
60        button: Button,
61        /// Phantom data for the width and height.
62        _marker: PhantomData<fn() -> (W, H)>
63    },
64    /// A custom button.
65    ///
66    /// This button has custom behavior when clicked.
67    Button(Box<dyn CustomButton<C>>),
68}
69
70/// A trait for custom buttons.
71///
72/// This trait is implemented by types that represent custom buttons
73/// in a customizable view. It provides methods for getting the button state,
74/// fetching state, and handling clicks.
75#[async_trait::async_trait]
76pub trait CustomButton<C>: Send + Sync + 'static
77where
78    C: Send + Clone + Sync + 'static,
79{
80    /// Get the button state.
81    ///
82    /// This method returns the current state of the button.
83    fn get_state(&self) -> Button;
84
85    /// Fetch state for the button.
86    ///
87    /// This method fetches the state for the button.
88    /// It takes the application context.
89    async fn fetch(&self, context: &C) -> Result<(), Box<dyn std::error::Error>>;
90
91    /// Handle a button click.
92    ///
93    /// This method is called when the button is clicked.
94    /// It takes the application context.
95    async fn click(&self, context: &C) -> Result<(), Box<dyn std::error::Error>>;
96}
97
98/// A future that returns a boolean.
99pub type FetchFuture =
100    Pin<Box<dyn Future<Output = Result<bool, Box<dyn std::error::Error>>> + Send + Sync>>;
101
102/// A function that returns a fetch future.
103pub type FetchFunction<C> = Arc<Box<dyn Fn(&C) -> FetchFuture + Send + Sync>>;
104
105/// A future that returns a unit.
106pub type ClickFuture =
107    Pin<Box<dyn Future<Output = Result<(), Box<dyn std::error::Error>>> + Send + Sync>>;
108
109/// A function that returns a click future.
110pub type ClickAction<C> = Arc<Box<dyn Fn(&C) -> ClickFuture + Send + Sync>>;
111
112/// A future that returns a unit.
113pub type PushFuture =
114    Pin<Box<dyn Future<Output = Result<(), Box<dyn std::error::Error>>> + Send + Sync>>;
115
116/// A function that returns a push future.
117pub type PushFunction<C> = Arc<Box<dyn Fn(&C, bool) -> PushFuture + Send + Sync>>;
118
119/// A toggle button.
120///
121/// This struct represents a toggle button in a customizable view.
122/// It has two states: active and inactive.
123pub struct ToggleButton<C>
124where
125    C: Send + Clone + Sync + 'static,
126{
127    /// The function to fetch the active state.
128    pub(crate) fetch_active: FetchFunction<C>,
129    /// The function to push the active state.
130    pub(crate) push_active: PushFunction<C>,
131    /// The button to display when inactive.
132    pub(crate) button: Button,
133    /// The button to display when active.
134    pub(crate) active_button: Button,
135    /// The current active state.
136    pub(crate) active: AtomicBool,
137}
138
139/// A click button.
140///
141/// This struct represents a click button in a customizable view.
142/// It has a single action that is performed when clicked.
143pub struct ClickButton<C>
144where
145    C: Send + Clone + Sync + 'static,
146{
147    /// The function to call when clicked.
148    pub(crate) push_click: ClickAction<C>,
149    /// The button to display.
150    pub(crate) button: Button,
151}
152
153impl<C> ClickButton<C>
154where
155    C: Send + Clone + Sync + 'static,
156{
157    /// Create a new click button.
158    ///
159    /// This method creates a new click button with the given text,
160    /// icon, and action.
161    pub fn new<A, F, S>(text: S, icon: Option<&'static str>, action: A) -> Self
162    where
163        F: Future<Output = Result<(), Box<dyn std::error::Error>>> + Send + Sync + 'static,
164        A: Fn(C) -> F + Send + Sync + Clone + 'static,
165        S: Into<String>
166    {
167        ClickButton {
168            push_click: Arc::new(Box::new(move |ctx| {
169                let action = action.clone();
170                let ctx = ctx.clone();
171                Box::pin(async move { action(ctx).await })
172            })),
173            button: Button {
174                text: text.into(),
175                icon,
176                state: ButtonState::Default,
177            },
178        }
179    }
180}
181
182impl<C> ToggleButton<C>
183where
184    C: Send + Clone + Sync + 'static,
185{
186    /// Create a new toggle button.
187    ///
188    /// This method creates a new toggle button with the given text,
189    /// icon, fetch function, and push function.
190    pub fn new<FF, PF, F, P, S>(
191        text: S,
192        icon: Option<&'static str>,
193        fetch_active: F,
194        push_active: P,
195    ) -> Self
196    where
197        FF: Future<Output = Result<bool, Box<dyn std::error::Error>>> + Send + Sync + 'static,
198        PF: Future<Output = Result<(), Box<dyn std::error::Error>>> + Send + Sync + 'static,
199        F: Fn(C) -> FF + Send + Sync + Clone + 'static,
200        P: Fn(C, bool) -> PF + Send + Sync + Clone + 'static,
201        S: Into<String>
202    {
203        let text = text.into();
204        ToggleButton {
205            fetch_active: Arc::new(Box::new(move |ctx| {
206                let fetch_active = fetch_active.clone();
207                let ctx = ctx.clone();
208                Box::pin(async move { fetch_active(ctx).await })
209            })),
210            push_active: Arc::new(Box::new(move |ctx, x| {
211                let push_active = push_active.clone();
212                let ctx = ctx.clone();
213                Box::pin(async move { push_active(ctx, x).await })
214            })),
215            button: Button {
216                text: text.clone(),
217                icon,
218                state: ButtonState::Default,
219            },
220            active_button: Button {
221                text,
222                icon,
223                state: ButtonState::Active,
224            },
225            active: AtomicBool::new(false),
226        }
227    }
228
229    /// Set the active button.
230    ///
231    /// This method sets the button to display when active.
232    pub fn when_active<S: Into<String>>(self, text: S, icon: Option<&'static str>) -> Self {
233        ToggleButton {
234            active_button: Button {
235                text: text.into(),
236                icon,
237                state: ButtonState::Active,
238            },
239            ..self
240        }
241    }
242}
243
244#[async_trait::async_trait]
245impl<C> CustomButton<C> for ToggleButton<C>
246where
247    C: Send + Clone + Sync + 'static,
248{
249    fn get_state(&self) -> Button {
250        let current_state = self.active.load(Ordering::SeqCst);
251        match current_state {
252            true => self.active_button.clone(),
253            false => self.button.clone(),
254        }
255    }
256
257    async fn fetch(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
258        let new_state = (self.fetch_active)(context).await;
259        self.active.store(new_state?, Ordering::SeqCst);
260        Ok(())
261    }
262
263    async fn click(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
264        let current_state = self.active.load(Ordering::SeqCst);
265        (self.push_active)(context, !current_state).await?;
266        self.active.store(!current_state, Ordering::SeqCst);
267        Ok(())
268    }
269}
270
271#[async_trait::async_trait]
272impl<C> CustomButton<C> for ClickButton<C>
273where
274    C: Send + Clone + Sync + 'static,
275{
276    fn get_state(&self) -> Button {
277        self.button.clone()
278    }
279
280    async fn fetch(&self, _: &C) -> Result<(), Box<dyn std::error::Error>> {
281        Ok(())
282    }
283
284    async fn click(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
285        (self.push_click)(context).await?;
286        Ok(())
287    }
288}
289
290impl<W, H, C, N> Default for CustomizableView<W, H, C, N>
291where
292    W: ArrayLength,
293    H: ArrayLength,
294    C: Send + Clone + Sync + 'static,
295    N: NavigationEntry<W, H, C>,
296{
297    fn default() -> Self {
298        CustomizableView::new()
299    }
300}
301
302impl<W, H, C, N> CustomizableView<W, H, C, N>
303where
304    W: ArrayLength,
305    H: ArrayLength,
306    C: Send + Clone + Sync + 'static,
307    N: NavigationEntry<W, H, C>,
308{
309    /// Create a new customizable view.
310    pub fn new() -> Self {
311        CustomizableView {
312            matrix: GenericArray::generate(|_| GenericArray::generate(|_| None)),
313            _marker: PhantomData,
314        }
315    }
316
317    /// Set a button at the given coordinates.
318    ///
319    /// This method sets a custom button at the given coordinates.
320    pub fn set_button(
321        &mut self,
322        x: usize,
323        y: usize,
324        button: impl CustomButton<C>,
325    ) -> Result<(), Box<dyn std::error::Error>> {
326        if x < W::to_usize() && y < H::to_usize() {
327            self.matrix[y][x] = Some(CustomizableViewButton::Button(Box::new(button)));
328            Ok(())
329        } else {
330            Err(Box::new(std::io::Error::new(
331                std::io::ErrorKind::InvalidInput,
332                "Row or column out of bounds",
333            )))
334        }
335    }
336
337    /// Set a navigation button at the given coordinates.
338    ///
339    /// This method sets a navigation button at the given coordinates.
340    pub fn set_navigation<S: Into<String>>(
341        &mut self,
342        x: usize,
343        y: usize,
344        navigation: N,
345        text: S,
346        icon: Option<&'static str>,
347    ) -> Result<(), Box<dyn std::error::Error>> {
348        if x < W::to_usize() && y < H::to_usize() {
349            self.matrix[y][x] = Some(CustomizableViewButton::Navigation {
350                navigation,
351                button: Button {
352                    text: text.into(),
353                    icon,
354                    state: ButtonState::Default,
355                },
356                _marker: PhantomData,
357            });
358            Ok(())
359        } else {
360            Err(Box::new(std::io::Error::new(
361                std::io::ErrorKind::InvalidInput,
362                "Row or column out of bounds",
363            )))
364        }
365    }
366
367    /// Remove a button at the given coordinates.
368    ///
369    /// This method removes the button at the given coordinates.
370    pub fn remove_button(&mut self, x: usize, y: usize) -> Result<(), Box<dyn std::error::Error>> {
371        if x < W::to_usize() && y < H::to_usize() {
372            self.matrix[y][x] = None;
373            Ok(())
374        } else {
375            Err(Box::new(std::io::Error::new(
376                std::io::ErrorKind::InvalidInput,
377                "Row or column out of bounds",
378            )))
379        }
380    }
381}
382
383#[async_trait::async_trait]
384impl<W, H, C, N> View<W, H, C, N> for CustomizableView<W, H, C, N>
385where
386    W: ArrayLength,
387    H: ArrayLength,
388    C: Send + Clone + Sync + 'static,
389    N: NavigationEntry<W, H, C>,
390{
391    async fn render(&self) -> Result<ButtonMatrix<W, H>, Box<dyn std::error::Error>> {
392        let mut button_matrix = ButtonMatrix::new();
393        for x in 0..W::to_usize() {
394            for y in 0..H::to_usize() {
395                if let Some(button) = &self.matrix[y][x] {
396                    let state = match button {
397                        CustomizableViewButton::Navigation { button, .. } => button,
398                        CustomizableViewButton::Button(button) => &button.get_state(),
399                    };
400                    button_matrix.set_button(x, y, state.clone())?;
401                }
402            }
403        }
404        Ok(button_matrix)
405    }
406    
407    async fn on_click(
408        &self,
409        context: &C,
410        index: u8,
411        navigation: Arc<mpsc::Sender<N>>,
412    ) -> Result<(), Box<dyn std::error::Error>> {
413        if (index as usize) < W::to_usize() * H::to_usize() {
414            let x = index % W::to_u8();
415            let y = index / W::to_u8();
416            if let Some(button) = &self.matrix[y as usize][x as usize] {
417                match button {
418                    CustomizableViewButton::Navigation { navigation: nav, .. } => {
419                        navigation.send(nav.clone()).await?;
420                    }
421                    CustomizableViewButton::Button(button) => {
422                        button.click(context).await?;
423                    }
424                }
425            }
426            Ok(())
427        } else {
428            return Err(Box::new(std::io::Error::new(
429                std::io::ErrorKind::InvalidInput,
430                "Button index out of bounds",
431            )));
432        }
433    }
434
435    async fn fetch_all(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
436        for x in 0..W::to_usize() {
437            for y in 0..H::to_usize() {
438                if let Some(button) = &self.matrix[y][x] {
439                    match button {
440                        CustomizableViewButton::Navigation { .. } => {}
441                        CustomizableViewButton::Button(button) => {
442                            button.fetch(context).await?;
443                        }
444                    }
445                }
446            }
447        }
448        Ok(())
449    }
450}