gpui_component/speech/
button.rs1use std::sync::Arc;
2
3use gpui::{
4 App, ElementId, Entity, IntoElement, ParentElement as _, RenderOnce, StyleRefinement, Styled,
5 Window, div, prelude::FluentBuilder as _, px,
6};
7use rust_i18n::t;
8
9use crate::{
10 Disableable, IconName, Selectable as _, Sizable, Size, StyledExt as _,
11 button::{Button, ButtonVariants as _},
12};
13
14use super::{SpeechIndicator, SpeechState, SpeechStatus};
15
16#[derive(IntoElement)]
28pub struct SpeechButton {
29 id: ElementId,
30 state: Entity<SpeechState>,
31 size: Size,
32 disabled: bool,
33 show_when_unsupported: bool,
34 style: StyleRefinement,
35}
36
37impl SpeechButton {
38 pub fn new(state: &Entity<SpeechState>) -> Self {
40 Self {
41 id: ("speech-button", state.entity_id()).into(),
42 state: state.clone(),
43 size: Size::default(),
44 disabled: false,
45 show_when_unsupported: false,
46 style: StyleRefinement::default(),
47 }
48 }
49
50 pub fn show_when_unsupported(mut self, show: bool) -> Self {
53 self.show_when_unsupported = show;
54 self
55 }
56}
57
58impl Sizable for SpeechButton {
59 fn with_size(mut self, size: impl Into<Size>) -> Self {
60 self.size = size.into();
61 self
62 }
63}
64
65impl Styled for SpeechButton {
66 fn style(&mut self) -> &mut StyleRefinement {
69 &mut self.style
70 }
71}
72
73impl Disableable for SpeechButton {
74 fn disabled(mut self, disabled: bool) -> Self {
75 self.disabled = disabled;
76 self
77 }
78}
79
80impl RenderOnce for SpeechButton {
81 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
82 let focus_handle = window
86 .use_keyed_state(
87 ElementId::NamedChild(Arc::new(self.id.clone()), "focus".into()),
88 cx,
89 |_, cx| cx.focus_handle(),
90 )
91 .read(cx)
92 .clone();
93 let keyboard_focused = focus_handle.is_focused(window) && window.last_input_was_keyboard();
94
95 let state = self.state.read(cx);
96 let status = state.status();
97 let supported = state.has_recognizer();
98 if !supported && !self.show_when_unsupported {
99 return div().into_any_element();
100 }
101 let available = status.is_active() || state.is_available(cx);
104 let capturing = status.is_capturing();
105 let label = if !available {
106 t!("Speech.Unavailable")
107 } else if capturing {
108 t!("Speech.Stop")
109 } else {
110 t!("Speech.Start")
111 };
112
113 let stopping = status == SpeechStatus::Stopping;
114
115 Button::new(self.id)
116 .ghost()
117 .with_size(self.size)
118 .track_focus(&focus_handle)
119 .map(|this| {
120 if capturing && !keyboard_focused {
121 let (side, icon) = match self.size {
125 Size::Size(side) => (side, Size::Size(side * 0.75)),
126 Size::XSmall => (px(20.), Size::XSmall),
127 Size::Small => (px(24.), Size::Small),
128 size => (px(32.), size),
129 };
130 this.size(side)
131 .p_0()
132 .child(SpeechIndicator::new(&self.state).with_size(icon))
133 } else {
134 this.icon(if capturing || stopping {
135 IconName::Square
136 } else {
137 IconName::Mic
138 })
139 }
140 })
141 .selected(capturing)
142 .loading(stopping)
143 .disabled(self.disabled || !available)
144 .tooltip(label.clone())
145 .accessibility_label(label)
146 .refine_style(&self.style)
147 .on_click({
148 let state = self.state.clone();
149 move |_, _, cx| state.update(cx, |state, cx| state.toggle(cx))
150 })
151 .into_any_element()
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use gpui::{AppContext as _, TestAppContext};
158
159 use super::*;
160
161 #[gpui::test]
162 fn test_speech_button_builder(cx: &mut TestAppContext) {
163 let state = cx.update(|cx| cx.new(|cx| SpeechState::new(cx).system_fallback(false)));
164 let button = SpeechButton::new(&state)
165 .small()
166 .disabled(true)
167 .show_when_unsupported(true);
168
169 assert_eq!(button.size, Size::Small);
170 assert!(button.disabled);
171 assert!(button.show_when_unsupported);
172 assert_eq!(
173 button.id,
174 ElementId::from(("speech-button", state.entity_id()))
175 );
176 }
177}