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>(text: &'static str, 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    {
166        ClickButton {
167            push_click: Arc::new(Box::new(move |ctx| {
168                let action = action.clone();
169                let ctx = ctx.clone();
170                Box::pin(async move { action(ctx).await })
171            })),
172            button: Button {
173                text,
174                icon,
175                state: ButtonState::Default,
176            },
177        }
178    }
179}
180
181impl<C> ToggleButton<C>
182where
183    C: Send + Clone + Sync + 'static,
184{
185    /// Create a new toggle button.
186    ///
187    /// This method creates a new toggle button with the given text,
188    /// icon, fetch function, and push function.
189    pub fn new<FF, PF, F, P>(
190        text: &'static str,
191        icon: Option<&'static str>,
192        fetch_active: F,
193        push_active: P,
194    ) -> Self
195    where
196        FF: Future<Output = Result<bool, Box<dyn std::error::Error>>> + Send + Sync + 'static,
197        PF: Future<Output = Result<(), Box<dyn std::error::Error>>> + Send + Sync + 'static,
198        F: Fn(C) -> FF + Send + Sync + Clone + 'static,
199        P: Fn(C, bool) -> PF + Send + Sync + Clone + 'static,
200    {
201        ToggleButton {
202            fetch_active: Arc::new(Box::new(move |ctx| {
203                let fetch_active = fetch_active.clone();
204                let ctx = ctx.clone();
205                Box::pin(async move { fetch_active(ctx).await })
206            })),
207            push_active: Arc::new(Box::new(move |ctx, x| {
208                let push_active = push_active.clone();
209                let ctx = ctx.clone();
210                Box::pin(async move { push_active(ctx, x).await })
211            })),
212            button: Button {
213                text,
214                icon,
215                state: ButtonState::Default,
216            },
217            active_button: Button {
218                text,
219                icon,
220                state: ButtonState::Active,
221            },
222            active: AtomicBool::new(false),
223        }
224    }
225
226    /// Set the active button.
227    ///
228    /// This method sets the button to display when active.
229    pub fn when_active(self, text: &'static str, icon: Option<&'static str>) -> Self {
230        ToggleButton {
231            active_button: Button {
232                text,
233                icon,
234                state: ButtonState::Active,
235            },
236            ..self
237        }
238    }
239}
240
241#[async_trait::async_trait]
242impl<C> CustomButton<C> for ToggleButton<C>
243where
244    C: Send + Clone + Sync + 'static,
245{
246    fn get_state(&self) -> Button {
247        let current_state = self.active.load(Ordering::SeqCst);
248        match current_state {
249            true => self.active_button,
250            false => self.button,
251        }
252    }
253
254    async fn fetch(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
255        let new_state = (self.fetch_active)(context).await;
256        self.active.store(new_state?, Ordering::SeqCst);
257        Ok(())
258    }
259
260    async fn click(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
261        let current_state = self.active.load(Ordering::SeqCst);
262        (self.push_active)(context, !current_state).await?;
263        self.active.store(!current_state, Ordering::SeqCst);
264        Ok(())
265    }
266}
267
268#[async_trait::async_trait]
269impl<C> CustomButton<C> for ClickButton<C>
270where
271    C: Send + Clone + Sync + 'static,
272{
273    fn get_state(&self) -> Button {
274        self.button
275    }
276
277    async fn fetch(&self, _: &C) -> Result<(), Box<dyn std::error::Error>> {
278        Ok(())
279    }
280
281    async fn click(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
282        (self.push_click)(context).await?;
283        Ok(())
284    }
285}
286
287impl<W, H, C, N> Default for CustomizableView<W, H, C, N>
288where
289    W: ArrayLength,
290    H: ArrayLength,
291    C: Send + Clone + Sync + 'static,
292    N: NavigationEntry<W, H, C>,
293{
294    fn default() -> Self {
295        CustomizableView::new()
296    }
297}
298
299impl<W, H, C, N> CustomizableView<W, H, C, N>
300where
301    W: ArrayLength,
302    H: ArrayLength,
303    C: Send + Clone + Sync + 'static,
304    N: NavigationEntry<W, H, C>,
305{
306    /// Create a new customizable view.
307    pub fn new() -> Self {
308        CustomizableView {
309            matrix: GenericArray::generate(|_| GenericArray::generate(|_| None)),
310            _marker: PhantomData,
311        }
312    }
313
314    /// Set a button at the given coordinates.
315    ///
316    /// This method sets a custom button at the given coordinates.
317    pub fn set_button(
318        &mut self,
319        x: usize,
320        y: usize,
321        button: impl CustomButton<C>,
322    ) -> Result<(), Box<dyn std::error::Error>> {
323        if x < W::to_usize() && y < H::to_usize() {
324            self.matrix[y][x] = Some(CustomizableViewButton::Button(Box::new(button)));
325            Ok(())
326        } else {
327            Err(Box::new(std::io::Error::new(
328                std::io::ErrorKind::InvalidInput,
329                "Row or column out of bounds",
330            )))
331        }
332    }
333
334    /// Set a navigation button at the given coordinates.
335    ///
336    /// This method sets a navigation button at the given coordinates.
337    pub fn set_navigation(
338        &mut self,
339        x: usize,
340        y: usize,
341        navigation: N,
342        text: &'static str,
343        icon: Option<&'static str>,
344    ) -> Result<(), Box<dyn std::error::Error>> {
345        if x < W::to_usize() && y < H::to_usize() {
346            self.matrix[y][x] = Some(CustomizableViewButton::Navigation {
347                navigation,
348                button: Button {
349                    text,
350                    icon,
351                    state: ButtonState::Default,
352                },
353                _marker: PhantomData,
354            });
355            Ok(())
356        } else {
357            Err(Box::new(std::io::Error::new(
358                std::io::ErrorKind::InvalidInput,
359                "Row or column out of bounds",
360            )))
361        }
362    }
363
364    /// Remove a button at the given coordinates.
365    ///
366    /// This method removes the button at the given coordinates.
367    pub fn remove_button(&mut self, x: usize, y: usize) -> Result<(), Box<dyn std::error::Error>> {
368        if x < W::to_usize() && y < H::to_usize() {
369            self.matrix[y][x] = None;
370            Ok(())
371        } else {
372            Err(Box::new(std::io::Error::new(
373                std::io::ErrorKind::InvalidInput,
374                "Row or column out of bounds",
375            )))
376        }
377    }
378}
379
380#[async_trait::async_trait]
381impl<W, H, C, N> View<W, H, C, N> for CustomizableView<W, H, C, N>
382where
383    W: ArrayLength,
384    H: ArrayLength,
385    C: Send + Clone + Sync + 'static,
386    N: NavigationEntry<W, H, C>,
387{
388    async fn render(&self) -> Result<ButtonMatrix<W, H>, Box<dyn std::error::Error>> {
389        let mut button_matrix = ButtonMatrix::new();
390        for x in 0..W::to_usize() {
391            for y in 0..H::to_usize() {
392                if let Some(button) = &self.matrix[y][x] {
393                    let state = match button {
394                        CustomizableViewButton::Navigation { button, .. } => button,
395                        CustomizableViewButton::Button(button) => &button.get_state(),
396                    };
397                    button_matrix.set_button(x, y, *state)?;
398                }
399            }
400        }
401        Ok(button_matrix)
402    }
403    
404    async fn on_click(
405        &self,
406        context: &C,
407        index: u8,
408        navigation: Arc<mpsc::Sender<N>>,
409    ) -> Result<(), Box<dyn std::error::Error>> {
410        if (index as usize) < W::to_usize() * H::to_usize() {
411            let x = index % W::to_u8();
412            let y = index / W::to_u8();
413            if let Some(button) = &self.matrix[y as usize][x as usize] {
414                match button {
415                    CustomizableViewButton::Navigation { navigation: nav, .. } => {
416                        navigation.send(nav.clone()).await?;
417                    }
418                    CustomizableViewButton::Button(button) => {
419                        button.click(context).await?;
420                    }
421                }
422            }
423            Ok(())
424        } else {
425            return Err(Box::new(std::io::Error::new(
426                std::io::ErrorKind::InvalidInput,
427                "Button index out of bounds",
428            )));
429        }
430    }
431
432    async fn fetch_all(&self, context: &C) -> Result<(), Box<dyn std::error::Error>> {
433        for x in 0..W::to_usize() {
434            for y in 0..H::to_usize() {
435                if let Some(button) = &self.matrix[y][x] {
436                    match button {
437                        CustomizableViewButton::Navigation { .. } => {}
438                        CustomizableViewButton::Button(button) => {
439                            button.fetch(context).await?;
440                        }
441                    }
442                }
443            }
444        }
445        Ok(())
446    }
447}