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