{ let device_refresh_button = gtk::Button::with_label("Refresh Devices"); stabilize_button(&device_refresh_button, 132); device_refresh_button.set_tooltip_text(Some("Re-scan connected devices.")); let (devices_panel, devices_body) = build_panel_with_action("Device Staging", Some(device_refresh_button.upcast_ref())); devices_panel.set_hexpand(false); devices_panel.set_size_request(DEVICE_STAGING_PANEL_WIDTH, -1); devices_panel.set_vexpand(true); devices_panel.set_valign(gtk::Align::Fill); devices_body.set_vexpand(true); devices_body.set_spacing(8); let control_group = build_subgroup("Control Inputs"); let control_stack = gtk::Box::new(gtk::Orientation::Vertical, 10); control_group.append(&control_stack); let camera_combo = gtk::ComboBoxText::new(); camera_combo.add_css_class("compact-combo"); install_device_choice_combo_behavior(&camera_combo); sync_stage_device_combo( &camera_combo, &catalog.cameras, state.devices.camera.as_deref(), ); let camera_quality_combo = gtk::ComboBoxText::new(); camera_quality_combo.add_css_class("compact-combo"); sync_camera_quality_combo( &camera_quality_combo, &state.camera_quality_options(catalog), state.selected_camera_quality(catalog), ); camera_quality_combo.set_size_request(88, -1); camera_quality_combo.set_tooltip_text(Some("Webcam uplink quality.")); let camera_test_button = gtk::Button::with_label("Start Preview"); stabilize_button(&camera_test_button, 118); camera_test_button.set_tooltip_text(Some("Preview selected webcam locally.")); let speaker_combo = gtk::ComboBoxText::new(); speaker_combo.add_css_class("compact-combo"); install_device_choice_combo_behavior(&speaker_combo); sync_stage_device_combo( &speaker_combo, &catalog.speakers, state.devices.speaker.as_deref(), ); let speaker_test_button = gtk::Button::with_label("Play Tone"); stabilize_button(&speaker_test_button, 118); speaker_test_button.set_tooltip_text(Some("Play a local test tone.")); let keyboard_combo = gtk::ComboBoxText::new(); keyboard_combo.add_css_class("compact-combo"); install_device_choice_combo_behavior(&keyboard_combo); keyboard_combo.append(Some("all"), "all keyboards"); for keyboard in &catalog.keyboards { append_input_choice(&keyboard_combo, keyboard); } super::ui_runtime::set_combo_active_text(&keyboard_combo, state.devices.keyboard.as_deref()); keyboard_combo.set_tooltip_text(Some("Keyboard source for relay input.")); let keyboard_row = build_inline_selector_row("Keyboard", &keyboard_combo); control_stack.append(&keyboard_row); let mouse_combo = gtk::ComboBoxText::new(); mouse_combo.add_css_class("compact-combo"); install_device_choice_combo_behavior(&mouse_combo); mouse_combo.append(Some("all"), "all mice"); for mouse in &catalog.mice { append_input_choice(&mouse_combo, mouse); } super::ui_runtime::set_combo_active_text(&mouse_combo, state.devices.mouse.as_deref()); mouse_combo.set_tooltip_text(Some("Pointer source for relay input.")); let mouse_row = build_inline_selector_row("Mouse", &mouse_combo); control_stack.append(&mouse_row); devices_body.append(&control_group); let media_group = build_subgroup("Media Controls"); media_group.set_vexpand(true); media_group.set_valign(gtk::Align::Fill); let media_grid = gtk::Grid::new(); media_grid.set_row_spacing(10); media_grid.set_column_spacing(8); media_group.append(&media_grid); let camera_channel_toggle = gtk::ToggleButton::with_label("Camera"); camera_channel_toggle.set_active(state.channels.camera); camera_channel_toggle.set_tooltip_text(Some("Send webcam during relay.")); camera_channel_toggle.add_css_class("pill-toggle"); camera_channel_toggle.add_css_class("media-toggle"); stabilize_button(&camera_channel_toggle, 92); let audio_channel_toggle = gtk::ToggleButton::with_label("Speaker"); audio_channel_toggle.set_active(state.channels.audio); audio_channel_toggle.set_tooltip_text(Some("Play remote audio here.")); audio_channel_toggle.add_css_class("pill-toggle"); audio_channel_toggle.add_css_class("media-toggle"); stabilize_button(&audio_channel_toggle, 92); let microphone_channel_toggle = gtk::ToggleButton::with_label("Mic"); microphone_channel_toggle.set_active(state.channels.microphone); microphone_channel_toggle.set_tooltip_text(Some("Send mic during relay.")); microphone_channel_toggle.add_css_class("pill-toggle"); microphone_channel_toggle.add_css_class("media-toggle"); microphone_channel_toggle.add_css_class("media-toggle-split"); stabilize_button(µphone_channel_toggle, 44); let noise_suppression_toggle = gtk::ToggleButton::with_label("🧹"); noise_suppression_toggle.set_active(state.mic_noise_suppression); noise_suppression_toggle.set_tooltip_text(Some(if state.mic_noise_suppression { "Noise cancellation is on for the upstream microphone." } else { "Noise cancellation is off; upstream microphone is raw aside from gain." })); noise_suppression_toggle.add_css_class("pill-toggle"); noise_suppression_toggle.add_css_class("media-toggle"); noise_suppression_toggle.add_css_class("media-toggle-split"); stabilize_button(&noise_suppression_toggle, 44); let audio_gain_adjustment = gtk::Adjustment::new( f64::from(state.audio_gain_percent), 0.0, f64::from(super::state::MAX_AUDIO_GAIN_PERCENT), 25.0, 100.0, 0.0, ); let audio_gain_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&audio_gain_adjustment)); audio_gain_scale.set_draw_value(false); audio_gain_scale.set_hexpand(false); audio_gain_scale.set_size_request(96, -1); audio_gain_scale.set_tooltip_text(Some("Speaker volume. Double-click resets to 200%.")); attach_scale_reset_gesture( &audio_gain_scale, f64::from(super::state::DEFAULT_AUDIO_GAIN_PERCENT), ); let audio_gain_value = gtk::Label::new(Some(&state.audio_gain_label())); audio_gain_value.set_visible(false); let mic_gain_adjustment = gtk::Adjustment::new( f64::from(state.mic_gain_percent), 0.0, f64::from(super::state::MAX_MIC_GAIN_PERCENT), 25.0, 100.0, 0.0, ); let mic_gain_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&mic_gain_adjustment)); mic_gain_scale.set_draw_value(false); mic_gain_scale.set_hexpand(false); mic_gain_scale.set_size_request(96, -1); mic_gain_scale.set_tooltip_text(Some("Mic gain. Double-click resets to 100%.")); attach_scale_reset_gesture( &mic_gain_scale, f64::from(super::state::DEFAULT_MIC_GAIN_PERCENT), ); let mic_gain_value = gtk::Label::new(Some(&state.mic_gain_label())); mic_gain_value.set_visible(false); camera_combo.set_size_request(0, -1); let camera_selectors = gtk::Box::new(gtk::Orientation::Horizontal, 6); camera_combo.set_hexpand(true); camera_quality_combo.set_hexpand(false); camera_selectors.append(&camera_combo); camera_selectors.append(&camera_quality_combo); speaker_combo.set_size_request(0, -1); attach_device_control_row( &media_grid, 0, &camera_channel_toggle, &camera_selectors, &camera_test_button, ); let speaker_selectors = gtk::Box::new(gtk::Orientation::Horizontal, 6); speaker_combo.set_hexpand(true); speaker_selectors.append(&speaker_combo); speaker_selectors.append(&audio_gain_scale); attach_device_control_row( &media_grid, 1, &audio_channel_toggle, &speaker_selectors, &speaker_test_button, ); let microphone_combo = gtk::ComboBoxText::new(); microphone_combo.add_css_class("compact-combo"); install_device_choice_combo_behavior(µphone_combo); sync_stage_device_combo( µphone_combo, &catalog.microphones, state.devices.microphone.as_deref(), ); let microphone_test_button = gtk::Button::with_label("Monitor Mic"); stabilize_button(µphone_test_button, 118); microphone_test_button.set_tooltip_text(Some("Monitor mic through speaker.")); microphone_combo.set_size_request(0, -1); let microphone_selectors = gtk::Box::new(gtk::Orientation::Horizontal, 6); microphone_combo.set_hexpand(true); microphone_selectors.append(µphone_combo); microphone_selectors.append(&mic_gain_scale); let microphone_toggle_group = gtk::Box::new(gtk::Orientation::Horizontal, 4); microphone_toggle_group.set_homogeneous(true); microphone_toggle_group.set_size_request(92, -1); microphone_toggle_group.append(µphone_channel_toggle); microphone_toggle_group.append(&noise_suppression_toggle); attach_device_control_row( &media_grid, 2, µphone_toggle_group, µphone_selectors, µphone_test_button, ); let audio_check_detail = gtk::Label::new(Some("Idle")); audio_check_detail.add_css_class("dim-label"); audio_check_detail.set_wrap(false); audio_check_detail.set_ellipsize(pango::EllipsizeMode::End); audio_check_detail.set_xalign(0.0); audio_check_detail.set_visible(false); let audio_check_meter = gtk::ProgressBar::new(); audio_check_meter.add_css_class("audio-check-meter"); audio_check_meter.set_show_text(false); devices_body.append(&media_group); staging_row.append(&devices_panel); let upstream_transport_row = gtk::Box::new(gtk::Orientation::Horizontal, 6); upstream_transport_row.set_halign(gtk::Align::End); let webcam_transport_combo = gtk::ComboBoxText::new(); webcam_transport_combo.add_css_class("compact-combo"); for transport in [WebcamTransport::Mjpeg, WebcamTransport::Hevc] { webcam_transport_combo.append(Some(transport.as_id()), transport.label()); } webcam_transport_combo.set_active_id(Some(state.effective_webcam_transport().as_id())); webcam_transport_combo.set_sensitive(true); webcam_transport_combo.set_size_request(98, -1); webcam_transport_combo.set_tooltip_text(Some( "Upstream webcam transport for the next relay connection. MJPEG is the safe calibrated default; HEVC is selectable for hardware-accelerated testing.", )); let upstream_audio_transport_combo = gtk::ComboBoxText::new(); upstream_audio_transport_combo.add_css_class("compact-combo"); for transport in [UpstreamAudioTransport::Pcm, UpstreamAudioTransport::Opus] { upstream_audio_transport_combo.append(Some(transport.as_id()), transport.label()); } upstream_audio_transport_combo.set_active_id(Some(state.upstream_audio_transport.as_id())); upstream_audio_transport_combo.set_sensitive(true); upstream_audio_transport_combo.set_size_request(88, -1); upstream_audio_transport_combo.set_tooltip_text(Some( "Upstream microphone transport for the live relay. PCM is the known-good default; Opus is compressed and experimental.", )); upstream_transport_row.append(&webcam_transport_combo); upstream_transport_row.append(&upstream_audio_transport_combo); let (preview_panel, preview_body) = build_panel_with_action("Upstream Media", Some(upstream_transport_row.upcast_ref())); preview_panel.set_hexpand(true); preview_panel.set_vexpand(true); preview_panel.set_valign(gtk::Align::Fill); preview_body.set_hexpand(true); preview_body.set_vexpand(true); preview_body.set_spacing(8); let testing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); testing_row.set_hexpand(true); testing_row.set_vexpand(true); testing_row.set_valign(gtk::Align::Fill); let camera_preview = gtk::Picture::new(); camera_preview.set_can_shrink(true); camera_preview.set_hexpand(true); camera_preview.set_vexpand(true); camera_preview.set_halign(gtk::Align::Fill); camera_preview.set_valign(gtk::Align::Fill); camera_preview.set_size_request( CAMERA_PREVIEW_VIEWPORT_WIDTH, CAMERA_PREVIEW_VIEWPORT_HEIGHT, ); camera_preview.set_keep_aspect_ratio(true); camera_preview.add_css_class("camera-preview-frame"); let camera_preview_idle = gtk::Picture::new(); camera_preview_idle.set_can_shrink(true); camera_preview_idle.set_hexpand(true); camera_preview_idle.set_vexpand(true); camera_preview_idle.set_halign(gtk::Align::Fill); camera_preview_idle.set_valign(gtk::Align::Fill); camera_preview_idle.set_keep_aspect_ratio(true); camera_preview_idle.add_css_class("display-placeholder-art"); camera_preview_idle.set_paintable(Some(&super::device_test::camera_preview_placeholder_texture())); let camera_mirror_button = gtk::ToggleButton::new(); camera_mirror_button.add_css_class("camera-preview-mirror-toggle"); camera_mirror_button.add_css_class("flat"); camera_mirror_button.set_focus_on_click(false); camera_mirror_button.set_halign(gtk::Align::End); camera_mirror_button.set_valign(gtk::Align::Start); camera_mirror_button.set_margin_top(10); camera_mirror_button.set_margin_end(10); camera_mirror_button.set_tooltip_text(Some("Mirror launcher preview only.")); camera_mirror_button.set_visible(false); let camera_mirror_icon = gtk::Image::from_icon_name("object-flip-horizontal-symbolic"); camera_mirror_button.set_child(Some(&camera_mirror_icon)); let camera_mirror_revealer = gtk::Revealer::new(); camera_mirror_revealer.set_transition_type(gtk::RevealerTransitionType::Crossfade); camera_mirror_revealer.set_transition_duration(120); camera_mirror_revealer.set_halign(gtk::Align::End); camera_mirror_revealer.set_valign(gtk::Align::Start); camera_mirror_revealer.set_reveal_child(false); camera_mirror_revealer.set_child(Some(&camera_mirror_button)); let camera_status = gtk::Label::new(Some("Select a webcam and click Start Preview.")); camera_status.add_css_class("dim-label"); camera_status.set_wrap(false); camera_status.set_ellipsize(pango::EllipsizeMode::End); camera_status.set_xalign(0.0); camera_status.set_visible(false); let camera_preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 0); camera_preview_shell.set_hexpand(true); camera_preview_shell.set_vexpand(true); camera_preview_shell.set_halign(gtk::Align::Fill); camera_preview_shell.set_valign(gtk::Align::Fill); camera_preview_shell.set_size_request( CAMERA_PREVIEW_VIEWPORT_WIDTH, CAMERA_PREVIEW_VIEWPORT_HEIGHT, ); let camera_preview_frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false); camera_preview_frame.set_hexpand(true); camera_preview_frame.set_vexpand(true); camera_preview_frame.set_halign(gtk::Align::Fill); camera_preview_frame.set_valign(gtk::Align::Fill); camera_preview_frame.set_size_request( CAMERA_PREVIEW_VIEWPORT_WIDTH, CAMERA_PREVIEW_VIEWPORT_HEIGHT, ); camera_preview_frame.set_child(Some(&camera_preview)); let camera_preview_overlay = gtk::Overlay::new(); camera_preview_overlay.set_hexpand(true); camera_preview_overlay.set_vexpand(true); camera_preview_overlay.set_halign(gtk::Align::Fill); camera_preview_overlay.set_valign(gtk::Align::Fill); camera_preview_overlay.set_size_request( CAMERA_PREVIEW_VIEWPORT_WIDTH, CAMERA_PREVIEW_VIEWPORT_HEIGHT, ); camera_preview_overlay.set_child(Some(&camera_preview_frame)); camera_preview_overlay.add_overlay(&camera_mirror_revealer); let hover_revealer = camera_mirror_revealer.clone(); let hover_button = camera_mirror_button.clone(); let hover_controller = gtk::EventControllerMotion::new(); hover_controller.connect_enter(move |_, _, _| { if hover_button.is_visible() { hover_revealer.set_reveal_child(true); } }); let leave_revealer = camera_mirror_revealer.clone(); let leave_button = camera_mirror_button.clone(); hover_controller.connect_leave(move |_| { if leave_button.is_visible() { leave_revealer.set_reveal_child(false); } }); camera_preview_overlay.add_controller(hover_controller); let camera_preview_idle_frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false); camera_preview_idle_frame.set_hexpand(true); camera_preview_idle_frame.set_vexpand(true); camera_preview_idle_frame.set_halign(gtk::Align::Fill); camera_preview_idle_frame.set_valign(gtk::Align::Fill); camera_preview_idle_frame.set_size_request( CAMERA_PREVIEW_VIEWPORT_WIDTH, CAMERA_PREVIEW_VIEWPORT_HEIGHT, ); camera_preview_idle_frame.set_child(Some(&camera_preview_idle)); let camera_preview_idle_shell = gtk::Box::new(gtk::Orientation::Vertical, 0); camera_preview_idle_shell.set_hexpand(true); camera_preview_idle_shell.set_vexpand(true); camera_preview_idle_shell.set_halign(gtk::Align::Fill); camera_preview_idle_shell.set_valign(gtk::Align::Fill); camera_preview_idle_shell.append(&camera_preview_idle_frame); let camera_preview_stack = gtk::Stack::new(); camera_preview_stack.set_hexpand(true); camera_preview_stack.set_vexpand(true); camera_preview_stack.set_halign(gtk::Align::Fill); camera_preview_stack.set_valign(gtk::Align::Fill); camera_preview_stack.set_size_request( CAMERA_PREVIEW_VIEWPORT_WIDTH, CAMERA_PREVIEW_VIEWPORT_HEIGHT, ); camera_preview_stack.add_named(&camera_preview_idle_shell, Some("idle")); camera_preview_stack.add_named(&camera_preview_overlay, Some("live")); camera_preview_stack.set_visible_child_name("idle"); camera_preview_shell.append(&camera_preview_stack); let webcam_group = build_subgroup("Webcam Preview"); webcam_group.set_hexpand(true); webcam_group.set_vexpand(true); webcam_group.set_valign(gtk::Align::Fill); webcam_group.append(&camera_preview_shell); testing_row.append(&webcam_group); let playback_group = build_subgroup("Mic Playback"); playback_group.set_hexpand(false); playback_group.set_vexpand(true); playback_group.set_valign(gtk::Align::Fill); playback_group.set_size_request(72, -1); let playback_body = gtk::Box::new(gtk::Orientation::Vertical, 6); playback_body.set_halign(gtk::Align::Center); playback_body.set_vexpand(true); playback_body.set_valign(gtk::Align::Fill); let microphone_replay_button = gtk::Button::with_label("Replay"); stabilize_button(µphone_replay_button, 70); audio_check_meter.set_orientation(gtk::Orientation::Vertical); audio_check_meter.set_inverted(true); audio_check_meter.set_hexpand(false); audio_check_meter.set_vexpand(true); audio_check_meter.set_halign(gtk::Align::Center); audio_check_meter.set_size_request(20, 0); audio_check_meter.set_show_text(false); audio_check_meter.set_text(Some("Idle")); playback_body.append(&audio_check_meter); playback_body.append(µphone_replay_button); playback_group.append(&playback_body); testing_row.append(&playback_group); preview_body.append(&testing_row); staging_row.append(&preview_panel); DeviceControlsContext { device_refresh_button, camera_combo, camera_quality_combo, microphone_combo, speaker_combo, keyboard_combo, mouse_combo, camera_channel_toggle, microphone_channel_toggle, noise_suppression_toggle, audio_channel_toggle, audio_gain_scale, audio_gain_value, mic_gain_scale, mic_gain_value, audio_check_detail, audio_check_meter, devices_panel, preview_panel, camera_preview_stack, camera_preview_frame, camera_preview, webcam_transport_combo, upstream_audio_transport_combo, camera_mirror_button, camera_mirror_revealer, camera_status, camera_test_button, microphone_test_button, microphone_replay_button, speaker_test_button, } }