1use audio::{AudioDeviceInfo, AvailableAudioDevices};
2use cpal::DeviceId;
3use gpui::{AnyElement, App, ElementId, ReadGlobal, SharedString, Window};
4use settings::{AudioInputDeviceName, AudioOutputDeviceName, SettingsStore};
5use std::str::FromStr;
6use ui::{ContextMenu, DropdownMenu, DropdownStyle, IconPosition, IntoElement};
7use util::ResultExt;
8
9use crate::{SettingField, SettingsFieldMetadata, SettingsUiFile, update_settings_file};
10
11pub(crate) const SYSTEM_DEFAULT: &str = "System Default";
12
13pub(crate) fn get_current_device(
14 current_id: Option<&DeviceId>,
15 is_input: bool,
16 devices: &[AudioDeviceInfo],
17) -> Option<AudioDeviceInfo> {
18 let Some(current_id) = current_id else {
19 return None;
20 };
21 devices
22 .iter()
23 .find(|d| d.matches(current_id, is_input))
24 .cloned()
25}
26
27pub(crate) fn render_audio_device_dropdown<F>(
28 dropdown_id: impl Into<ElementId>,
29 current_device_id: Option<DeviceId>,
30 is_input: bool,
31 on_select: F,
32 window: &mut Window,
33 cx: &mut App,
34) -> AnyElement
35where
36 F: Fn(Option<DeviceId>, &mut Window, &mut App) + Clone + 'static,
37{
38 let devices = cx.default_global::<AvailableAudioDevices>().0.clone();
39 let current_device = get_current_device(current_device_id.as_ref(), is_input, &devices);
40
41 let menu = ContextMenu::build(window, cx, {
42 let current_device = current_device.clone();
43 move |mut menu, _, _cx| {
44 let is_system_default = current_device.is_none();
45 menu = menu.toggleable_entry(
46 SYSTEM_DEFAULT,
47 is_system_default,
48 IconPosition::Start,
49 None,
50 {
51 let on_select = on_select.clone();
52 move |window, cx| {
53 on_select(None, window, cx);
54 }
55 },
56 );
57
58 for device in devices.iter().filter(|d| d.matches_input(is_input)) {
59 let is_current = current_device
60 .as_ref()
61 .map(|info| info.matches(&device.id, is_input))
62 .unwrap_or(false);
63 let device_id = device.id.clone();
64
65 menu = menu.toggleable_entry(
66 device.to_string(),
67 is_current,
68 IconPosition::Start,
69 None,
70 {
71 let on_select = on_select.clone();
72 move |window, cx| {
73 on_select(Some(device_id.clone()), window, cx);
74 }
75 },
76 );
77 }
78 menu
79 }
80 });
81
82 DropdownMenu::new(
83 dropdown_id,
84 current_device
85 .map(|info| info.desc.name().to_string())
86 .unwrap_or(SYSTEM_DEFAULT.to_string()),
87 menu,
88 )
89 .style(DropdownStyle::Outlined)
90 .full_width(true)
91 .into_any_element()
92}
93
94fn render_settings_audio_device_dropdown<T: AsRef<Option<String>> + From<Option<String>> + Send>(
95 field: SettingField<T>,
96 file: SettingsUiFile,
97 is_input: bool,
98 window: &mut Window,
99 cx: &mut App,
100) -> AnyElement {
101 let (_, current_value): (_, Option<&T>) =
102 SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
103 let current_device_id =
104 current_value.and_then(|x| x.as_ref().clone().and_then(|x| DeviceId::from_str(&x).ok()));
105
106 let dropdown_id: SharedString = if is_input {
107 "input-audio-device-dropdown".into()
108 } else {
109 "output-audio-device-dropdown".into()
110 };
111
112 render_audio_device_dropdown(
113 dropdown_id,
114 current_device_id,
115 is_input,
116 move |device_id, window, cx| {
117 let value: Option<T> = device_id.map(|id| T::from(Some(id.to_string())));
118 update_settings_file(
119 file.clone(),
120 field.json_path,
121 window,
122 cx,
123 move |settings, _cx| {
124 (field.write)(settings, value);
125 },
126 )
127 .log_err();
128 },
129 window,
130 cx,
131 )
132}
133
134pub fn render_input_audio_device_dropdown(
135 field: SettingField<AudioInputDeviceName>,
136 file: SettingsUiFile,
137 _metadata: Option<&SettingsFieldMetadata>,
138 window: &mut Window,
139 cx: &mut App,
140) -> AnyElement {
141 render_settings_audio_device_dropdown(field, file, true, window, cx)
142}
143
144pub fn render_output_audio_device_dropdown(
145 field: SettingField<AudioOutputDeviceName>,
146 file: SettingsUiFile,
147 _metadata: Option<&SettingsFieldMetadata>,
148 window: &mut Window,
149 cx: &mut App,
150) -> AnyElement {
151 render_settings_audio_device_dropdown(field, file, false, window, cx)
152}