fix(launcher): rebalance layout and local preview

This commit is contained in:
Brad Stein 2026-04-15 02:46:59 -03:00
parent e7dcfd2fd5
commit 308ea1bf85
3 changed files with 98 additions and 77 deletions

View File

@ -337,14 +337,7 @@ fn run_camera_preview_feed(
}
fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app::AppSink)> {
let device = gst_quote(device);
let desc = format!(
"v4l2src device=\"{device}\" do-timestamp=true ! \
video/x-raw,width={CAMERA_PREVIEW_WIDTH},height={CAMERA_PREVIEW_HEIGHT},framerate=30/1 ! \
videoconvert ! videoscale ! \
video/x-raw,format=RGBA,width={CAMERA_PREVIEW_WIDTH},height={CAMERA_PREVIEW_HEIGHT},pixel-aspect-ratio=1/1 ! \
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true"
);
let desc = camera_preview_pipeline_desc(device);
let pipeline = gst::parse::launch(&desc)?
.downcast::<gst::Pipeline>()
.expect("camera preview pipeline");
@ -363,6 +356,16 @@ fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app
Ok((pipeline, appsink))
}
fn camera_preview_pipeline_desc(device: &str) -> String {
let device = gst_quote(device);
format!(
"v4l2src device=\"{device}\" do-timestamp=true ! \
videoconvert ! videoscale ! videorate ! \
video/x-raw,format=RGBA,width={CAMERA_PREVIEW_WIDTH},height={CAMERA_PREVIEW_HEIGHT},framerate=30/1,pixel-aspect-ratio=1/1 ! \
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true"
)
}
fn sample_to_frame(sample: &gst::Sample) -> Option<PreviewFrame> {
let caps = sample.caps()?;
let structure = caps.structure(0)?;
@ -422,7 +425,7 @@ fn quote(value: impl Into<String>) -> String {
#[cfg(test)]
mod tests {
use super::{normalize_camera_selection, resolve_camera_device};
use super::{camera_preview_pipeline_desc, normalize_camera_selection, resolve_camera_device};
#[test]
fn resolve_camera_device_accepts_explicit_paths_and_catalog_names() {
@ -443,4 +446,12 @@ mod tests {
Some("usb-Logitech_C920-video-index0".to_string())
);
}
#[test]
fn camera_preview_pipeline_scales_after_source_instead_of_pinning_raw_source_caps() {
let desc = camera_preview_pipeline_desc("/dev/video0");
assert!(desc.contains("v4l2src device=\"/dev/video0\""));
assert!(desc.contains("videoconvert ! videoscale ! videorate !"));
assert!(!desc.contains("v4l2src device=\"/dev/video0\" do-timestamp=true ! video/x-raw,"));
}
}

View File

@ -135,7 +135,7 @@ pub fn build_launcher_view(
root.append(&content);
let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 12);
sidebar.set_size_request(410, -1);
sidebar.set_size_request(420, -1);
sidebar.set_valign(gtk::Align::Fill);
content.append(&sidebar);
@ -145,7 +145,6 @@ pub fn build_launcher_view(
content.append(&stage);
let (connection_panel, connection_body) = build_panel("Session");
let server_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let server_entry = gtk::Entry::new();
server_entry.add_css_class("server-entry");
server_entry.set_hexpand(true);
@ -153,20 +152,39 @@ pub fn build_launcher_view(
server_entry.set_tooltip_text(Some(
"Relay host address for previews, power control, and the live session.",
));
connection_body.append(&server_entry);
let relay_actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let start_button = gtk::Button::with_label("Start Relay");
start_button.add_css_class("suggested-action");
start_button.set_hexpand(true);
start_button.set_tooltip_text(Some(
"Launch the relay using the staged devices and current input routing.",
));
let stop_button = gtk::Button::with_label("Stop Relay");
stop_button.add_css_class("destructive-action");
stop_button.set_hexpand(true);
stop_button.set_tooltip_text(Some(
"Stop the live relay session. Local staging and previews stay available.",
));
server_row.append(&server_entry);
server_row.append(&start_button);
server_row.append(&stop_button);
connection_body.append(&server_row);
relay_actions_row.append(&start_button);
relay_actions_row.append(&stop_button);
connection_body.append(&relay_actions_row);
let live_actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let clipboard_button = gtk::Button::with_label("Send Clipboard");
clipboard_button.set_hexpand(true);
clipboard_button.set_tooltip_text(Some(
"Type the current local clipboard into the remote target. This stays launcher-only.",
));
let probe_button = gtk::Button::with_label("Copy Gate Probe");
probe_button.set_hexpand(true);
probe_button.set_tooltip_text(Some(
"Copy the hygiene/quality probe command into the local clipboard.",
));
live_actions_row.append(&clipboard_button);
live_actions_row.append(&probe_button);
connection_body.append(&live_actions_row);
let power_intro = gtk::Label::new(Some(
"Capture power can stay automatic or be forced on/off while you stage a session.",
@ -303,13 +321,42 @@ pub fn build_launcher_view(
&speaker_test_button,
);
let preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 8);
preview_shell.add_css_class("camera-preview-shell");
let preview_heading = gtk::Label::new(Some("Selected Camera Preview"));
preview_heading.add_css_class("panel-title");
preview_heading.set_halign(gtk::Align::Start);
sidebar.append(&devices_panel);
let stage_header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let stage_title = gtk::Label::new(Some("Remote Eye Feeds"));
stage_title.add_css_class("title-4");
stage_title.set_halign(gtk::Align::Start);
stage_header.append(&stage_title);
let stage_note = gtk::Label::new(Some(
"Live server-side eye feeds. In Auto mode, open previews and active relay sessions count as capture demand.",
));
stage_note.add_css_class("dim-label");
stage_note.set_wrap(true);
stage_note.set_xalign(0.0);
stage.append(&stage_header);
stage.append(&stage_note);
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 16);
display_row.set_hexpand(true);
display_row.set_vexpand(true);
display_row.set_homogeneous(true);
let left_pane = build_display_pane("Left Eye", "/dev/lesavka_l_eye");
let right_pane = build_display_pane("Right Eye", "/dev/lesavka_r_eye");
display_row.append(&left_pane.root);
display_row.append(&right_pane.root);
stage.append(&display_row);
let workspace_row = gtk::Box::new(gtk::Orientation::Horizontal, 16);
workspace_row.set_hexpand(true);
workspace_row.set_vexpand(true);
stage.append(&workspace_row);
let (preview_panel, preview_body) = build_panel("Selected Camera Preview");
preview_panel.set_hexpand(true);
preview_panel.set_vexpand(true);
let preview_note = gtk::Label::new(Some(
"Use this to verify the chosen webcam in-place. Audio device tests still stay local.",
"Verify the chosen webcam here before you launch. Audio device tests still stay local.",
));
preview_note.add_css_class("dim-label");
preview_note.set_wrap(true);
@ -317,18 +364,22 @@ pub fn build_launcher_view(
let camera_preview = gtk::Picture::new();
camera_preview.set_can_shrink(true);
camera_preview.set_hexpand(true);
camera_preview.set_size_request(360, 202);
camera_preview.set_vexpand(true);
camera_preview.set_size_request(420, 210);
camera_preview.set_keep_aspect_ratio(true);
camera_preview.add_css_class("camera-preview-frame");
let camera_status = gtk::Label::new(Some("Select a camera and click Start Preview."));
camera_status.add_css_class("dim-label");
camera_status.set_wrap(true);
camera_status.set_xalign(0.0);
preview_shell.append(&preview_heading);
preview_shell.append(&preview_note);
preview_shell.append(&camera_preview);
preview_shell.append(&camera_status);
devices_body.append(&preview_shell);
sidebar.append(&devices_panel);
preview_body.append(&preview_note);
preview_body.append(&camera_preview);
preview_body.append(&camera_status);
workspace_row.append(&preview_panel);
let operations_column = gtk::Box::new(gtk::Orientation::Vertical, 12);
operations_column.set_size_request(340, -1);
workspace_row.append(&operations_column);
let (plan_panel, plan_body) = build_panel("Launch Plan");
let launch_plan_title = gtk::Label::new(Some("Stage locally, then start the relay."));
@ -359,45 +410,7 @@ pub fn build_launcher_view(
plan_body.append(&launch_plan_summary);
plan_body.append(&local_test_detail);
plan_body.append(&launch_plan_detail);
sidebar.append(&plan_panel);
let (actions_panel, actions_body) = build_panel("Remote Actions");
let actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let clipboard_button = gtk::Button::with_label("Send Clipboard");
clipboard_button.set_tooltip_text(Some(
"Type the current local clipboard into the remote target. This stays launcher-only.",
));
let probe_button = gtk::Button::with_label("Copy Gate Probe");
probe_button.set_tooltip_text(Some(
"Copy the hygiene/quality probe command into the local clipboard.",
));
actions_row.append(&clipboard_button);
actions_row.append(&probe_button);
actions_body.append(&actions_row);
sidebar.append(&actions_panel);
let stage_header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let stage_title = gtk::Label::new(Some("Remote Eye Feeds"));
stage_title.add_css_class("title-4");
stage_title.set_halign(gtk::Align::Start);
stage_header.append(&stage_title);
let stage_note = gtk::Label::new(Some(
"These are the live server-side eye feeds. In Auto mode, open eye previews and active relay sessions count as capture demand.",
));
stage_note.add_css_class("dim-label");
stage_note.set_wrap(true);
stage_note.set_xalign(0.0);
stage.append(&stage_header);
stage.append(&stage_note);
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 16);
display_row.set_hexpand(true);
display_row.set_vexpand(true);
let left_pane = build_display_pane("Left Eye", "/dev/lesavka_l_eye");
let right_pane = build_display_pane("Right Eye", "/dev/lesavka_r_eye");
display_row.append(&left_pane.root);
display_row.append(&right_pane.root);
stage.append(&display_row);
operations_column.append(&plan_panel);
let status_label = gtk::Label::new(Some("Launcher ready."));
status_label.add_css_class("status-line");
@ -520,12 +533,6 @@ pub fn install_css(window: &gtk::ApplicationWindow) {
border-radius: 16px;
padding: 24px;
}
box.camera-preview-shell {
background: rgba(255, 255, 255, 0.025);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 14px;
}
picture.camera-preview-frame {
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.10);
@ -544,6 +551,9 @@ pub fn install_css(window: &gtk::ApplicationWindow) {
entry.server-entry {
min-height: 38px;
}
picture {
content-fit: contain;
}
button.pill-toggle {
min-height: 36px;
padding: 0 14px;
@ -628,7 +638,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
picture.set_hexpand(true);
picture.set_vexpand(true);
picture.set_can_shrink(true);
picture.set_size_request(540, 304);
picture.set_size_request(540, 240);
let preview_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
preview_box.append(&picture);
@ -645,7 +655,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
placeholder_box.add_css_class("display-placeholder");
placeholder_box.set_hexpand(true);
placeholder_box.set_vexpand(true);
placeholder_box.set_size_request(540, 304);
placeholder_box.set_size_request(540, 240);
placeholder_box.append(&placeholder);
let stack = gtk::Stack::new();

View File

@ -58,7 +58,7 @@
"client/src/launcher/device_test.rs": {
"clippy_warnings": 22,
"doc_debt": 20,
"loc": 446
"loc": 457
},
"client/src/launcher/devices.rs": {
"clippy_warnings": 6,
@ -98,7 +98,7 @@
"client/src/launcher/ui_components.rs": {
"clippy_warnings": 8,
"doc_debt": 4,
"loc": 679
"loc": 689
},
"client/src/launcher/ui_runtime.rs": {
"clippy_warnings": 10,