Skip to main content

gpui_component/speech/
button.rs

1use gpui::{App, ElementId, Entity, IntoElement, RenderOnce, StyleRefinement, Styled, Window, div};
2use rust_i18n::t;
3
4use crate::{
5    Disableable, IconName, Selectable as _, Sizable, Size, StyledExt as _,
6    button::{Button, ButtonVariants as _},
7};
8
9use super::{SpeechState, SpeechStatus};
10
11/// The button that starts and stops a [`SpeechState`]'s session.
12///
13/// Shows a microphone at rest and a stop glyph, pressed, while capturing; a
14/// click toggles the session. While the final result is pending it shows a
15/// spinner and ignores clicks.
16///
17/// Renders nothing when the state has no recognizer on this platform, unless
18/// [`Self::show_when_unsupported`] is set, and renders disabled while the
19/// recognizer reports itself unavailable.
20#[derive(IntoElement)]
21pub struct SpeechButton {
22    id: ElementId,
23    state: Entity<SpeechState>,
24    size: Size,
25    disabled: bool,
26    show_when_unsupported: bool,
27    style: StyleRefinement,
28}
29
30impl SpeechButton {
31    /// A button for `state`.
32    pub fn new(state: &Entity<SpeechState>) -> Self {
33        Self {
34            id: ("speech-button", state.entity_id()).into(),
35            state: state.clone(),
36            size: Size::default(),
37            disabled: false,
38            show_when_unsupported: false,
39            style: StyleRefinement::default(),
40        }
41    }
42
43    /// Render a disabled button, instead of nothing, when the state has no
44    /// recognizer on this platform. Default `false`.
45    pub fn show_when_unsupported(mut self, show: bool) -> Self {
46        self.show_when_unsupported = show;
47        self
48    }
49}
50
51impl Sizable for SpeechButton {
52    fn with_size(mut self, size: impl Into<Size>) -> Self {
53        self.size = size.into();
54        self
55    }
56}
57
58impl Styled for SpeechButton {
59    /// Refines the button, e.g. its size or corner radius, to match the
60    /// controls around it.
61    fn style(&mut self) -> &mut StyleRefinement {
62        &mut self.style
63    }
64}
65
66impl Disableable for SpeechButton {
67    fn disabled(mut self, disabled: bool) -> Self {
68        self.disabled = disabled;
69        self
70    }
71}
72
73impl RenderOnce for SpeechButton {
74    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
75        let state = self.state.read(cx);
76        let status = state.status();
77        let supported = state.has_recognizer();
78        if !supported && !self.show_when_unsupported {
79            return div().into_any_element();
80        }
81        // A running session stays stoppable even if the recognizer turns
82        // unavailable mid-way.
83        let available = status.is_active() || state.is_available(cx);
84        let capturing = status.is_capturing();
85        let label = if !available {
86            t!("Speech.Unavailable")
87        } else if capturing {
88            t!("Speech.Stop")
89        } else {
90            t!("Speech.Start")
91        };
92
93        Button::new(self.id)
94            .ghost()
95            .with_size(self.size)
96            .icon(if capturing {
97                IconName::Square
98            } else {
99                IconName::Mic
100            })
101            .selected(capturing)
102            .loading(status == SpeechStatus::Stopping)
103            .disabled(self.disabled || !available)
104            .tooltip(label.clone())
105            .accessibility_label(label)
106            .refine_style(&self.style)
107            .on_click({
108                let state = self.state.clone();
109                move |_, _, cx| state.update(cx, |state, cx| state.toggle(cx))
110            })
111            .into_any_element()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use gpui::{AppContext as _, TestAppContext};
118
119    use super::*;
120
121    #[gpui::test]
122    fn test_speech_button_builder(cx: &mut TestAppContext) {
123        let state = cx.update(|cx| cx.new(|cx| SpeechState::new(cx).system_fallback(false)));
124        let button = SpeechButton::new(&state)
125            .small()
126            .disabled(true)
127            .show_when_unsupported(true);
128
129        assert_eq!(button.size, Size::Small);
130        assert!(button.disabled);
131        assert!(button.show_when_unsupported);
132        assert_eq!(
133            button.id,
134            ElementId::from(("speech-button", state.entity_id()))
135        );
136    }
137}