//! Preferences dialog: stream mode, bitrate, host compositor, gamepad type, microphone, //! capture behavior. Written back to disk when the dialog closes. use crate::trust::Settings; use adw::prelude::*; use std::cell::RefCell; use std::rc::Rc; /// `(0, 0)` = the native size of the monitor the window is on, resolved at connect. const RESOLUTIONS: &[(u32, u32)] = &[ (0, 0), (1280, 720), (1920, 1080), (2560, 1440), (3840, 2160), ]; /// `0` = the monitor's native refresh, resolved at connect. const REFRESH: &[u32] = &[0, 30, 60, 90, 120, 144, 165, 240]; const GAMEPADS: &[&str] = &["auto", "xbox360", "dualsense"]; const COMPOSITORS: &[&str] = &["auto", "kwin", "wlroots", "mutter", "gamescope"]; pub fn show( parent: &impl IsA, settings: Rc>, gamepads: &crate::gamepad::GamepadService, ) { let page = adw::PreferencesPage::new(); let stream = adw::PreferencesGroup::builder().title("Stream").build(); let res_names: Vec = RESOLUTIONS .iter() .map(|&(w, h)| { if w == 0 { "Native display".to_string() } else { format!("{w} × {h}") } }) .collect(); let res_row = adw::ComboRow::builder() .title("Resolution") .subtitle("The host creates a virtual output at exactly this size") .model(>k::StringList::new( &res_names.iter().map(String::as_str).collect::>(), )) .build(); let hz_names: Vec = REFRESH .iter() .map(|&r| { if r == 0 { "Native".to_string() } else { format!("{r} Hz") } }) .collect(); let hz_row = adw::ComboRow::builder() .title("Refresh rate") .model(>k::StringList::new( &hz_names.iter().map(String::as_str).collect::>(), )) .build(); let bitrate_row = adw::SpinRow::with_range(0.0, 3000.0, 5.0); bitrate_row.set_title("Bitrate"); bitrate_row.set_subtitle("Mbit/s · 0 = host default · run a speed test before going high"); let compositor_row = adw::ComboRow::builder() .title("Host compositor") .subtitle("Advisory — the host falls back to auto-detect when unavailable") .model(>k::StringList::new(&[ "Automatic", "KWin", "wlroots (Sway/Hyprland)", "Mutter (GNOME)", "gamescope", ])) .build(); stream.add(&res_row); stream.add(&hz_row); stream.add(&bitrate_row); stream.add(&compositor_row); let input = adw::PreferencesGroup::builder().title("Input").build(); // Which physical controller forwards as pad 0: automatic = the most recently // connected; pinning survives until the app exits (Swift parity). let pads = gamepads.pads(); let mut pad_names = vec!["Automatic (most recent)".to_string()]; pad_names.extend(pads.iter().map(|p| { if p.is_dualsense { format!("{} · DualSense", p.name) } else { p.name.clone() } })); let forward_row = adw::ComboRow::builder() .title("Forwarded controller") .subtitle(if pads.is_empty() { "No controllers detected" } else { "Exactly one controller is forwarded to the host" }) .model(>k::StringList::new( &pad_names.iter().map(String::as_str).collect::>(), )) .build(); let pinned_i = gamepads .pinned() .and_then(|id| pads.iter().position(|p| p.id == id)) .map_or(0, |i| i + 1); forward_row.set_selected(pinned_i as u32); { let svc = gamepads.clone(); let ids: Vec = pads.iter().map(|p| p.id).collect(); forward_row.connect_selected_notify(move |row| { let sel = row.selected() as usize; svc.set_pinned(if sel == 0 { None } else { ids.get(sel - 1).copied() }); }); } let pad_row = adw::ComboRow::builder() .title("Gamepad type") .subtitle("The virtual pad the host creates — Automatic matches the physical pad") .model(>k::StringList::new(&[ "Automatic", "Xbox 360", "DualSense", ])) .build(); let inhibit_row = adw::SwitchRow::builder() .title("Capture system shortcuts") .subtitle("Forward Alt+Tab, Super, … to the host while input is captured") .build(); input.add(&forward_row); input.add(&pad_row); input.add(&inhibit_row); let audio = adw::PreferencesGroup::builder().title("Audio").build(); let mic_row = adw::SwitchRow::builder() .title("Stream microphone") .subtitle("Send the default input device to the host's virtual microphone") .build(); audio.add(&mic_row); page.add(&stream); page.add(&input); page.add(&audio); // Seed from the current settings. { let s = settings.borrow(); let res_i = RESOLUTIONS .iter() .position(|&(w, h)| w == s.width && h == s.height) .unwrap_or(0); res_row.set_selected(res_i as u32); let hz_i = REFRESH.iter().position(|&r| r == s.refresh_hz).unwrap_or(0); hz_row.set_selected(hz_i as u32); bitrate_row.set_value(f64::from(s.bitrate_kbps) / 1000.0); let pad_i = GAMEPADS.iter().position(|&g| g == s.gamepad).unwrap_or(0); pad_row.set_selected(pad_i as u32); let comp_i = COMPOSITORS .iter() .position(|&c| c == s.compositor) .unwrap_or(0); compositor_row.set_selected(comp_i as u32); inhibit_row.set_active(s.inhibit_shortcuts); mic_row.set_active(s.mic_enabled); } let dialog = adw::PreferencesDialog::new(); dialog.set_title("Preferences"); dialog.add(&page); dialog.connect_closed(move |_| { let mut s = settings.borrow_mut(); let (w, h) = RESOLUTIONS[(res_row.selected() as usize).min(RESOLUTIONS.len() - 1)]; (s.width, s.height) = (w, h); s.refresh_hz = REFRESH[(hz_row.selected() as usize).min(REFRESH.len() - 1)]; s.bitrate_kbps = (bitrate_row.value() * 1000.0) as u32; s.gamepad = GAMEPADS[(pad_row.selected() as usize).min(GAMEPADS.len() - 1)].to_string(); s.compositor = COMPOSITORS[(compositor_row.selected() as usize).min(COMPOSITORS.len() - 1)] .to_string(); s.inhibit_shortcuts = inhibit_row.is_active(); s.mic_enabled = mic_row.is_active(); s.save(); }); dialog.present(Some(parent)); }