2026-04-23 07:00:06 -03:00
|
|
|
{
|
|
|
|
|
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()));
|
2026-05-11 01:15:15 -03:00
|
|
|
devices_panel.set_hexpand(false);
|
|
|
|
|
devices_panel.set_size_request(DEVICE_STAGING_PANEL_WIDTH, -1);
|
2026-04-23 17:03:12 -03:00
|
|
|
devices_panel.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
devices_panel.set_valign(gtk::Align::Fill);
|
2026-04-23 17:03:12 -03:00
|
|
|
devices_body.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
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();
|
2026-04-23 11:14:58 -03:00
|
|
|
camera_combo.add_css_class("compact-combo");
|
2026-05-11 01:15:15 -03:00
|
|
|
install_device_choice_combo_behavior(&camera_combo);
|
2026-04-23 07:00:06 -03:00
|
|
|
sync_stage_device_combo(
|
|
|
|
|
&camera_combo,
|
|
|
|
|
&catalog.cameras,
|
|
|
|
|
state.devices.camera.as_deref(),
|
|
|
|
|
);
|
|
|
|
|
let camera_quality_combo = gtk::ComboBoxText::new();
|
2026-04-23 11:14:58 -03:00
|
|
|
camera_quality_combo.add_css_class("compact-combo");
|
2026-04-23 07:00:06 -03:00
|
|
|
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();
|
2026-04-23 11:14:58 -03:00
|
|
|
speaker_combo.add_css_class("compact-combo");
|
2026-05-11 01:15:15 -03:00
|
|
|
install_device_choice_combo_behavior(&speaker_combo);
|
2026-04-23 07:00:06 -03:00
|
|
|
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();
|
2026-04-23 11:14:58 -03:00
|
|
|
keyboard_combo.add_css_class("compact-combo");
|
2026-05-11 01:15:15 -03:00
|
|
|
install_device_choice_combo_behavior(&keyboard_combo);
|
2026-04-23 07:00:06 -03:00
|
|
|
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();
|
2026-04-23 11:14:58 -03:00
|
|
|
mouse_combo.add_css_class("compact-combo");
|
2026-05-11 01:15:15 -03:00
|
|
|
install_device_choice_combo_behavior(&mouse_combo);
|
2026-04-23 07:00:06 -03:00
|
|
|
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");
|
2026-04-23 17:03:12 -03:00
|
|
|
media_group.set_vexpand(true);
|
|
|
|
|
media_group.set_valign(gtk::Align::Fill);
|
2026-04-23 07:00:06 -03:00
|
|
|
let media_grid = gtk::Grid::new();
|
|
|
|
|
media_grid.set_row_spacing(10);
|
|
|
|
|
media_grid.set_column_spacing(8);
|
|
|
|
|
media_group.append(&media_grid);
|
2026-04-23 18:05:21 -03:00
|
|
|
let camera_channel_toggle = gtk::ToggleButton::with_label("Camera");
|
2026-04-23 07:00:06 -03:00
|
|
|
camera_channel_toggle.set_active(state.channels.camera);
|
|
|
|
|
camera_channel_toggle.set_tooltip_text(Some("Send webcam during relay."));
|
2026-04-23 18:05:21 -03:00
|
|
|
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");
|
2026-04-23 07:00:06 -03:00
|
|
|
audio_channel_toggle.set_active(state.channels.audio);
|
|
|
|
|
audio_channel_toggle.set_tooltip_text(Some("Play remote audio here."));
|
2026-04-23 18:05:21 -03:00
|
|
|
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");
|
2026-04-23 07:00:06 -03:00
|
|
|
microphone_channel_toggle.set_active(state.channels.microphone);
|
|
|
|
|
microphone_channel_toggle.set_tooltip_text(Some("Send mic during relay."));
|
2026-04-23 18:05:21 -03:00
|
|
|
microphone_channel_toggle.add_css_class("pill-toggle");
|
|
|
|
|
microphone_channel_toggle.add_css_class("media-toggle");
|
2026-05-11 01:15:15 -03:00
|
|
|
microphone_channel_toggle.add_css_class("media-toggle-split");
|
|
|
|
|
stabilize_button(µphone_channel_toggle, 44);
|
2026-05-10 23:14:15 -03:00
|
|
|
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");
|
2026-05-11 01:15:15 -03:00
|
|
|
noise_suppression_toggle.add_css_class("media-toggle-split");
|
|
|
|
|
stabilize_button(&noise_suppression_toggle, 44);
|
2026-04-23 07:00:06 -03:00
|
|
|
|
|
|
|
|
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();
|
2026-04-23 11:14:58 -03:00
|
|
|
microphone_combo.add_css_class("compact-combo");
|
2026-05-11 01:15:15 -03:00
|
|
|
install_device_choice_combo_behavior(µphone_combo);
|
2026-04-23 07:00:06 -03:00
|
|
|
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);
|
2026-05-10 23:14:15 -03:00
|
|
|
let microphone_toggle_group = gtk::Box::new(gtk::Orientation::Horizontal, 4);
|
2026-05-11 01:15:15 -03:00
|
|
|
microphone_toggle_group.set_homogeneous(true);
|
|
|
|
|
microphone_toggle_group.set_size_request(92, -1);
|
2026-05-10 23:14:15 -03:00
|
|
|
microphone_toggle_group.append(µphone_channel_toggle);
|
|
|
|
|
microphone_toggle_group.append(&noise_suppression_toggle);
|
2026-04-23 07:00:06 -03:00
|
|
|
attach_device_control_row(
|
|
|
|
|
&media_grid,
|
|
|
|
|
2,
|
2026-05-10 23:14:15 -03:00
|
|
|
µphone_toggle_group,
|
2026-04-23 07:00:06 -03:00
|
|
|
µ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);
|
|
|
|
|
|
2026-05-10 23:14:15 -03:00
|
|
|
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");
|
2026-05-13 12:07:53 -03:00
|
|
|
for transport in [WebcamTransport::Mjpeg, WebcamTransport::Hevc] {
|
2026-05-10 23:14:15 -03:00
|
|
|
webcam_transport_combo.append(Some(transport.as_id()), transport.label());
|
|
|
|
|
}
|
2026-05-13 03:32:31 -03:00
|
|
|
webcam_transport_combo.set_active_id(Some(state.effective_webcam_transport().as_id()));
|
2026-05-10 23:14:15 -03:00
|
|
|
webcam_transport_combo.set_sensitive(true);
|
|
|
|
|
webcam_transport_combo.set_size_request(98, -1);
|
|
|
|
|
webcam_transport_combo.set_tooltip_text(Some(
|
2026-05-13 03:32:31 -03:00
|
|
|
"Upstream webcam transport for the next relay connection. MJPEG is the safe calibrated default; HEVC is used only when the server advertises it.",
|
2026-05-10 23:14:15 -03:00
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let upstream_audio_transport_combo = gtk::ComboBoxText::new();
|
|
|
|
|
upstream_audio_transport_combo.add_css_class("compact-combo");
|
2026-05-13 12:07:53 -03:00
|
|
|
for transport in [UpstreamAudioTransport::Pcm, UpstreamAudioTransport::Opus] {
|
2026-05-10 23:14:15 -03:00
|
|
|
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(
|
2026-05-13 12:07:53 -03:00
|
|
|
"Upstream microphone transport for the live relay. PCM is the known-good default; Opus is compressed and experimental.",
|
2026-05-10 23:14:15 -03:00
|
|
|
));
|
|
|
|
|
|
|
|
|
|
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()));
|
2026-04-23 17:03:12 -03:00
|
|
|
preview_panel.set_hexpand(true);
|
|
|
|
|
preview_panel.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
preview_panel.set_valign(gtk::Align::Fill);
|
2026-04-23 17:03:12 -03:00
|
|
|
preview_body.set_hexpand(true);
|
|
|
|
|
preview_body.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
preview_body.set_spacing(8);
|
|
|
|
|
let testing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
2026-04-23 15:18:30 -03:00
|
|
|
testing_row.set_hexpand(true);
|
2026-04-23 17:03:12 -03:00
|
|
|
testing_row.set_vexpand(true);
|
|
|
|
|
testing_row.set_valign(gtk::Align::Fill);
|
2026-04-23 07:00:06 -03:00
|
|
|
let camera_preview = gtk::Picture::new();
|
2026-04-23 14:15:02 -03:00
|
|
|
camera_preview.set_can_shrink(true);
|
2026-04-23 17:03:12 -03:00
|
|
|
camera_preview.set_hexpand(true);
|
|
|
|
|
camera_preview.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
camera_preview.set_halign(gtk::Align::Fill);
|
2026-04-23 17:03:12 -03:00
|
|
|
camera_preview.set_valign(gtk::Align::Fill);
|
2026-04-23 07:00:06 -03:00
|
|
|
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");
|
2026-04-23 19:27:04 -03:00
|
|
|
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()));
|
2026-04-23 19:11:54 -03:00
|
|
|
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));
|
2026-04-23 07:00:06 -03:00
|
|
|
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);
|
2026-04-23 17:03:12 -03:00
|
|
|
camera_preview_shell.set_hexpand(true);
|
|
|
|
|
camera_preview_shell.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
camera_preview_shell.set_halign(gtk::Align::Fill);
|
2026-04-23 17:03:12 -03:00
|
|
|
camera_preview_shell.set_valign(gtk::Align::Fill);
|
2026-04-23 07:00:06 -03:00
|
|
|
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);
|
2026-04-23 17:03:12 -03:00
|
|
|
camera_preview_frame.set_hexpand(true);
|
2026-05-11 01:15:15 -03:00
|
|
|
camera_preview_frame.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
camera_preview_frame.set_halign(gtk::Align::Fill);
|
2026-05-11 01:15:15 -03:00
|
|
|
camera_preview_frame.set_valign(gtk::Align::Fill);
|
2026-04-23 07:00:06 -03:00
|
|
|
camera_preview_frame.set_size_request(
|
|
|
|
|
CAMERA_PREVIEW_VIEWPORT_WIDTH,
|
|
|
|
|
CAMERA_PREVIEW_VIEWPORT_HEIGHT,
|
|
|
|
|
);
|
|
|
|
|
camera_preview_frame.set_child(Some(&camera_preview));
|
2026-04-23 19:11:54 -03:00
|
|
|
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);
|
2026-04-23 19:27:04 -03:00
|
|
|
let camera_preview_idle_frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false);
|
|
|
|
|
camera_preview_idle_frame.set_hexpand(true);
|
2026-05-11 01:15:15 -03:00
|
|
|
camera_preview_idle_frame.set_vexpand(true);
|
2026-04-23 19:27:04 -03:00
|
|
|
camera_preview_idle_frame.set_halign(gtk::Align::Fill);
|
2026-05-11 01:15:15 -03:00
|
|
|
camera_preview_idle_frame.set_valign(gtk::Align::Fill);
|
2026-04-23 19:27:04 -03:00
|
|
|
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);
|
2026-05-10 23:14:15 -03:00
|
|
|
let webcam_group = build_subgroup("Webcam Preview");
|
2026-04-23 17:03:12 -03:00
|
|
|
webcam_group.set_hexpand(true);
|
|
|
|
|
webcam_group.set_vexpand(true);
|
|
|
|
|
webcam_group.set_valign(gtk::Align::Fill);
|
2026-04-23 07:00:06 -03:00
|
|
|
webcam_group.append(&camera_preview_shell);
|
|
|
|
|
testing_row.append(&webcam_group);
|
|
|
|
|
|
|
|
|
|
let playback_group = build_subgroup("Mic Playback");
|
|
|
|
|
playback_group.set_hexpand(false);
|
2026-04-23 17:03:12 -03:00
|
|
|
playback_group.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
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);
|
2026-04-23 17:03:12 -03:00
|
|
|
playback_body.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
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);
|
2026-04-23 17:03:12 -03:00
|
|
|
audio_check_meter.set_vexpand(true);
|
2026-04-23 07:00:06 -03:00
|
|
|
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,
|
2026-05-10 23:14:15 -03:00
|
|
|
noise_suppression_toggle,
|
2026-04-23 07:00:06 -03:00
|
|
|
audio_channel_toggle,
|
|
|
|
|
audio_gain_scale,
|
|
|
|
|
audio_gain_value,
|
|
|
|
|
mic_gain_scale,
|
|
|
|
|
mic_gain_value,
|
|
|
|
|
audio_check_detail,
|
|
|
|
|
audio_check_meter,
|
2026-04-23 15:18:30 -03:00
|
|
|
devices_panel,
|
2026-04-23 14:15:02 -03:00
|
|
|
preview_panel,
|
2026-04-23 19:27:04 -03:00
|
|
|
camera_preview_stack,
|
2026-04-23 15:18:30 -03:00
|
|
|
camera_preview_frame,
|
2026-04-23 07:00:06 -03:00
|
|
|
camera_preview,
|
2026-04-29 23:19:57 -03:00
|
|
|
webcam_transport_combo,
|
2026-05-10 23:14:15 -03:00
|
|
|
upstream_audio_transport_combo,
|
2026-04-23 19:11:54 -03:00
|
|
|
camera_mirror_button,
|
|
|
|
|
camera_mirror_revealer,
|
2026-04-23 07:00:06 -03:00
|
|
|
camera_status,
|
|
|
|
|
camera_test_button,
|
|
|
|
|
microphone_test_button,
|
|
|
|
|
microphone_replay_button,
|
|
|
|
|
speaker_test_button,
|
|
|
|
|
}
|
|
|
|
|
}
|