{ 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(true); devices_panel.set_vexpand(false); devices_panel.set_valign(gtk::Align::Fill); 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"); 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"); 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"); 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"); 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"); 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::CheckButton::with_label("Camera"); camera_channel_toggle.set_active(state.channels.camera); camera_channel_toggle.set_tooltip_text(Some("Send webcam during relay.")); let audio_channel_toggle = gtk::CheckButton::with_label("Speaker"); audio_channel_toggle.set_active(state.channels.audio); audio_channel_toggle.set_tooltip_text(Some("Play remote audio here.")); let microphone_channel_toggle = gtk::CheckButton::with_label("Mic"); microphone_channel_toggle.set_active(state.channels.microphone); microphone_channel_toggle.set_tooltip_text(Some("Send mic during relay.")); 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"); 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); attach_device_control_row( &media_grid, 2, µphone_channel_toggle, µ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 (preview_panel, preview_body) = build_panel("Upstream Media"); preview_panel.set_hexpand(false); preview_panel.set_vexpand(false); preview_panel.set_valign(gtk::Align::Fill); preview_body.set_vexpand(false); preview_body.set_spacing(8); let testing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); testing_row.set_hexpand(true); testing_row.set_vexpand(false); testing_row.set_valign(gtk::Align::Start); let camera_preview = gtk::Picture::new(); camera_preview.set_can_shrink(true); camera_preview.set_hexpand(false); camera_preview.set_vexpand(false); camera_preview.set_halign(gtk::Align::Fill); camera_preview.set_valign(gtk::Align::Start); 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_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(false); camera_preview_shell.set_vexpand(false); camera_preview_shell.set_halign(gtk::Align::Fill); camera_preview_shell.set_valign(gtk::Align::Start); 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(false); camera_preview_frame.set_vexpand(false); camera_preview_frame.set_halign(gtk::Align::Fill); camera_preview_frame.set_valign(gtk::Align::Start); camera_preview_frame.set_size_request( CAMERA_PREVIEW_VIEWPORT_WIDTH, CAMERA_PREVIEW_VIEWPORT_HEIGHT, ); camera_preview_frame.set_child(Some(&camera_preview)); camera_preview_shell.append(&camera_preview_frame); let webcam_group = build_subgroup("Webcam Preview"); webcam_group.set_hexpand(false); webcam_group.set_vexpand(false); webcam_group.set_valign(gtk::Align::Start); 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(false); 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(false); 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(false); 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, 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_frame, camera_preview, camera_status, camera_test_button, microphone_test_button, microphone_replay_button, speaker_test_button, } }