diff --git a/client/src/launcher/device_test.rs b/client/src/launcher/device_test.rs index 638b385..890f887 100644 --- a/client/src/launcher/device_test.rs +++ b/client/src/launcher/device_test.rs @@ -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::() .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 { let caps = sample.caps()?; let structure = caps.structure(0)?; @@ -422,7 +425,7 @@ fn quote(value: impl Into) -> 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,")); + } } diff --git a/client/src/launcher/ui_components.rs b/client/src/launcher/ui_components.rs index eea1452..168453f 100644 --- a/client/src/launcher/ui_components.rs +++ b/client/src/launcher/ui_components.rs @@ -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: >k::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: >k::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(); diff --git a/scripts/ci/hygiene_gate_baseline.json b/scripts/ci/hygiene_gate_baseline.json index 72c8e96..e0c296b 100644 --- a/scripts/ci/hygiene_gate_baseline.json +++ b/scripts/ci/hygiene_gate_baseline.json @@ -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,