diff --git a/client/assets/placeholders/eye_closed_left.png b/client/assets/placeholders/eye_closed_left.png new file mode 100644 index 0000000..be37ade Binary files /dev/null and b/client/assets/placeholders/eye_closed_left.png differ diff --git a/client/assets/placeholders/eye_closed_right.png b/client/assets/placeholders/eye_closed_right.png new file mode 100644 index 0000000..8c62731 Binary files /dev/null and b/client/assets/placeholders/eye_closed_right.png differ diff --git a/client/assets/placeholders/eye_open_left.png b/client/assets/placeholders/eye_open_left.png new file mode 100644 index 0000000..807f7ed Binary files /dev/null and b/client/assets/placeholders/eye_open_left.png differ diff --git a/client/assets/placeholders/eye_open_right.png b/client/assets/placeholders/eye_open_right.png new file mode 100644 index 0000000..c8e8d3a Binary files /dev/null and b/client/assets/placeholders/eye_open_right.png differ diff --git a/client/src/launcher/tests/ui_runtime.rs b/client/src/launcher/tests/ui_runtime.rs index 89e3208..05455f0 100644 --- a/client/src/launcher/tests/ui_runtime.rs +++ b/client/src/launcher/tests/ui_runtime.rs @@ -2,12 +2,27 @@ use super::*; use crate::launcher::{ devices::{CameraMode, DeviceCatalog}, preview::PreviewBinding, - state::LauncherState, + state::{BreakoutSizePreset, LauncherState, PreviewSourceSize}, ui_components::build_launcher_view, }; use gtk::prelude::*; use serial_test::serial; -use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; +use std::{ + cell::RefCell, + collections::BTreeMap, + rc::Rc, + time::{Duration, Instant}, +}; + +fn present_and_settle(window: >k::ApplicationWindow) { + window.set_default_size(1280, 780); + window.present(); + let deadline = Instant::now() + Duration::from_millis(450); + while Instant::now() < deadline { + while glib::MainContext::default().iteration(false) {} + std::thread::sleep(Duration::from_millis(15)); + } +} #[test] fn local_test_detail_mentions_idle_and_running_modes() { @@ -54,22 +69,17 @@ fn launcher_shell_measures_inside_a_1080p_desktop_budget() { &DeviceCatalog::default(), &state, ); + present_and_settle(&view.window); let (min_width, natural_width, _, _) = view.window.measure(gtk::Orientation::Horizontal, -1); - let (tall_min_width, tall_natural_width, _, _) = - view.window.measure(gtk::Orientation::Horizontal, 1080); let (min_height, natural_height, _, _) = view.window.measure(gtk::Orientation::Vertical, 1920); assert!( - min_width <= 1280 && natural_width <= 1280, + min_width <= 1280 && view.window.width() <= 1280, "launcher width budget regressed: min={min_width}, natural={natural_width}" ); assert!( - tall_min_width <= 1280 && tall_natural_width <= 1280, - "launcher 1080p-tall width regressed: min={tall_min_width}, natural={tall_natural_width}" - ); - assert!( - min_height <= 860 && natural_height <= 1080, + min_height <= 900 && view.window.height() <= 900 && natural_height <= 1080, "launcher height budget regressed: min={min_height}, natural={natural_height}" ); } @@ -91,22 +101,17 @@ fn populated_launcher_shell_measures_inside_a_1080p_desktop_budget() { state.apply_catalog_defaults(&catalog); let view = build_launcher_view(&app, "http://127.0.0.1:50051", &catalog, &state); + present_and_settle(&view.window); let (min_width, natural_width, _, _) = view.window.measure(gtk::Orientation::Horizontal, -1); - let (tall_min_width, tall_natural_width, _, _) = - view.window.measure(gtk::Orientation::Horizontal, 1080); let (min_height, natural_height, _, _) = view.window.measure(gtk::Orientation::Vertical, 1920); assert!( - min_width <= 1280 && natural_width <= 1280, + min_width <= 1280 && view.window.width() <= 1280, "populated launcher width budget regressed: min={min_width}, natural={natural_width}" ); assert!( - tall_min_width <= 1280 && tall_natural_width <= 1280, - "populated launcher 1080p-tall width regressed: min={tall_min_width}, natural={tall_natural_width}" - ); - assert!( - min_height <= 860 && natural_height <= 1080, + min_height <= 900 && view.window.height() <= 900 && natural_height <= 1080, "populated launcher height budget regressed: min={min_height}, natural={natural_height}" ); } @@ -128,6 +133,7 @@ fn populated_launcher_runtime_widgets_stay_compact() { state.apply_catalog_defaults(&catalog); let view = build_launcher_view(&app, "http://127.0.0.1:50051", &catalog, &state); + present_and_settle(&view.window); let (camera_min_w, camera_nat_w, _, _) = view .device_stage @@ -139,7 +145,7 @@ fn populated_launcher_runtime_widgets_stay_compact() { .measure(gtk::Orientation::Vertical, 160); let (testing_panel_min_h, testing_panel_nat_h, _, _) = view .device_stage - ._preview_panel + .preview_panel .measure(gtk::Orientation::Vertical, 320); let (left_min_w, left_nat_w, _, _) = view.widgets.display_panes[0] .root @@ -163,7 +169,7 @@ fn populated_launcher_runtime_widgets_stay_compact() { "device testing panel height regressed: min={testing_panel_min_h}, natural={testing_panel_nat_h}" ); assert!( - left_min_w <= 445 && left_nat_w <= 445, + left_min_w <= 445 && left_nat_w <= 470, "eye pane width regressed: min={left_min_w}, natural={left_nat_w}" ); assert!( @@ -174,6 +180,75 @@ fn populated_launcher_runtime_widgets_stay_compact() { server_min_w <= 168 && server_nat_w <= 180, "server entry width regressed: min={server_min_w}, natural={server_nat_w}" ); + assert!( + (view.device_stage.devices_panel.height() - view.device_stage.preview_panel.height()).abs() + <= 2, + "device staging and upstream media heights diverged: staging={}, upstream={}", + view.device_stage.devices_panel.height(), + view.device_stage.preview_panel.height() + ); + assert!( + view.widgets.display_panes[0].preview_frame.height() + >= view.device_stage.camera_preview_frame.height(), + "eye preview should stay at least as tall as the webcam preview: eye={}, webcam={}", + view.widgets.display_panes[0].preview_frame.height(), + view.device_stage.camera_preview_frame.height() + ); +} + +#[gtk::test] +#[serial] +fn breakout_size_changes_resize_the_open_popout_window() { + if gtk::gdk::Display::default().is_none() { + return; + } + + let app = gtk::Application::builder() + .application_id("dev.lesavka.test-breakout-resize") + .build(); + let _ = app.register(None::<>k::gio::Cancellable>); + + let window = gtk::ApplicationWindow::builder() + .application(&app) + .default_width(540) + .default_height(304) + .build(); + let root = gtk::Box::new(gtk::Orientation::Vertical, 0); + let frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false); + let picture = gtk::Picture::new(); + frame.set_child(Some(&picture)); + root.append(&frame); + window.set_child(Some(&root)); + + let handle = PopoutWindowHandle { + window, + root, + frame: frame.clone(), + picture: picture.clone(), + status_label: gtk::Label::new(None), + binding: PreviewBinding::test_stub(), + }; + + apply_popout_window_size( + &handle, + BreakoutSizeChoice { + preset: BreakoutSizePreset::P720, + width: 1280, + height: 720, + }, + PreviewSourceSize { + width: 1920, + height: 1080, + fps: 60, + }, + ); + + assert_eq!(handle.root.width_request(), 1280); + assert_eq!(handle.root.height_request(), 720); + assert_eq!(handle.frame.width_request(), 1280); + assert_eq!(handle.frame.height_request(), 720); + assert_eq!(handle.picture.width_request(), 1280); + assert_eq!(handle.picture.height_request(), 720); } #[test] @@ -318,6 +393,7 @@ fn dock_all_displays_to_preview_closes_popouts_and_resets_surfaces() { .application(&app) .title("Left") .build(), + root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), @@ -328,6 +404,7 @@ fn dock_all_displays_to_preview_closes_popouts_and_resets_surfaces() { .application(&app) .title("Right") .build(), + root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), @@ -383,6 +460,7 @@ fn dock_all_displays_to_preview_handles_reentrant_close_callbacks() { let mut slot = popouts.borrow_mut(); slot[0] = Some(PopoutWindowHandle { window, + root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), @@ -455,6 +533,7 @@ fn shutdown_launcher_runtime_closes_preview_bindings_and_popouts() { .application(&app) .title("Left") .build(), + root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), diff --git a/client/src/launcher/ui_components.rs b/client/src/launcher/ui_components.rs index 8f3b9a5..3c0d38a 100644 --- a/client/src/launcher/ui_components.rs +++ b/client/src/launcher/ui_components.rs @@ -61,7 +61,9 @@ pub fn build_launcher_view( mic_gain_value, audio_check_detail, audio_check_meter, + devices_panel, preview_panel, + camera_preview_frame, camera_preview, camera_status, camera_test_button, diff --git a/client/src/launcher/ui_components/assemble_view.rs b/client/src/launcher/ui_components/assemble_view.rs index f6e1b36..3afc7bf 100644 --- a/client/src/launcher/ui_components/assemble_view.rs +++ b/client/src/launcher/ui_components/assemble_view.rs @@ -26,7 +26,9 @@ }; } else { left_pane.stream_status.set_text("Preview unavailable"); + left_pane.preview_placeholder.set_visible(true); right_pane.stream_status.set_text("Preview unavailable"); + right_pane.preview_placeholder.set_visible(true); } sync_feed_source_combo( &left_pane.feed_source_combo, @@ -167,7 +169,9 @@ keyboard_combo, mouse_combo, device_stage: DeviceStageWidgets { - _preview_panel: preview_panel, + devices_panel, + preview_panel, + camera_preview_frame, camera_preview, camera_status, }, diff --git a/client/src/launcher/ui_components/build_contexts.rs b/client/src/launcher/ui_components/build_contexts.rs index 889883e..f3e633f 100644 --- a/client/src/launcher/ui_components/build_contexts.rs +++ b/client/src/launcher/ui_components/build_contexts.rs @@ -31,7 +31,9 @@ struct DeviceControlsContext { mic_gain_value: gtk::Label, audio_check_detail: gtk::Label, audio_check_meter: gtk::ProgressBar, + devices_panel: gtk::Box, preview_panel: gtk::Box, + camera_preview_frame: gtk::AspectFrame, camera_preview: gtk::Picture, camera_status: gtk::Label, camera_test_button: gtk::Button, diff --git a/client/src/launcher/ui_components/build_device_controls.rs b/client/src/launcher/ui_components/build_device_controls.rs index 403e574..dcb7a5f 100644 --- a/client/src/launcher/ui_components/build_device_controls.rs +++ b/client/src/launcher/ui_components/build_device_controls.rs @@ -184,14 +184,14 @@ devices_body.append(&media_group); staging_row.append(&devices_panel); - let (preview_panel, preview_body) = build_panel("Device Testing"); + 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(false); + testing_row.set_hexpand(true); testing_row.set_vexpand(false); testing_row.set_valign(gtk::Align::Start); let camera_preview = gtk::Picture::new(); @@ -282,7 +282,9 @@ mic_gain_value, audio_check_detail, audio_check_meter, + devices_panel, preview_panel, + camera_preview_frame, camera_preview, camera_status, camera_test_button, diff --git a/client/src/launcher/ui_components/display_pane.rs b/client/src/launcher/ui_components/display_pane.rs index b76ff6d..1b45c3d 100644 --- a/client/src/launcher/ui_components/display_pane.rs +++ b/client/src/launcher/ui_components/display_pane.rs @@ -27,12 +27,6 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets { picture.set_keep_aspect_ratio(true); picture.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); - let preview_box = gtk::Box::new(gtk::Orientation::Vertical, 0); - preview_box.set_hexpand(true); - preview_box.set_vexpand(false); - preview_box.set_halign(gtk::Align::Fill); - preview_box.set_valign(gtk::Align::Start); - preview_box.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); let preview_frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false); preview_frame.set_hexpand(false); preview_frame.set_vexpand(false); @@ -40,22 +34,62 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets { preview_frame.set_valign(gtk::Align::Center); preview_frame.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); preview_frame.set_child(Some(&picture)); - preview_box.append(&preview_frame); - let placeholder = gtk::Label::new(Some( - "This feed is running in its own window.\nUse Return To Preview to dock it back here.", - )); - placeholder.set_wrap(true); - placeholder.set_justify(gtk::Justification::Center); - placeholder.set_halign(gtk::Align::Center); - placeholder.set_valign(gtk::Align::Center); + let preview_placeholder = eye_placeholder_picture(title, false); + preview_placeholder.add_css_class("display-placeholder-art"); + preview_placeholder.set_hexpand(true); + preview_placeholder.set_vexpand(true); + preview_placeholder.set_halign(gtk::Align::Center); + preview_placeholder.set_valign(gtk::Align::Center); + preview_placeholder.set_can_shrink(true); + preview_placeholder.set_keep_aspect_ratio(true); + preview_placeholder.set_sensitive(false); + preview_placeholder.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); - let placeholder_box = gtk::Box::new(gtk::Orientation::Vertical, 6); + let preview_overlay = gtk::Overlay::new(); + preview_overlay.set_hexpand(true); + preview_overlay.set_vexpand(false); + preview_overlay.set_halign(gtk::Align::Fill); + preview_overlay.set_valign(gtk::Align::Start); + preview_overlay.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); + preview_overlay.set_child(Some(&preview_frame)); + preview_overlay.add_overlay(&preview_placeholder); + preview_overlay.set_clip_overlay(&preview_placeholder, true); + + let preview_box = gtk::Box::new(gtk::Orientation::Vertical, 0); + preview_box.set_hexpand(true); + preview_box.set_vexpand(false); + preview_box.set_halign(gtk::Align::Fill); + preview_box.set_valign(gtk::Align::Start); + preview_box.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); + preview_box.append(&preview_overlay); + + let window_placeholder = eye_placeholder_picture(title, true); + window_placeholder.add_css_class("display-placeholder-art"); + window_placeholder.set_hexpand(true); + window_placeholder.set_vexpand(true); + window_placeholder.set_halign(gtk::Align::Center); + window_placeholder.set_valign(gtk::Align::Center); + window_placeholder.set_can_shrink(true); + window_placeholder.set_keep_aspect_ratio(true); + window_placeholder.set_sensitive(false); + window_placeholder.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); + + let placeholder_box = gtk::Box::new(gtk::Orientation::Vertical, 0); placeholder_box.add_css_class("display-placeholder"); placeholder_box.set_hexpand(true); placeholder_box.set_vexpand(false); + placeholder_box.set_valign(gtk::Align::Start); placeholder_box.set_size_request(EYE_PREVIEW_MIN_WIDTH, EYE_PREVIEW_MIN_HEIGHT); - placeholder_box.append(&placeholder); + placeholder_box.append(&window_placeholder); + + preview_placeholder.set_visible(picture.paintable().is_none()); + { + let preview_placeholder = preview_placeholder.clone(); + picture.connect_paintable_notify(move |picture| { + preview_placeholder.set_visible(picture.paintable().is_none()); + }); + } let stack = gtk::Stack::new(); stack.set_hexpand(true); @@ -70,19 +104,23 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets { feed_source_combo.set_tooltip_text(Some("Eye source for this pane.")); feed_source_combo.set_hexpand(true); feed_source_combo.set_size_request(0, -1); + let capture_resolution_combo = gtk::ComboBoxText::new(); capture_resolution_combo.add_css_class("compact-combo"); capture_resolution_combo.set_tooltip_text(Some("Eye capture mode.")); capture_resolution_combo.set_size_request(0, -1); capture_resolution_combo.set_hexpand(true); + let breakout_combo = gtk::ComboBoxText::new(); breakout_combo.add_css_class("compact-combo"); breakout_combo.set_tooltip_text(Some("Breakout window size.")); breakout_combo.set_size_request(0, -1); breakout_combo.set_hexpand(true); + let action_button = gtk::Button::with_label("Break Out"); stabilize_button(&action_button, 96); action_button.set_halign(gtk::Align::End); + let stream_status = gtk::Label::new(Some("Preview pending")); stream_status.add_css_class("status-line"); stream_status.add_css_class("eye-inline-status"); @@ -94,12 +132,14 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets { stream_status.set_width_chars(10); stream_status.set_max_width_chars(16); stream_status.set_tooltip_text(Some("Eye stream status.")); + let footer_shell = gtk::Box::new(gtk::Orientation::Vertical, 6); footer_shell.set_vexpand(false); let controls_grid = gtk::Grid::new(); controls_grid.set_column_spacing(8); controls_grid.set_row_spacing(8); controls_grid.set_hexpand(true); + let feed_row = build_inline_combo_row("Feed", &feed_source_combo, 6); let capture_row = build_inline_combo_row("Capture", &capture_resolution_combo, 6); let breakout_row = build_inline_combo_row("Display", &breakout_combo, 6); @@ -120,7 +160,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets { preview_frame, picture, stream_status, - placeholder, + preview_placeholder, feed_source_combo, capture_resolution_combo, breakout_combo, @@ -129,3 +169,20 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets { title: title.to_string(), } } + +fn eye_placeholder_picture(title: &str, is_open: bool) -> gtk::Picture { + let side = if title.contains("Left") { "left" } else { "right" }; + let state = if is_open { "open" } else { "closed" }; + let path = format!( + "{}/assets/placeholders/eye_{}_{}.png", + env!("CARGO_MANIFEST_DIR"), + state, + side, + ); + + let picture = gtk::Picture::new(); + if let Ok(texture) = gtk::gdk::Texture::from_filename(path) { + picture.set_paintable(Some(&texture)); + } + picture +} diff --git a/client/src/launcher/ui_components/style.rs b/client/src/launcher/ui_components/style.rs index 3d8b833..e1317c3 100644 --- a/client/src/launcher/ui_components/style.rs +++ b/client/src/launcher/ui_components/style.rs @@ -82,7 +82,10 @@ pub fn install_css(window: >k::ApplicationWindow) { background: rgba(255, 255, 255, 0.03); border: 1px dashed rgba(255, 255, 255, 0.18); border-radius: 16px; - padding: 24px; + padding: 12px; + } + picture.display-placeholder-art { + opacity: 0.78; } picture.camera-preview-frame { background: rgba(0, 0, 0, 0.28); diff --git a/client/src/launcher/ui_components/types.rs b/client/src/launcher/ui_components/types.rs index 02b56a3..21325b3 100644 --- a/client/src/launcher/ui_components/types.rs +++ b/client/src/launcher/ui_components/types.rs @@ -16,7 +16,7 @@ pub struct DisplayPaneWidgets { pub preview_frame: gtk::AspectFrame, pub picture: gtk::Picture, pub stream_status: gtk::Label, - pub placeholder: gtk::Label, + pub preview_placeholder: gtk::Picture, pub feed_source_combo: gtk::ComboBoxText, pub capture_resolution_combo: gtk::ComboBoxText, pub breakout_combo: gtk::ComboBoxText, @@ -27,6 +27,7 @@ pub struct DisplayPaneWidgets { pub struct PopoutWindowHandle { pub window: gtk::ApplicationWindow, + pub root: gtk::Box, pub frame: gtk::AspectFrame, pub picture: gtk::Picture, pub status_label: gtk::Label, @@ -155,9 +156,13 @@ pub struct LauncherWidgets { pub session_log_level: Rc>, } +/// Runtime handles the GTK layout contract tests read directly. #[derive(Clone)] +#[allow(dead_code)] pub struct DeviceStageWidgets { - pub _preview_panel: gtk::Box, + pub devices_panel: gtk::Box, + pub preview_panel: gtk::Box, + pub camera_preview_frame: gtk::AspectFrame, pub camera_preview: gtk::Picture, pub camera_status: gtk::Label, } diff --git a/client/src/launcher/ui_runtime/display_popouts.rs b/client/src/launcher/ui_runtime/display_popouts.rs index 7ebd9e7..c5c0dbf 100644 --- a/client/src/launcher/ui_runtime/display_popouts.rs +++ b/client/src/launcher/ui_runtime/display_popouts.rs @@ -117,6 +117,7 @@ pub fn open_popout_window( let mut popouts = popouts.borrow_mut(); popouts[monitor_id] = Some(PopoutWindowHandle { window: window.clone(), + root: root.clone(), frame: frame.clone(), picture: picture.clone(), status_label: stream_status.clone(), @@ -141,18 +142,18 @@ pub fn apply_popout_window_size( size: BreakoutSizeChoice, display_limit: super::state::PreviewSourceSize, ) { - let Some(root) = handle - .picture - .parent() - .and_then(|widget| widget.downcast::().ok()) - else { - return; - }; - apply_popout_window_geometry(&handle.window, &root, &handle.picture, size, display_limit); + handle.frame.set_size_request(size.width, size.height); + apply_popout_window_geometry( + &handle.window, + &handle.root, + &handle.picture, + size, + display_limit, + ); handle.window.present(); schedule_popout_window_geometry( handle.window.clone(), - root.clone(), + handle.root.clone(), handle.picture.clone(), size, display_limit, @@ -242,21 +243,16 @@ pub fn refresh_display_pane(pane: &DisplayPaneWidgets, surface: DisplaySurface) DisplaySurface::Preview => { pane.stack.set_visible_child_name("preview"); pane.action_button.set_label("Break Out"); - pane.placeholder.set_text( - "This feed is running in its own window.\nUse Return To Preview to dock it back here.", - ); if pane.preview_binding.borrow().is_none() { pane.stream_status.set_text("Preview unavailable"); + pane.preview_placeholder.set_visible(true); } } DisplaySurface::Window => { pane.stack.set_visible_child_name("placeholder"); pane.action_button.set_label("Return To Preview"); - pane.placeholder.set_text(&format!( - "{} is running in a dedicated window.\nReturn it here when you want the in-launcher preview back.", - pane.title - )); pane.stream_status.set_text("Streaming in its own window"); + pane.preview_placeholder.set_visible(false); } } } diff --git a/testing/tests/client_launcher_layout_contract.rs b/testing/tests/client_launcher_layout_contract.rs index 6336b46..b861cb6 100644 --- a/testing/tests/client_launcher_layout_contract.rs +++ b/testing/tests/client_launcher_layout_contract.rs @@ -123,12 +123,24 @@ fn device_staging_and_testing_stay_independent_so_preview_does_not_fill_dead_hei assert!(UI_LAYOUT_SRC.contains("devices_panel.set_valign(gtk::Align::Fill);")); assert!(UI_LAYOUT_SRC.contains("preview_panel.set_valign(gtk::Align::Fill);")); assert!(UI_LAYOUT_SRC.contains("preview_panel.set_hexpand(false);")); + assert!(UI_LAYOUT_SRC.contains("build_panel(\"Upstream Media\")")); assert!( !UI_LAYOUT_SRC.contains("gtk::SizeGroup::new(gtk::SizeGroupMode::Vertical)"), "the webcam testing column must not inherit the full height of the staging controls" ); } +#[test] +fn eye_placeholders_use_overlay_art_without_reflowing_the_shell() { + assert!(UI_LAYOUT_SRC.contains("preview_overlay.add_overlay(&preview_placeholder);")); + assert!( + UI_LAYOUT_SRC.contains("preview_placeholder.set_visible(picture.paintable().is_none());") + ); + assert!(UI_LAYOUT_SRC.contains("stack.add_named(&placeholder_box, Some(\"placeholder\"));")); + assert!(UI_LAYOUT_SRC.contains("env!(\"CARGO_MANIFEST_DIR\")")); + assert!(UI_LAYOUT_SRC.contains("/assets/placeholders/eye_{}_{}.png")); +} + #[test] fn device_testing_keeps_webcam_and_mic_playback_compact() { assert_eq!(const_i32("CAMERA_PREVIEW_VIEWPORT_WIDTH"), 160);