Skip to main content

screen_app/tray/
bubble_toggle.rs

1//! Webcam-bubble visibility state machine (M-BUBBLE.0 / AUT-273).
2//!
3//! Mirrors the [`super::toggle::TrayPopoverState`] shape — a pure-Rust
4//! `Hidden ↔ Visible` state machine that returns an action enum so the
5//! caller in `commands.rs` is responsible for the Tauri `show()` /
6//! `hide()` calls. Splitting state from I/O keeps the transition logic
7//! cross-OS-testable without a Tauri runtime (CLAUDE.md "Tauri 2
8//! `mock_builder` aborts at list-time on Windows").
9
10/// Whether the `webcam-bubble` window is currently shown to the user.
11///
12/// The state lives in `tauri::Manager`-managed storage; this enum is
13/// the source of truth for the "Show webcam bubble" toggle button in
14/// the Recorder surface. `Default` is `Hidden` (matching
15/// `tauri.conf.json`'s `visible: false`).
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub enum BubbleVisibility {
18    /// Bubble window is hidden; next toggle shows it.
19    #[default]
20    Hidden,
21    /// Bubble window is visible; next toggle hides it.
22    Visible,
23}
24
25/// The action the toggle handler should perform after observing a
26/// user click on "Show webcam bubble." Returning an enum (rather than
27/// mutating Tauri windows directly here) keeps this module free of
28/// Tauri types so the unit tests don't need a runtime.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum BubbleAction {
31    /// Caller should `window.show()` (and not `set_focus()` — the
32    /// bubble is meant to float as a peripheral, not steal focus from
33    /// the `AppShell`).
34    Show,
35    /// Caller should `window.hide()`.
36    Hide,
37}
38
39impl BubbleVisibility {
40    /// Advance the state machine by one click; return the action the
41    /// caller must perform.
42    pub fn on_click(&mut self) -> BubbleAction {
43        match *self {
44            Self::Hidden => {
45                *self = Self::Visible;
46                BubbleAction::Show
47            }
48            Self::Visible => {
49                *self = Self::Hidden;
50                BubbleAction::Hide
51            }
52        }
53    }
54
55    /// Align the state to `visible`, returning the action the caller
56    /// should perform — or `None` when already in the requested state.
57    ///
58    /// Distinct from [`Self::on_click`] (which always flips). Used by
59    /// callers that own their own source of truth for the desired
60    /// visibility (e.g. the recorder's `camera_enabled` signal) and
61    /// need lockstep alignment without depending on the state
62    /// machine's prior position. ISS-05.
63    pub fn set(&mut self, visible: bool) -> Option<BubbleAction> {
64        match (*self, visible) {
65            (Self::Hidden, true) => {
66                *self = Self::Visible;
67                Some(BubbleAction::Show)
68            }
69            (Self::Visible, false) => {
70                *self = Self::Hidden;
71                Some(BubbleAction::Hide)
72            }
73            (Self::Hidden, false) | (Self::Visible, true) => None,
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn default_is_hidden() {
84        assert_eq!(BubbleVisibility::default(), BubbleVisibility::Hidden);
85    }
86
87    #[test]
88    fn hidden_click_yields_show_and_becomes_visible() {
89        let mut s = BubbleVisibility::Hidden;
90        let action = s.on_click();
91        assert_eq!(action, BubbleAction::Show);
92        assert_eq!(s, BubbleVisibility::Visible);
93    }
94
95    #[test]
96    fn visible_click_yields_hide_and_becomes_hidden() {
97        let mut s = BubbleVisibility::Visible;
98        let action = s.on_click();
99        assert_eq!(action, BubbleAction::Hide);
100        assert_eq!(s, BubbleVisibility::Hidden);
101    }
102
103    #[test]
104    fn ten_alternating_clicks_round_trip() {
105        let mut s = BubbleVisibility::Hidden;
106        for i in 0..10 {
107            let action = s.on_click();
108            if i % 2 == 0 {
109                assert_eq!(action, BubbleAction::Show);
110                assert_eq!(s, BubbleVisibility::Visible);
111            } else {
112                assert_eq!(action, BubbleAction::Hide);
113                assert_eq!(s, BubbleVisibility::Hidden);
114            }
115        }
116        assert_eq!(s, BubbleVisibility::Hidden);
117    }
118
119    #[test]
120    fn set_true_from_hidden_yields_show() {
121        let mut s = BubbleVisibility::Hidden;
122        assert_eq!(s.set(true), Some(BubbleAction::Show));
123        assert_eq!(s, BubbleVisibility::Visible);
124    }
125
126    #[test]
127    fn set_false_from_visible_yields_hide() {
128        let mut s = BubbleVisibility::Visible;
129        assert_eq!(s.set(false), Some(BubbleAction::Hide));
130        assert_eq!(s, BubbleVisibility::Hidden);
131    }
132
133    #[test]
134    fn set_to_current_state_is_a_noop() {
135        let mut hidden = BubbleVisibility::Hidden;
136        assert_eq!(hidden.set(false), None);
137        let mut visible = BubbleVisibility::Visible;
138        assert_eq!(visible.set(true), None);
139    }
140}