2026-04-14 23:03:18 -03:00
use std ::{ cell ::RefCell , rc ::Rc } ;
2026-04-16 12:58:05 -03:00
use evdev ::Device ;
use gtk ::{ pango , prelude ::* } ;
2026-04-14 23:03:18 -03:00
use super ::{
devices ::DeviceCatalog ,
2026-04-16 19:19:37 -03:00
diagnostics ::DiagnosticsLog ,
2026-04-15 12:20:02 -03:00
preview ::{ LauncherPreview , PreviewBinding , PreviewSurface } ,
2026-04-16 12:58:05 -03:00
state ::{
2026-04-19 14:33:52 -03:00
BreakoutSizeChoice , BreakoutSizePreset , CaptureSizeChoice , CaptureSizePreset ,
FeedSourceChoice , FeedSourcePreset , LauncherState ,
2026-04-16 12:58:05 -03:00
} ,
2026-04-14 23:03:18 -03:00
} ;
#[ derive(Clone) ]
pub struct SummaryWidgets {
2026-04-16 12:58:05 -03:00
pub relay_light : gtk ::Box ,
2026-04-14 23:03:18 -03:00
pub relay_value : gtk ::Label ,
2026-04-16 12:58:05 -03:00
pub routing_light : gtk ::Box ,
2026-04-14 23:03:18 -03:00
pub routing_value : gtk ::Label ,
2026-04-16 12:58:05 -03:00
pub gpio_light : gtk ::Box ,
pub gpio_value : gtk ::Label ,
2026-04-14 23:03:18 -03:00
pub shortcut_value : gtk ::Label ,
}
#[ derive(Clone) ]
pub struct DisplayPaneWidgets {
pub root : gtk ::Box ,
pub stack : gtk ::Stack ,
2026-04-19 15:07:24 -03:00
pub preview_frame : gtk ::AspectFrame ,
2026-04-14 23:03:18 -03:00
pub picture : gtk ::Picture ,
pub stream_status : gtk ::Label ,
pub placeholder : gtk ::Label ,
2026-04-19 03:28:23 -03:00
pub feed_source_combo : gtk ::ComboBoxText ,
2026-04-17 01:09:33 -03:00
pub capture_resolution_combo : gtk ::ComboBoxText ,
2026-04-16 12:58:05 -03:00
pub breakout_combo : gtk ::ComboBoxText ,
2026-04-14 23:03:18 -03:00
pub action_button : gtk ::Button ,
2026-04-16 12:58:05 -03:00
pub preview_binding : Rc < RefCell < Option < PreviewBinding > > > ,
2026-04-14 23:03:18 -03:00
pub title : String ,
}
pub struct PopoutWindowHandle {
pub window : gtk ::ApplicationWindow ,
2026-04-19 15:07:24 -03:00
pub frame : gtk ::AspectFrame ,
2026-04-16 12:58:05 -03:00
pub picture : gtk ::Picture ,
pub status_label : gtk ::Label ,
2026-04-14 23:03:18 -03:00
pub binding : PreviewBinding ,
}
#[ derive(Clone) ]
pub struct LauncherWidgets {
pub status_label : gtk ::Label ,
2026-04-16 19:19:37 -03:00
pub diagnostics_log : Rc < RefCell < DiagnosticsLog > > ,
pub diagnostics_buffer : gtk ::TextBuffer ,
2026-04-16 12:58:05 -03:00
pub session_log_buffer : gtk ::TextBuffer ,
pub session_log_view : gtk ::TextView ,
2026-04-14 23:03:18 -03:00
pub summary : SummaryWidgets ,
pub power_detail : gtk ::Label ,
2026-04-16 12:58:05 -03:00
pub audio_check_detail : gtk ::Label ,
pub audio_check_meter : gtk ::ProgressBar ,
2026-04-14 23:03:18 -03:00
pub display_panes : [ DisplayPaneWidgets ; 2 ] ,
pub start_button : gtk ::Button ,
2026-04-15 01:20:51 -03:00
pub power_auto_button : gtk ::Button ,
pub power_on_button : gtk ::Button ,
pub power_off_button : gtk ::Button ,
2026-04-14 23:03:18 -03:00
pub input_toggle_button : gtk ::Button ,
pub clipboard_button : gtk ::Button ,
pub probe_button : gtk ::Button ,
2026-04-15 04:44:06 -03:00
pub swap_key_button : gtk ::Button ,
2026-04-14 23:03:18 -03:00
pub camera_test_button : gtk ::Button ,
pub microphone_test_button : gtk ::Button ,
2026-04-16 12:58:05 -03:00
pub microphone_replay_button : gtk ::Button ,
2026-04-14 23:03:18 -03:00
pub speaker_test_button : gtk ::Button ,
2026-04-16 19:19:37 -03:00
pub diagnostics_copy_button : gtk ::Button ,
pub diagnostics_popout_button : gtk ::Button ,
2026-04-16 12:58:05 -03:00
pub console_copy_button : gtk ::Button ,
pub console_popout_button : gtk ::Button ,
2026-04-14 23:03:18 -03:00
}
2026-04-15 01:20:51 -03:00
#[ derive(Clone) ]
pub struct DeviceStageWidgets {
pub camera_preview : gtk ::Picture ,
pub camera_status : gtk ::Label ,
}
2026-04-14 23:03:18 -03:00
pub struct LauncherView {
pub window : gtk ::ApplicationWindow ,
pub server_entry : gtk ::Entry ,
pub camera_combo : gtk ::ComboBoxText ,
pub microphone_combo : gtk ::ComboBoxText ,
pub speaker_combo : gtk ::ComboBoxText ,
2026-04-16 12:58:05 -03:00
pub keyboard_combo : gtk ::ComboBoxText ,
pub mouse_combo : gtk ::ComboBoxText ,
2026-04-15 01:20:51 -03:00
pub device_stage : DeviceStageWidgets ,
2026-04-14 23:03:18 -03:00
pub widgets : LauncherWidgets ,
pub preview : Option < Rc < LauncherPreview > > ,
pub popouts : Rc < RefCell < [ Option < PopoutWindowHandle > ; 2 ] > > ,
2026-04-16 19:19:37 -03:00
pub diagnostics_popout : Rc < RefCell < Option < gtk ::ApplicationWindow > > > ,
2026-04-16 12:58:05 -03:00
pub log_popout : Rc < RefCell < Option < gtk ::ApplicationWindow > > > ,
2026-04-14 23:03:18 -03:00
}
2026-04-16 12:58:05 -03:00
pub const LESAVKA_ICON_NAME : & str = " dev.lesavka.launcher " ;
const LESAVKA_ICON_SEARCH_PATH : & str = concat! ( env! ( " CARGO_MANIFEST_DIR " ) , " /assets/icons " ) ;
2026-04-19 04:24:27 -03:00
const LAUNCHER_DEFAULT_WIDTH : i32 = 1380 ;
const LAUNCHER_DEFAULT_HEIGHT : i32 = 860 ;
const OPERATIONS_RAIL_WIDTH : i32 = 288 ;
2026-04-16 12:58:05 -03:00
const CAMERA_PREVIEW_VIEWPORT_HEIGHT : i32 = 178 ;
const CAMERA_PREVIEW_VIEWPORT_WIDTH : i32 = 316 ;
2026-04-14 23:03:18 -03:00
pub fn build_launcher_view (
app : & gtk ::Application ,
server_addr : & str ,
catalog : & DeviceCatalog ,
state : & LauncherState ,
) -> LauncherView {
let window = gtk ::ApplicationWindow ::builder ( )
. application ( app )
2026-04-16 12:58:05 -03:00
. title ( " Lesavka " )
. default_width ( LAUNCHER_DEFAULT_WIDTH )
. default_height ( LAUNCHER_DEFAULT_HEIGHT )
2026-04-14 23:03:18 -03:00
. build ( ) ;
install_css ( & window ) ;
2026-04-16 12:58:05 -03:00
install_window_icon ( & window ) ;
2026-04-14 23:03:18 -03:00
2026-04-16 12:58:05 -03:00
let root = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
2026-04-14 23:03:18 -03:00
root . add_css_class ( " launcher-root " ) ;
2026-04-16 12:58:05 -03:00
root . set_margin_start ( 10 ) ;
root . set_margin_end ( 10 ) ;
root . set_margin_top ( 10 ) ;
root . set_margin_bottom ( 10 ) ;
2026-04-14 23:03:18 -03:00
2026-04-16 12:58:05 -03:00
let hero = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
2026-04-14 23:03:18 -03:00
hero . set_hexpand ( true ) ;
2026-04-16 12:58:05 -03:00
let brand_box = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 0 ) ;
2026-04-16 15:59:42 -03:00
let brand_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
brand_row . set_halign ( gtk ::Align ::Start ) ;
2026-04-16 12:58:05 -03:00
let heading = gtk ::Label ::new ( Some ( " Lesavka " ) ) ;
2026-04-14 23:03:18 -03:00
heading . add_css_class ( " title-2 " ) ;
heading . set_halign ( gtk ::Align ::Start ) ;
2026-04-16 15:59:42 -03:00
let version_tag = gtk ::Label ::new ( Some ( & format! ( " v {} " , crate ::VERSION ) ) ) ;
version_tag . add_css_class ( " version-tag " ) ;
version_tag . set_halign ( gtk ::Align ::Start ) ;
version_tag . set_valign ( gtk ::Align ::End ) ;
brand_row . append ( & heading ) ;
brand_row . append ( & version_tag ) ;
brand_box . append ( & brand_row ) ;
2026-04-14 23:03:18 -03:00
hero . append ( & brand_box ) ;
2026-04-16 12:58:05 -03:00
let chips = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 6 ) ;
2026-04-14 23:03:18 -03:00
chips . set_halign ( gtk ::Align ::End ) ;
chips . set_hexpand ( true ) ;
2026-04-16 15:59:42 -03:00
let ( relay_chip , relay_light , relay_value ) = build_status_chip_with_light ( " Server " , " " ) ;
2026-04-16 12:58:05 -03:00
let ( routing_chip , routing_light , routing_value ) =
build_status_chip_with_light ( " Inputs " , " Local " ) ;
let ( gpio_chip , gpio_light , gpio_value ) = build_status_chip_with_light ( " GPIO " , " Unknown " ) ;
2026-04-14 23:03:18 -03:00
let ( shortcut_chip , shortcut_value ) = build_status_chip ( " Swap Key " , " Pause " ) ;
2026-04-17 05:07:07 -03:00
stabilize_chip ( & relay_chip , 102 ) ;
2026-04-16 12:58:05 -03:00
stabilize_chip ( & routing_chip , 84 ) ;
stabilize_chip ( & gpio_chip , 84 ) ;
stabilize_chip ( & shortcut_chip , 88 ) ;
2026-04-14 23:03:18 -03:00
chips . append ( & relay_chip ) ;
chips . append ( & routing_chip ) ;
2026-04-16 12:58:05 -03:00
chips . append ( & gpio_chip ) ;
2026-04-14 23:03:18 -03:00
chips . append ( & shortcut_chip ) ;
hero . append ( & chips ) ;
root . append ( & hero ) ;
2026-04-16 12:58:05 -03:00
let content = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
2026-04-14 23:03:18 -03:00
content . set_hexpand ( true ) ;
content . set_vexpand ( true ) ;
root . append ( & content ) ;
2026-04-16 12:58:05 -03:00
let workspace = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
workspace . set_hexpand ( true ) ;
workspace . set_vexpand ( true ) ;
content . append ( & workspace ) ;
let operations = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
operations . set_size_request ( OPERATIONS_RAIL_WIDTH , - 1 ) ;
operations . set_hexpand ( false ) ;
operations . set_vexpand ( true ) ;
content . append ( & operations ) ;
let display_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
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 ) ;
workspace . append ( & display_row ) ;
let staging_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
staging_row . set_hexpand ( true ) ;
staging_row . set_vexpand ( false ) ;
workspace . append ( & staging_row ) ;
let ( devices_panel , devices_body ) = build_panel ( " Device Staging " ) ;
devices_panel . set_hexpand ( true ) ;
devices_panel . set_vexpand ( false ) ;
devices_body . set_spacing ( 8 ) ;
2026-04-14 23:03:18 -03:00
2026-04-16 12:58:05 -03:00
let control_group = build_subgroup ( " Control Inputs " ) ;
let control_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 12 ) ;
control_row . set_homogeneous ( true ) ;
control_group . append ( & control_row ) ;
let camera_combo = gtk ::ComboBoxText ::new ( ) ;
camera_combo . append ( Some ( " auto " ) , " auto " ) ;
for camera in & catalog . cameras {
append_stage_choice ( & camera_combo , camera ) ;
}
super ::ui_runtime ::set_combo_active_text ( & camera_combo , state . devices . camera . as_deref ( ) ) ;
let camera_test_button = gtk ::Button ::with_label ( " Start Preview " ) ;
stabilize_button ( & camera_test_button , 118 ) ;
camera_test_button . set_tooltip_text ( Some (
" Open a local preview for the selected webcam so you can confirm the right source. " ,
) ) ;
let speaker_combo = gtk ::ComboBoxText ::new ( ) ;
speaker_combo . append ( Some ( " auto " ) , " auto " ) ;
for speaker in & catalog . speakers {
append_stage_choice ( & speaker_combo , speaker ) ;
}
super ::ui_runtime ::set_combo_active_text ( & speaker_combo , 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 short continuous tone through the selected speaker until you stop the test. " ,
) ) ;
let keyboard_combo = gtk ::ComboBoxText ::new ( ) ;
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 (
" Leave this on all keyboards to relay every keyboard, or pick one specific device. " ,
) ) ;
let keyboard_block = build_selector_block ( " Keyboard " , & keyboard_combo ) ;
control_row . append ( & keyboard_block ) ;
let mouse_combo = gtk ::ComboBoxText ::new ( ) ;
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 (
" Leave this on all mice to relay every pointer, or pick one specific device. " ,
) ) ;
let mouse_block = build_selector_block ( " Mouse " , & mouse_combo ) ;
control_row . append ( & mouse_block ) ;
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 ) ;
2026-04-19 04:24:27 -03:00
camera_combo . set_size_request ( 0 , - 1 ) ;
speaker_combo . set_size_request ( 0 , - 1 ) ;
2026-04-16 12:58:05 -03:00
attach_device_row ( & media_grid , 0 , " Camera " , & camera_combo , & camera_test_button ) ;
attach_device_row (
& media_grid ,
1 ,
" Speaker " ,
& speaker_combo ,
& speaker_test_button ,
) ;
let microphone_combo = gtk ::ComboBoxText ::new ( ) ;
microphone_combo . append ( Some ( " auto " ) , " auto " ) ;
for microphone in & catalog . microphones {
append_stage_choice ( & microphone_combo , microphone ) ;
}
super ::ui_runtime ::set_combo_active_text (
& microphone_combo ,
state . devices . microphone . as_deref ( ) ,
) ;
let microphone_test_button = gtk ::Button ::with_label ( " Monitor Mic " ) ;
stabilize_button ( & microphone_test_button , 118 ) ;
microphone_test_button . set_tooltip_text ( Some (
" Monitor the selected microphone through the selected speaker until you stop the test. " ,
) ) ;
2026-04-19 04:24:27 -03:00
microphone_combo . set_size_request ( 0 , - 1 ) ;
2026-04-16 12:58:05 -03:00
attach_device_row (
& media_grid ,
2 ,
" Microphone " ,
& microphone_combo ,
& microphone_test_button ,
) ;
let audio_check_detail = gtk ::Label ::new ( Some (
" Monitor Mic listens locally, Replay Last 3s replays the latest captured mic audio, and Play Tone verifies the speaker path. " ,
) ) ;
audio_check_detail . add_css_class ( " dim-label " ) ;
audio_check_detail . set_wrap ( true ) ;
audio_check_detail . set_xalign ( 0.0 ) ;
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 ( " Selected Camera Preview " ) ;
preview_panel . set_hexpand ( true ) ;
preview_panel . set_vexpand ( false ) ;
preview_body . set_spacing ( 6 ) ;
let camera_preview = gtk ::Picture ::new ( ) ;
camera_preview . set_can_shrink ( false ) ;
camera_preview . set_hexpand ( true ) ;
camera_preview . set_vexpand ( true ) ;
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 camera and click Start Preview. " ) ) ;
camera_status . add_css_class ( " dim-label " ) ;
camera_status . set_wrap ( true ) ;
camera_status . set_xalign ( 0.0 ) ;
let camera_preview_shell = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 0 ) ;
camera_preview_shell . set_hexpand ( true ) ;
camera_preview_shell . set_vexpand ( false ) ;
camera_preview_shell . set_size_request ( - 1 , 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 ( true ) ;
camera_preview_frame . set_vexpand ( false ) ;
camera_preview_frame . set_size_request ( - 1 , CAMERA_PREVIEW_VIEWPORT_HEIGHT ) ;
camera_preview_frame . set_child ( Some ( & camera_preview ) ) ;
camera_preview_shell . append ( & camera_preview_frame ) ;
preview_body . append ( & camera_preview_shell ) ;
let playback_group = build_subgroup ( " Mic Playback " ) ;
let playback_body = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
let playback_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
playback_row . set_homogeneous ( true ) ;
let microphone_replay_button = gtk ::Button ::with_label ( " Replay Last 3s " ) ;
stabilize_button ( & microphone_replay_button , 124 ) ;
let audio_preview_heading = gtk ::Label ::new ( Some ( " Local Playback / Activity " ) ) ;
audio_preview_heading . add_css_class ( " subgroup-title " ) ;
audio_preview_heading . set_hexpand ( true ) ;
audio_preview_heading . set_halign ( gtk ::Align ::Start ) ;
playback_row . append ( & microphone_replay_button ) ;
playback_row . append ( & audio_preview_heading ) ;
playback_body . append ( & playback_row ) ;
playback_body . append ( & audio_check_meter ) ;
playback_group . append ( & playback_body ) ;
preview_body . append ( & playback_group ) ;
staging_row . append ( & preview_panel ) ;
2026-04-14 23:03:18 -03:00
2026-04-15 01:20:51 -03:00
let ( connection_panel , connection_body ) = build_panel ( " Session " ) ;
2026-04-14 23:03:18 -03:00
let server_entry = gtk ::Entry ::new ( ) ;
server_entry . add_css_class ( " server-entry " ) ;
server_entry . set_hexpand ( true ) ;
2026-04-16 12:58:05 -03:00
server_entry . set_width_chars ( 18 ) ;
2026-04-14 23:03:18 -03:00
server_entry . set_text ( server_addr ) ;
server_entry . set_tooltip_text ( Some (
" Relay host address for previews, power control, and the live session. " ,
) ) ;
2026-04-15 02:46:59 -03:00
connection_body . append ( & server_entry ) ;
let relay_actions_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
2026-04-16 12:58:05 -03:00
relay_actions_row . set_homogeneous ( true ) ;
2026-04-15 04:11:47 -03:00
let start_button = gtk ::Button ::with_label ( " Connect Relay " ) ;
2026-04-14 23:03:18 -03:00
start_button . add_css_class ( " suggested-action " ) ;
2026-04-15 02:46:59 -03:00
start_button . set_hexpand ( true ) ;
2026-04-16 12:58:05 -03:00
stabilize_button ( & start_button , 180 ) ;
2026-04-15 02:46:59 -03:00
relay_actions_row . append ( & start_button ) ;
connection_body . append ( & relay_actions_row ) ;
let live_actions_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
2026-04-16 12:58:05 -03:00
live_actions_row . set_homogeneous ( true ) ;
2026-04-15 02:46:59 -03:00
let clipboard_button = gtk ::Button ::with_label ( " Send Clipboard " ) ;
clipboard_button . set_hexpand ( true ) ;
2026-04-16 12:58:05 -03:00
stabilize_button ( & clipboard_button , 108 ) ;
2026-04-15 02:46:59 -03:00
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 ) ;
2026-04-16 12:58:05 -03:00
stabilize_button ( & probe_button , 108 ) ;
2026-04-15 02:46:59 -03:00
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 ) ;
2026-04-14 23:03:18 -03:00
2026-04-16 12:58:05 -03:00
connection_body . append ( & gtk ::Separator ::new ( gtk ::Orientation ::Horizontal ) ) ;
let power_heading = gtk ::Label ::new ( Some ( " GPIO Power " ) ) ;
power_heading . add_css_class ( " subgroup-title " ) ;
power_heading . set_halign ( gtk ::Align ::Start ) ;
connection_body . append ( & power_heading ) ;
2026-04-15 01:20:51 -03:00
2026-04-16 12:58:05 -03:00
let power_shell = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 0 ) ;
power_shell . set_halign ( gtk ::Align ::Center ) ;
2026-04-14 23:03:18 -03:00
let power_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
2026-04-16 12:58:05 -03:00
let power_on_button = gtk ::Button ::with_label ( " On " ) ;
stabilize_button ( & power_on_button , 64 ) ;
power_on_button . add_css_class ( " pill-toggle " ) ;
2026-04-15 01:20:51 -03:00
let power_auto_button = gtk ::Button ::with_label ( " Auto " ) ;
2026-04-16 12:58:05 -03:00
stabilize_button ( & power_auto_button , 64 ) ;
2026-04-15 01:20:51 -03:00
power_auto_button . add_css_class ( " pill-toggle " ) ;
2026-04-16 12:58:05 -03:00
let power_off_button = gtk ::Button ::with_label ( " Off " ) ;
stabilize_button ( & power_off_button , 64 ) ;
2026-04-15 01:20:51 -03:00
power_off_button . add_css_class ( " pill-toggle " ) ;
2026-04-14 23:03:18 -03:00
let power_detail = gtk ::Label ::new ( Some ( " Capture power status is loading... " ) ) ;
power_detail . add_css_class ( " dim-label " ) ;
power_detail . set_wrap ( true ) ;
power_detail . set_xalign ( 0.0 ) ;
2026-04-15 01:20:51 -03:00
power_row . append ( & power_on_button ) ;
2026-04-16 12:58:05 -03:00
power_row . append ( & power_auto_button ) ;
2026-04-15 01:20:51 -03:00
power_row . append ( & power_off_button ) ;
2026-04-16 12:58:05 -03:00
power_shell . append ( & power_row ) ;
connection_body . append ( & power_shell ) ;
let routing_heading = gtk ::Label ::new ( Some ( " Input Routing " ) ) ;
routing_heading . add_css_class ( " subgroup-title " ) ;
routing_heading . set_halign ( gtk ::Align ::Start ) ;
connection_body . append ( & gtk ::Separator ::new ( gtk ::Orientation ::Horizontal ) ) ;
connection_body . append ( & routing_heading ) ;
2026-04-14 23:03:18 -03:00
let routing_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
2026-04-16 12:58:05 -03:00
routing_row . set_homogeneous ( true ) ;
let input_toggle_button = gtk ::Button ::with_label ( " Change Routing " ) ;
2026-04-14 23:03:18 -03:00
input_toggle_button . set_hexpand ( true ) ;
2026-04-16 12:58:05 -03:00
stabilize_button ( & input_toggle_button , 128 ) ;
2026-04-14 23:03:18 -03:00
input_toggle_button . set_tooltip_text ( Some (
2026-04-16 12:58:05 -03:00
" Change live keyboard and mouse ownership between this machine and the remote target. " ,
2026-04-14 23:03:18 -03:00
) ) ;
2026-04-16 12:58:05 -03:00
let swap_key_button = gtk ::Button ::with_label ( " Set Swap Key " ) ;
stabilize_button ( & swap_key_button , 128 ) ;
2026-04-15 04:11:47 -03:00
routing_row . append ( & input_toggle_button ) ;
2026-04-15 04:44:06 -03:00
routing_row . append ( & swap_key_button ) ;
2026-04-16 12:58:05 -03:00
connection_body . append ( & routing_row ) ;
operations . append ( & connection_panel ) ;
2026-04-16 19:19:37 -03:00
let ( diagnostics_panel , diagnostics_body ) = build_panel ( " Diagnostics " ) ;
let diagnostics_toolbar = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
diagnostics_toolbar . set_homogeneous ( true ) ;
let diagnostics_copy_button = gtk ::Button ::with_label ( " Copy Report " ) ;
stabilize_button ( & diagnostics_copy_button , 112 ) ;
let diagnostics_popout_button = gtk ::Button ::with_label ( " Break Out " ) ;
stabilize_button ( & diagnostics_popout_button , 112 ) ;
diagnostics_toolbar . append ( & diagnostics_copy_button ) ;
diagnostics_toolbar . append ( & diagnostics_popout_button ) ;
let diagnostics_log = Rc ::new ( RefCell ::new ( DiagnosticsLog ::new ( 16 ) ) ) ;
let diagnostics_buffer = gtk ::TextBuffer ::new ( None ) ;
let diagnostics_view = gtk ::TextView ::with_buffer ( & diagnostics_buffer ) ;
diagnostics_view . add_css_class ( " status-log " ) ;
diagnostics_view . set_editable ( false ) ;
diagnostics_view . set_cursor_visible ( false ) ;
diagnostics_view . set_monospace ( true ) ;
diagnostics_view . set_wrap_mode ( gtk ::WrapMode ::WordChar ) ;
let diagnostics_scroll = gtk ::ScrolledWindow ::builder ( )
. hexpand ( true )
. vexpand ( false )
. min_content_height ( 190 )
. child ( & diagnostics_view )
. build ( ) ;
diagnostics_body . append ( & diagnostics_toolbar ) ;
diagnostics_body . append ( & diagnostics_scroll ) ;
operations . append ( & diagnostics_panel ) ;
2026-04-16 12:58:05 -03:00
let ( console_panel , console_body ) = build_panel ( " Session Console " ) ;
console_panel . set_vexpand ( true ) ;
let console_toolbar = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
console_toolbar . set_homogeneous ( true ) ;
let console_copy_button = gtk ::Button ::with_label ( " Copy Log " ) ;
stabilize_button ( & console_copy_button , 104 ) ;
let console_popout_button = gtk ::Button ::with_label ( " Break Out Log " ) ;
stabilize_button ( & console_popout_button , 104 ) ;
console_toolbar . append ( & console_copy_button ) ;
console_toolbar . append ( & console_popout_button ) ;
let status_label = gtk ::Label ::new ( Some ( " Session log ready. " ) ) ;
2026-04-14 23:03:18 -03:00
status_label . add_css_class ( " status-line " ) ;
status_label . set_halign ( gtk ::Align ::Start ) ;
2026-04-16 12:58:05 -03:00
status_label . set_wrap ( true ) ;
status_label . set_xalign ( 0.0 ) ;
let session_log_buffer = gtk ::TextBuffer ::new ( None ) ;
session_log_buffer . create_tag ( Some ( " log-launcher " ) , & [ ( " foreground " , & " #8bd5ca " ) ] ) ;
session_log_buffer . create_tag ( Some ( " log-relay " ) , & [ ( " foreground " , & " #89b4fa " ) ] ) ;
session_log_buffer . create_tag ( Some ( " log-preview " ) , & [ ( " foreground " , & " #cba6f7 " ) ] ) ;
session_log_buffer . create_tag ( Some ( " log-stderr " ) , & [ ( " foreground " , & " #f9e2af " ) ] ) ;
session_log_buffer . create_tag ( Some ( " log-warn " ) , & [ ( " foreground " , & " #fab387 " ) ] ) ;
session_log_buffer . create_tag ( Some ( " log-error " ) , & [ ( " foreground " , & " #f38ba8 " ) ] ) ;
super ::ui_runtime ::append_session_log ( & session_log_buffer , " [launcher] Session log ready. " ) ;
let session_log_view = gtk ::TextView ::with_buffer ( & session_log_buffer ) ;
session_log_view . add_css_class ( " status-log " ) ;
session_log_view . set_editable ( false ) ;
session_log_view . set_cursor_visible ( false ) ;
session_log_view . set_monospace ( true ) ;
session_log_view . set_wrap_mode ( gtk ::WrapMode ::WordChar ) ;
let log_scroll = gtk ::ScrolledWindow ::builder ( )
. hexpand ( true )
. vexpand ( true )
. min_content_height ( 220 )
. child ( & session_log_view )
. build ( ) ;
console_body . append ( & console_toolbar ) ;
console_body . append ( & log_scroll ) ;
operations . append ( & console_panel ) ;
{
let buffer = session_log_buffer . clone ( ) ;
let view = session_log_view . clone ( ) ;
status_label . connect_notify_local ( Some ( " label " ) , move | label , _ | {
super ::ui_runtime ::append_session_log ( & buffer , & format! ( " [launcher] {} " , label . text ( ) ) ) ;
let mut end = buffer . end_iter ( ) ;
view . scroll_to_iter ( & mut end , 0.0 , false , 0.0 , 1.0 ) ;
} ) ;
}
2026-04-14 23:03:18 -03:00
let preview = match LauncherPreview ::new ( server_addr . to_string ( ) ) {
Ok ( preview ) = > Some ( Rc ::new ( preview ) ) ,
Err ( err ) = > {
status_label . set_text ( & format! ( " Preview unavailable: {err} " ) ) ;
None
}
} ;
2026-04-16 12:58:05 -03:00
let left_pane = left_pane ;
let right_pane = right_pane ;
2026-04-14 23:03:18 -03:00
if let Some ( preview ) = preview . as_ref ( ) {
2026-04-19 03:28:23 -03:00
* left_pane . preview_binding . borrow_mut ( ) =
if state . feed_source_preset ( 0 ) = = FeedSourcePreset ::Off {
None
} else {
preview . install_on_picture (
0 ,
PreviewSurface ::Inline ,
& left_pane . picture ,
& left_pane . stream_status ,
)
} ;
* right_pane . preview_binding . borrow_mut ( ) =
if state . feed_source_preset ( 1 ) = = FeedSourcePreset ::Off {
None
} else {
preview . install_on_picture (
1 ,
PreviewSurface ::Inline ,
& right_pane . picture ,
& right_pane . stream_status ,
)
} ;
2026-04-14 23:03:18 -03:00
} else {
left_pane . stream_status . set_text ( " Preview unavailable " ) ;
right_pane . stream_status . set_text ( " Preview unavailable " ) ;
}
2026-04-19 03:28:23 -03:00
sync_feed_source_combo (
& left_pane . feed_source_combo ,
state . feed_source_options ( 0 ) ,
state . feed_source_preset ( 0 ) ,
2026-04-17 01:09:33 -03:00
) ;
2026-04-19 03:28:23 -03:00
sync_feed_source_combo (
& right_pane . feed_source_combo ,
state . feed_source_options ( 1 ) ,
state . feed_source_preset ( 1 ) ,
2026-04-17 01:09:33 -03:00
) ;
2026-04-19 03:28:23 -03:00
if state . feed_source_preset ( 0 ) ! = FeedSourcePreset ::Off {
2026-04-19 11:42:41 -03:00
let choice = state
. display_capture_size_choice ( 0 )
. unwrap_or_else ( | | state . capture_size_choice ( 0 ) ) ;
if state . feed_source_preset ( 0 ) = = FeedSourcePreset ::ThisEye {
sync_capture_resolution_combo (
& left_pane . capture_resolution_combo ,
state . capture_size_options ( ) ,
state . capture_size_preset ( 0 ) ,
) ;
} else {
sync_capture_resolution_locked (
& left_pane . capture_resolution_combo ,
state . capture_size_options ( ) ,
choice . preset ,
) ;
}
2026-04-19 03:28:23 -03:00
} else {
sync_capture_resolution_disabled ( & left_pane . capture_resolution_combo ) ;
}
if state . feed_source_preset ( 1 ) ! = FeedSourcePreset ::Off {
2026-04-19 11:42:41 -03:00
let choice = state
. display_capture_size_choice ( 1 )
. unwrap_or_else ( | | state . capture_size_choice ( 1 ) ) ;
if state . feed_source_preset ( 1 ) = = FeedSourcePreset ::ThisEye {
sync_capture_resolution_combo (
& right_pane . capture_resolution_combo ,
state . capture_size_options ( ) ,
state . capture_size_preset ( 1 ) ,
) ;
} else {
sync_capture_resolution_locked (
& right_pane . capture_resolution_combo ,
state . capture_size_options ( ) ,
choice . preset ,
) ;
}
2026-04-19 03:28:23 -03:00
} else {
sync_capture_resolution_disabled ( & right_pane . capture_resolution_combo ) ;
}
2026-04-16 12:58:05 -03:00
sync_breakout_size_combo (
& left_pane . breakout_combo ,
2026-04-19 14:14:14 -03:00
state . breakout_size_options ( 0 ) ,
2026-04-16 12:58:05 -03:00
state . breakout_size_preset ( 0 ) ,
) ;
sync_breakout_size_combo (
& right_pane . breakout_combo ,
2026-04-19 14:14:14 -03:00
state . breakout_size_options ( 1 ) ,
2026-04-16 12:58:05 -03:00
state . breakout_size_preset ( 1 ) ,
) ;
2026-04-14 23:03:18 -03:00
let widgets = LauncherWidgets {
status_label : status_label . clone ( ) ,
2026-04-16 19:19:37 -03:00
diagnostics_log : diagnostics_log . clone ( ) ,
diagnostics_buffer : diagnostics_buffer . clone ( ) ,
2026-04-16 12:58:05 -03:00
session_log_buffer : session_log_buffer . clone ( ) ,
session_log_view : session_log_view . clone ( ) ,
2026-04-14 23:03:18 -03:00
summary : SummaryWidgets {
2026-04-16 12:58:05 -03:00
relay_light ,
2026-04-14 23:03:18 -03:00
relay_value ,
2026-04-16 12:58:05 -03:00
routing_light ,
2026-04-14 23:03:18 -03:00
routing_value ,
2026-04-16 12:58:05 -03:00
gpio_light ,
gpio_value ,
2026-04-14 23:03:18 -03:00
shortcut_value ,
} ,
power_detail ,
2026-04-16 12:58:05 -03:00
audio_check_detail ,
audio_check_meter ,
2026-04-14 23:03:18 -03:00
display_panes : [ left_pane . clone ( ) , right_pane . clone ( ) ] ,
start_button : start_button . clone ( ) ,
2026-04-15 01:20:51 -03:00
power_auto_button : power_auto_button . clone ( ) ,
power_on_button : power_on_button . clone ( ) ,
power_off_button : power_off_button . clone ( ) ,
2026-04-14 23:03:18 -03:00
input_toggle_button : input_toggle_button . clone ( ) ,
clipboard_button : clipboard_button . clone ( ) ,
probe_button : probe_button . clone ( ) ,
2026-04-15 04:44:06 -03:00
swap_key_button : swap_key_button . clone ( ) ,
2026-04-14 23:03:18 -03:00
camera_test_button : camera_test_button . clone ( ) ,
microphone_test_button : microphone_test_button . clone ( ) ,
2026-04-16 12:58:05 -03:00
microphone_replay_button : microphone_replay_button . clone ( ) ,
2026-04-14 23:03:18 -03:00
speaker_test_button : speaker_test_button . clone ( ) ,
2026-04-16 19:19:37 -03:00
diagnostics_copy_button : diagnostics_copy_button . clone ( ) ,
diagnostics_popout_button : diagnostics_popout_button . clone ( ) ,
2026-04-16 12:58:05 -03:00
console_copy_button : console_copy_button . clone ( ) ,
console_popout_button : console_popout_button . clone ( ) ,
2026-04-14 23:03:18 -03:00
} ;
let popouts = Rc ::new ( RefCell ::new ( [ None , None ] ) ) ;
2026-04-16 19:19:37 -03:00
let diagnostics_popout = Rc ::new ( RefCell ::new ( None ) ) ;
2026-04-16 12:58:05 -03:00
let log_popout = Rc ::new ( RefCell ::new ( None ) ) ;
2026-04-14 23:03:18 -03:00
2026-04-16 19:19:37 -03:00
super ::ui_runtime ::refresh_diagnostics_report ( & widgets , state , false ) ;
2026-04-14 23:03:18 -03:00
window . set_child ( Some ( & root ) ) ;
LauncherView {
window ,
server_entry ,
camera_combo ,
microphone_combo ,
speaker_combo ,
2026-04-16 12:58:05 -03:00
keyboard_combo ,
mouse_combo ,
2026-04-15 01:20:51 -03:00
device_stage : DeviceStageWidgets {
camera_preview ,
camera_status ,
} ,
2026-04-14 23:03:18 -03:00
widgets ,
preview ,
popouts ,
2026-04-16 19:19:37 -03:00
diagnostics_popout ,
2026-04-16 12:58:05 -03:00
log_popout ,
2026-04-14 23:03:18 -03:00
}
}
pub fn install_css ( window : & gtk ::ApplicationWindow ) {
let provider = gtk ::CssProvider ::new ( ) ;
provider . load_from_data (
r #"
2026-04-16 12:58:05 -03:00
window . lesavka {
2026-04-14 23:03:18 -03:00
background : #101319 ;
color : #eef2f7 ;
}
box . launcher - root {
background : linear - gradient ( 180 deg , #11161 f 0 % , #161 d28 100 % ) ;
}
box . panel {
background : rgba ( 255 , 255 , 255 , 0.04 ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
border - radius : 18 px ;
2026-04-16 12:58:05 -03:00
padding : 10 px ;
}
box . subgroup {
background : rgba ( 255 , 255 , 255 , 0.025 ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.06 ) ;
border - radius : 14 px ;
padding : 8 px ;
2026-04-14 23:03:18 -03:00
}
label . panel - title {
font - weight : 700 ;
font - size : 1.05 rem ;
margin - bottom : 4 px ;
}
2026-04-16 12:58:05 -03:00
label . subgroup - title {
font - weight : 700 ;
opacity : 0.92 ;
}
2026-04-16 15:59:42 -03:00
label . version - tag {
font - size : 0.76 rem ;
opacity : 0.72 ;
margin - bottom : 3 px ;
}
2026-04-14 23:03:18 -03:00
box . status - chip {
background : rgba ( 91 , 179 , 162 , 0.12 ) ;
border : 1 px solid rgba ( 91 , 179 , 162 , 0.25 ) ;
border - radius : 999 px ;
2026-04-16 12:58:05 -03:00
padding : 7 px 10 px ;
}
box . status - light {
min - width : 10 px ;
min - height : 10 px ;
border - radius : 999 px ;
background : rgba ( 214 , 81 , 81 , 0.92 ) ;
}
box . status - light - live {
background : rgba ( 96 , 214 , 126 , 0.95 ) ;
}
box . status - light - idle {
background : rgba ( 214 , 81 , 81 , 0.92 ) ;
2026-04-14 23:03:18 -03:00
}
label . status - chip - label {
font - size : 0.78 rem ;
opacity : 0.72 ;
}
label . status - chip - value {
font - weight : 700 ;
}
box . display - card {
background : rgba ( 255 , 255 , 255 , 0.045 ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
border - radius : 22 px ;
padding : 16 px ;
}
box . display - placeholder {
background : rgba ( 255 , 255 , 255 , 0.03 ) ;
border : 1 px dashed rgba ( 255 , 255 , 255 , 0.18 ) ;
border - radius : 16 px ;
padding : 24 px ;
}
2026-04-15 01:20:51 -03:00
picture . camera - preview - frame {
background : rgba ( 0 , 0 , 0 , 0.28 ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.10 ) ;
border - radius : 14 px ;
}
2026-04-14 23:03:18 -03:00
label . status - line {
2026-04-16 12:58:05 -03:00
opacity : 0.9 ;
2026-04-14 23:03:18 -03:00
}
2026-04-16 12:58:05 -03:00
textview . status - log {
2026-04-15 01:57:14 -03:00
font - family : monospace ;
2026-04-16 12:58:05 -03:00
background : rgba ( 0 , 0 , 0 , 0.22 ) ;
2026-04-15 01:57:14 -03:00
border - radius : 14 px ;
2026-04-16 12:58:05 -03:00
padding : 10 px ;
}
progressbar . audio - check - meter trough {
min - height : 10 px ;
border - radius : 999 px ;
background : rgba ( 255 , 255 , 255 , 0.08 ) ;
}
progressbar . audio - check - meter progress {
border - radius : 999 px ;
background : rgba ( 91 , 179 , 162 , 0.88 ) ;
2026-04-15 01:57:14 -03:00
}
2026-04-14 23:03:18 -03:00
entry . server - entry {
min - height : 38 px ;
}
2026-04-15 01:20:51 -03:00
button . pill - toggle {
min - height : 36 px ;
padding : 0 14 px ;
}
2026-04-15 01:57:14 -03:00
button . pill - toggle - active {
background : rgba ( 91 , 179 , 162 , 0.2 ) ;
border - color : rgba ( 91 , 179 , 162 , 0.45 ) ;
font - weight : 700 ;
}
2026-04-14 23:03:18 -03:00
" #,
) ;
if let Some ( display ) = gtk ::gdk ::Display ::default ( ) {
gtk ::style_context_add_provider_for_display (
& display ,
& provider ,
gtk ::STYLE_PROVIDER_PRIORITY_APPLICATION ,
) ;
}
2026-04-16 12:58:05 -03:00
window . add_css_class ( " lesavka " ) ;
}
pub fn install_window_icon ( window : & impl IsA < gtk ::Window > ) {
if let Some ( display ) = gtk ::gdk ::Display ::default ( ) {
let theme = gtk ::IconTheme ::for_display ( & display ) ;
theme . add_search_path ( LESAVKA_ICON_SEARCH_PATH ) ;
}
gtk ::Window ::set_default_icon_name ( LESAVKA_ICON_NAME ) ;
window . as_ref ( ) . set_icon_name ( Some ( LESAVKA_ICON_NAME ) ) ;
2026-04-14 23:03:18 -03:00
}
fn build_panel ( title : & str ) -> ( gtk ::Box , gtk ::Box ) {
2026-04-16 12:58:05 -03:00
let panel = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
2026-04-14 23:03:18 -03:00
panel . add_css_class ( " panel " ) ;
let heading = gtk ::Label ::new ( Some ( title ) ) ;
heading . add_css_class ( " panel-title " ) ;
heading . set_halign ( gtk ::Align ::Start ) ;
panel . append ( & heading ) ;
2026-04-16 12:58:05 -03:00
let body = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
2026-04-14 23:03:18 -03:00
panel . append ( & body ) ;
( panel , body )
}
2026-04-16 12:58:05 -03:00
fn build_subgroup ( title : & str ) -> gtk ::Box {
let group = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
group . add_css_class ( " subgroup " ) ;
let heading = gtk ::Label ::new ( Some ( title ) ) ;
heading . add_css_class ( " subgroup-title " ) ;
heading . set_halign ( gtk ::Align ::Start ) ;
group . append ( & heading ) ;
group
}
2026-04-14 23:03:18 -03:00
fn build_status_chip ( label : & str , value : & str ) -> ( gtk ::Box , gtk ::Label ) {
2026-04-16 12:58:05 -03:00
let chip = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 4 ) ;
2026-04-14 23:03:18 -03:00
chip . add_css_class ( " status-chip " ) ;
let label_widget = gtk ::Label ::new ( Some ( label ) ) ;
label_widget . add_css_class ( " status-chip-label " ) ;
label_widget . set_halign ( gtk ::Align ::Start ) ;
let value_widget = gtk ::Label ::new ( Some ( value ) ) ;
value_widget . add_css_class ( " status-chip-value " ) ;
value_widget . set_halign ( gtk ::Align ::Start ) ;
chip . append ( & label_widget ) ;
chip . append ( & value_widget ) ;
( chip , value_widget )
}
2026-04-16 12:58:05 -03:00
fn build_status_chip_with_light ( label : & str , value : & str ) -> ( gtk ::Box , gtk ::Box , gtk ::Label ) {
let chip = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 4 ) ;
chip . add_css_class ( " status-chip " ) ;
let meta = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 6 ) ;
meta . add_css_class ( " status-chip-meta " ) ;
let light = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 0 ) ;
light . add_css_class ( " status-light " ) ;
light . add_css_class ( " status-light-idle " ) ;
let label_widget = gtk ::Label ::new ( Some ( label ) ) ;
label_widget . add_css_class ( " status-chip-label " ) ;
label_widget . set_halign ( gtk ::Align ::Start ) ;
meta . append ( & light ) ;
meta . append ( & label_widget ) ;
let value_widget = gtk ::Label ::new ( Some ( value ) ) ;
value_widget . add_css_class ( " status-chip-value " ) ;
value_widget . set_halign ( gtk ::Align ::Start ) ;
chip . append ( & meta ) ;
chip . append ( & value_widget ) ;
( chip , light , value_widget )
}
fn stabilize_chip ( chip : & gtk ::Box , width : i32 ) {
chip . set_size_request ( width , - 1 ) ;
}
2026-04-19 03:28:23 -03:00
pub fn sync_feed_source_combo (
combo : & gtk ::ComboBoxText ,
options : Vec < FeedSourceChoice > ,
selected : FeedSourcePreset ,
) {
combo . remove_all ( ) ;
for option in options {
combo . append ( Some ( option . preset . as_id ( ) ) , option . label ) ;
}
combo . set_active_id ( Some ( selected . as_id ( ) ) ) ;
combo . set_sensitive ( true ) ;
}
2026-04-17 01:09:33 -03:00
pub fn sync_capture_resolution_combo (
2026-04-16 12:58:05 -03:00
combo : & gtk ::ComboBoxText ,
options : Vec < CaptureSizeChoice > ,
selected : CaptureSizePreset ,
) {
combo . remove_all ( ) ;
2026-04-19 03:28:23 -03:00
let option_count = options . len ( ) ;
2026-04-16 12:58:05 -03:00
for option in options {
2026-04-19 03:28:23 -03:00
let label = format! (
" {} • {}x{} @ {} fps (Device H.264) " ,
option . preset . label ( ) ,
option . width ,
option . height ,
option . fps ,
) ;
2026-04-16 12:58:05 -03:00
combo . append ( Some ( option . preset . as_id ( ) ) , & label ) ;
}
combo . set_active_id ( Some ( selected . as_id ( ) ) ) ;
2026-04-19 03:28:23 -03:00
combo . set_sensitive ( option_count > 1 ) ;
}
2026-04-19 11:42:41 -03:00
pub fn sync_capture_resolution_locked (
combo : & gtk ::ComboBoxText ,
options : Vec < CaptureSizeChoice > ,
selected : CaptureSizePreset ,
) {
sync_capture_resolution_combo ( combo , options , selected ) ;
combo . set_sensitive ( false ) ;
}
2026-04-19 03:28:23 -03:00
pub fn sync_capture_resolution_disabled ( combo : & gtk ::ComboBoxText ) {
combo . remove_all ( ) ;
combo . append ( Some ( " off " ) , " Feed disabled " ) ;
combo . set_active_id ( Some ( " off " ) ) ;
combo . set_sensitive ( false ) ;
2026-04-16 12:58:05 -03:00
}
pub fn sync_breakout_size_combo (
combo : & gtk ::ComboBoxText ,
options : Vec < BreakoutSizeChoice > ,
selected : BreakoutSizePreset ,
) {
combo . remove_all ( ) ;
for option in options {
let label = match option . preset {
BreakoutSizePreset ::Source = > {
2026-04-16 22:15:59 -03:00
format! (
" {} • {}x{} (Source Size) " ,
option . preset . label ( ) ,
option . width ,
option . height
)
2026-04-16 12:58:05 -03:00
}
BreakoutSizePreset ::FillDisplay = > {
2026-04-16 22:15:59 -03:00
format! (
" {} • {}x{} (Display Size) " ,
option . preset . label ( ) ,
option . width ,
option . height
)
2026-04-16 12:58:05 -03:00
}
2026-04-16 22:15:59 -03:00
_ = > format! (
" {} • {}x{} " ,
option . preset . label ( ) ,
option . width ,
option . height
) ,
2026-04-16 12:58:05 -03:00
} ;
combo . append ( Some ( option . preset . as_id ( ) ) , & label ) ;
}
combo . set_active_id ( Some ( selected . as_id ( ) ) ) ;
}
2026-04-14 23:03:18 -03:00
fn attach_device_row (
grid : & gtk ::Grid ,
row : i32 ,
label : & str ,
combo : & gtk ::ComboBoxText ,
test_button : & gtk ::Button ,
) {
let label_widget = gtk ::Label ::new ( Some ( label ) ) ;
label_widget . set_halign ( gtk ::Align ::Start ) ;
combo . set_hexpand ( true ) ;
grid . attach ( & label_widget , 0 , row , 1 , 1 ) ;
grid . attach ( combo , 1 , row , 1 , 1 ) ;
grid . attach ( test_button , 2 , row , 1 , 1 ) ;
}
2026-04-16 12:58:05 -03:00
fn build_selector_block ( label : & str , combo : & gtk ::ComboBoxText ) -> gtk ::Box {
let block = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 6 ) ;
let label_widget = gtk ::Label ::new ( Some ( label ) ) ;
label_widget . set_halign ( gtk ::Align ::Start ) ;
combo . set_hexpand ( true ) ;
combo . set_size_request ( 0 , - 1 ) ;
block . append ( & label_widget ) ;
block . append ( combo ) ;
block
}
fn append_input_choice ( combo : & gtk ::ComboBoxText , value : & str ) {
let short = value . rsplit ( '/' ) . next ( ) . unwrap_or ( value ) ;
let label = Device ::open ( value )
. ok ( )
. and_then ( | device | device . name ( ) . map ( | name | format! ( " {name} • {short} " ) ) )
. unwrap_or_else ( | | short . to_string ( ) ) ;
combo . append ( Some ( value ) , & label ) ;
}
fn append_stage_choice ( combo : & gtk ::ComboBoxText , value : & str ) {
combo . append ( Some ( value ) , & compact_stage_label ( value ) ) ;
}
fn compact_stage_label ( value : & str ) -> String {
let trimmed = value . trim ( ) ;
if trimmed . is_empty ( ) {
return " auto " . to_string ( ) ;
}
if let Some ( short ) = trimmed . rsplit ( '/' ) . next ( )
& & short ! = trimmed
{
return shorten_label ( short ) ;
}
if let Some ( rest ) = trimmed
. strip_prefix ( " alsa_input. " )
. or_else ( | | trimmed . strip_prefix ( " alsa_output. " ) )
{
return shorten_label ( rest ) ;
}
shorten_label ( trimmed )
}
fn shorten_label ( value : & str ) -> String {
const MAX : usize = 44 ;
let compact = value . replace ( '_' , " " ) ;
let mut chars = compact . chars ( ) ;
let preview : String = chars . by_ref ( ) . take ( MAX ) . collect ( ) ;
if chars . next ( ) . is_some ( ) {
format! ( " {preview} … " )
} else {
preview
}
}
2026-04-14 23:03:18 -03:00
fn build_display_pane ( title : & str , capture_path : & str ) -> DisplayPaneWidgets {
let root = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 10 ) ;
root . add_css_class ( " display-card " ) ;
root . set_hexpand ( true ) ;
root . set_vexpand ( true ) ;
let title_label = gtk ::Label ::new ( Some ( title ) ) ;
title_label . add_css_class ( " title-4 " ) ;
title_label . set_halign ( gtk ::Align ::Start ) ;
let capture_label = gtk ::Label ::new ( Some ( capture_path ) ) ;
capture_label . add_css_class ( " dim-label " ) ;
capture_label . set_halign ( gtk ::Align ::Start ) ;
root . append ( & title_label ) ;
root . append ( & capture_label ) ;
let picture = gtk ::Picture ::new ( ) ;
picture . set_hexpand ( true ) ;
picture . set_vexpand ( true ) ;
2026-04-17 13:51:26 -03:00
picture . set_halign ( gtk ::Align ::Fill ) ;
picture . set_valign ( gtk ::Align ::Fill ) ;
2026-04-14 23:03:18 -03:00
picture . set_can_shrink ( true ) ;
2026-04-17 11:51:19 -03:00
picture . set_keep_aspect_ratio ( true ) ;
2026-04-16 12:58:05 -03:00
picture . set_size_request ( 220 , 124 ) ;
2026-04-14 23:03:18 -03:00
let preview_box = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 0 ) ;
2026-04-17 13:51:26 -03:00
preview_box . set_hexpand ( true ) ;
preview_box . set_vexpand ( true ) ;
preview_box . set_halign ( gtk ::Align ::Fill ) ;
preview_box . set_valign ( gtk ::Align ::Fill ) ;
preview_box . set_size_request ( 220 , 124 ) ;
let preview_frame = gtk ::AspectFrame ::new ( 0.5 , 0.5 , 16.0 / 9.0 , false ) ;
preview_frame . set_hexpand ( true ) ;
preview_frame . set_vexpand ( true ) ;
preview_frame . set_halign ( gtk ::Align ::Fill ) ;
preview_frame . set_valign ( gtk ::Align ::Fill ) ;
preview_frame . set_size_request ( 220 , 124 ) ;
preview_frame . set_child ( Some ( & picture ) ) ;
preview_box . append ( & preview_frame ) ;
2026-04-14 23:03:18 -03:00
let placeholder = gtk ::Label ::new ( Some (
" This feed is running in its own window. \n Use 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 placeholder_box = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 6 ) ;
placeholder_box . add_css_class ( " display-placeholder " ) ;
placeholder_box . set_hexpand ( true ) ;
placeholder_box . set_vexpand ( true ) ;
2026-04-16 12:58:05 -03:00
placeholder_box . set_size_request ( 220 , 124 ) ;
2026-04-14 23:03:18 -03:00
placeholder_box . append ( & placeholder ) ;
let stack = gtk ::Stack ::new ( ) ;
stack . set_hexpand ( true ) ;
stack . set_vexpand ( true ) ;
stack . add_named ( & preview_box , Some ( " preview " ) ) ;
stack . add_named ( & placeholder_box , Some ( " placeholder " ) ) ;
stack . set_visible_child_name ( " preview " ) ;
root . append ( & stack ) ;
2026-04-15 04:11:47 -03:00
let stream_status = gtk ::Label ::new ( Some ( " Connect relay to preview. " ) ) ;
2026-04-14 23:03:18 -03:00
stream_status . set_halign ( gtk ::Align ::Start ) ;
stream_status . set_hexpand ( true ) ;
2026-04-16 12:58:05 -03:00
stream_status . set_ellipsize ( pango ::EllipsizeMode ::End ) ;
stream_status . set_single_line_mode ( true ) ;
stream_status . set_max_width_chars ( 24 ) ;
stream_status . set_tooltip_text ( Some ( " Connect relay to preview. " ) ) ;
2026-04-19 03:28:23 -03:00
let feed_source_combo = gtk ::ComboBoxText ::new ( ) ;
feed_source_combo . set_tooltip_text ( Some (
" Choose which physical eye feed appears in this pane. Off disables the pane; the opposite-eye option mirrors the other physical feed while preserving a separate stream load for realistic validation. " ,
) ) ;
2026-04-19 04:24:27 -03:00
feed_source_combo . set_size_request ( 118 , - 1 ) ;
2026-04-17 01:09:33 -03:00
let capture_resolution_combo = gtk ::ComboBoxText ::new ( ) ;
capture_resolution_combo . set_tooltip_text ( Some (
2026-04-19 03:28:23 -03:00
" Choose the eye-stream source mode for this feed. Source keeps the HDMI device's own H.264 stream; cheaper source-device modes will appear here once the hardware proves it supports them. " ,
2026-04-17 01:09:33 -03:00
) ) ;
2026-04-19 04:24:27 -03:00
capture_resolution_combo . set_size_request ( 0 , - 1 ) ;
capture_resolution_combo . set_hexpand ( true ) ;
2026-04-16 12:58:05 -03:00
let breakout_combo = gtk ::ComboBoxText ::new ( ) ;
breakout_combo . set_tooltip_text ( Some (
2026-04-16 22:15:59 -03:00
" Choose the client-side breakout window size for this eye feed. Source Size preserves the feed's own dimensions; Display Size fills the effective monitor size. " ,
2026-04-16 12:58:05 -03:00
) ) ;
2026-04-19 04:24:27 -03:00
breakout_combo . set_size_request ( 0 , - 1 ) ;
breakout_combo . set_hexpand ( true ) ;
2026-04-14 23:03:18 -03:00
let action_button = gtk ::Button ::with_label ( " Break Out " ) ;
2026-04-16 12:58:05 -03:00
stabilize_button ( & action_button , 104 ) ;
2026-04-14 23:03:18 -03:00
action_button . set_halign ( gtk ::Align ::End ) ;
2026-04-17 01:09:33 -03:00
let footer_shell = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 6 ) ;
2026-04-19 04:24:27 -03:00
let controls_grid = gtk ::Grid ::new ( ) ;
controls_grid . set_column_spacing ( 8 ) ;
controls_grid . set_row_spacing ( 8 ) ;
controls_grid . set_hexpand ( true ) ;
controls_grid . attach ( & feed_source_combo , 0 , 0 , 1 , 1 ) ;
controls_grid . attach ( & capture_resolution_combo , 1 , 0 , 1 , 1 ) ;
2026-04-19 14:33:52 -03:00
controls_grid . attach ( & breakout_combo , 0 , 1 , 1 , 1 ) ;
controls_grid . attach ( & action_button , 1 , 1 , 1 , 1 ) ;
2026-04-19 04:24:27 -03:00
footer_shell . append ( & controls_grid ) ;
2026-04-17 01:09:33 -03:00
root . append ( & footer_shell ) ;
2026-04-14 23:03:18 -03:00
DisplayPaneWidgets {
root ,
stack ,
2026-04-19 15:07:24 -03:00
preview_frame ,
2026-04-14 23:03:18 -03:00
picture ,
stream_status ,
placeholder ,
2026-04-19 03:28:23 -03:00
feed_source_combo ,
2026-04-17 01:09:33 -03:00
capture_resolution_combo ,
2026-04-16 12:58:05 -03:00
breakout_combo ,
2026-04-14 23:03:18 -03:00
action_button ,
2026-04-16 12:58:05 -03:00
preview_binding : Rc ::new ( RefCell ::new ( None ) ) ,
2026-04-14 23:03:18 -03:00
title : title . to_string ( ) ,
}
}
2026-04-16 12:58:05 -03:00
fn stabilize_button ( button : & gtk ::Button , width : i32 ) {
button . set_size_request ( width , 36 ) ;
}