{ let (connection_panel, connection_body) = build_panel("Relay Controls"); let server_entry = gtk::Entry::new(); server_entry.add_css_class("server-entry"); server_entry.set_hexpand(true); server_entry.set_width_chars(14); server_entry.set_text(server_addr); server_entry.set_tooltip_text(Some("Relay host address.")); let relay_grid = gtk::Grid::new(); relay_grid.set_column_homogeneous(true); relay_grid.set_column_spacing(6); relay_grid.set_hexpand(true); relay_grid.set_row_spacing(8); relay_grid.attach(&server_entry, 0, 0, 2, 1); let start_button = rail_button("Connect", "Start or stop relay."); start_button.add_css_class("suggested-action"); relay_grid.attach(&start_button, 2, 0, 1, 1); connection_body.append(&relay_grid); let recovery_heading = gtk::Label::new(Some("Recovery")); recovery_heading.add_css_class("subgroup-title"); recovery_heading.set_halign(gtk::Align::Start); let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); recovery_row.set_hexpand(true); recovery_heading.set_width_chars(10); recovery_row.append(&recovery_heading); let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8); recovery_buttons.set_hexpand(true); recovery_buttons.set_homogeneous(true); let usb_recover_button = rail_button("Recover USB", "Re-enumerate remote USB gadget."); let uac_recover_button = rail_button("Recover UAC", "Rebuild remote USB audio function."); let uvc_recover_button = rail_button("Recover UVC", "Rebuild remote USB webcam function."); recovery_buttons.append(&usb_recover_button); recovery_buttons.append(&uac_recover_button); recovery_buttons.append(&uvc_recover_button); recovery_row.append(&recovery_buttons); connection_body.append(&recovery_row); let tools_heading = gtk::Label::new(Some("Tools")); tools_heading.add_css_class("subgroup-title"); tools_heading.set_halign(gtk::Align::Start); let tools_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); tools_row.set_hexpand(true); tools_heading.set_width_chars(10); tools_row.append(&tools_heading); let tools_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8); tools_buttons.set_hexpand(true); tools_buttons.set_homogeneous(true); let clipboard_button = rail_button("Clipboard", "Type clipboard remotely."); tools_buttons.append(&clipboard_button); tools_row.append(&tools_buttons); connection_body.append(&tools_row); connection_body.append(>k::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); let power_shell = gtk::Box::new(gtk::Orientation::Vertical, 6); power_shell.set_halign(gtk::Align::Fill); let power_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); power_row.set_hexpand(true); power_heading.set_width_chars(10); power_row.append(&power_heading); let power_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8); power_buttons.set_hexpand(true); power_buttons.set_homogeneous(true); let power_on_button = gtk::Button::with_label("On"); power_on_button.set_hexpand(true); stabilize_button(&power_on_button, 52); power_on_button.add_css_class("pill-toggle"); let power_auto_button = gtk::Button::with_label("Auto"); power_auto_button.set_hexpand(true); stabilize_button(&power_auto_button, 52); power_auto_button.add_css_class("pill-toggle"); let power_off_button = gtk::Button::with_label("Off"); power_off_button.set_hexpand(true); stabilize_button(&power_off_button, 52); power_off_button.add_css_class("pill-toggle"); 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); power_buttons.append(&power_on_button); power_buttons.append(&power_auto_button); power_buttons.append(&power_off_button); power_row.append(&power_buttons); power_shell.append(&power_row); connection_body.append(&power_shell); let routing_heading = gtk::Label::new(Some("Inputs")); routing_heading.add_css_class("subgroup-title"); routing_heading.set_halign(gtk::Align::Start); connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal)); let routing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); routing_row.set_hexpand(true); routing_heading.set_width_chars(10); routing_row.append(&routing_heading); let routing_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8); routing_buttons.set_hexpand(true); routing_buttons.set_homogeneous(true); let input_toggle_button = gtk::Button::with_label("Route"); input_toggle_button.set_hexpand(true); stabilize_button(&input_toggle_button, 106); input_toggle_button.set_tooltip_text(Some("Swap input ownership.")); let swap_key_button = gtk::Button::with_label("Set Swap Key"); swap_key_button.set_hexpand(true); stabilize_button(&swap_key_button, 106); routing_buttons.append(&input_toggle_button); routing_buttons.append(&swap_key_button); routing_row.append(&routing_buttons); connection_body.append(&routing_row); operations.append(&connection_panel); let (diagnostics_panel, diagnostics_body) = build_panel("Diagnostics"); diagnostics_panel.set_vexpand(true); diagnostics_panel.set_valign(gtk::Align::Fill); diagnostics_body.set_vexpand(true); 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_label = gtk::Label::new(None); diagnostics_label.add_css_class("status-log"); diagnostics_label.set_selectable(true); diagnostics_label.set_xalign(0.0); diagnostics_label.set_yalign(0.0); diagnostics_label.set_wrap(true); diagnostics_label.set_wrap_mode(pango::WrapMode::WordChar); diagnostics_label.set_halign(gtk::Align::Start); diagnostics_label.set_valign(gtk::Align::Start); diagnostics_label.set_hexpand(true); diagnostics_label.set_width_chars(1); let diagnostics_shell = gtk::Box::new(gtk::Orientation::Vertical, 0); diagnostics_shell.set_hexpand(true); diagnostics_shell.set_vexpand(false); diagnostics_shell.append(&diagnostics_label); let diagnostics_scroll = gtk::ScrolledWindow::builder() .hexpand(true) .vexpand(true) .min_content_height(SIDE_LOG_RECOVERY_MIN_HEIGHT) .child(&diagnostics_shell) .build(); diagnostics_scroll.set_propagate_natural_width(false); diagnostics_body.append(&diagnostics_toolbar); diagnostics_body.append(&diagnostics_scroll); operations.append(&diagnostics_panel); let (console_panel, console_body) = build_panel("Log"); console_panel.set_vexpand(true); console_panel.set_valign(gtk::Align::Fill); console_body.set_vexpand(true); let console_toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 8); let session_log_level = Rc::new(RefCell::new(ConsoleLogLevel::default())); let console_level_combo = gtk::ComboBoxText::new(); for level in ConsoleLogLevel::ALL { console_level_combo.append(Some(level.id()), level.label()); } console_level_combo.set_active_id(Some(ConsoleLogLevel::default().id())); console_level_combo.set_size_request(78, 36); console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher.")); let console_copy_button = gtk::Button::with_label("Copy"); console_copy_button.set_tooltip_text(Some("Copy visible log.")); let console_popout_button = gtk::Button::with_label("Pop Out"); console_popout_button.set_tooltip_text(Some("Open log window.")); let console_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8); console_buttons.set_hexpand(true); console_buttons.set_homogeneous(true); console_copy_button.set_hexpand(true); console_popout_button.set_hexpand(true); console_buttons.append(&console_copy_button); console_buttons.append(&console_popout_button); console_toolbar.append(&console_level_combo); console_toolbar.append(&console_buttons); let status_label = gtk::Label::new(Some("Session log ready.")); status_label.add_css_class("status-line"); status_label.set_halign(gtk::Align::Start); 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(SIDE_LOG_RECOVERY_MIN_HEIGHT) .child(&session_log_view) .build(); log_scroll.set_propagate_natural_width(false); 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); }); } 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 } }; OperationsRailContext { server_entry, start_button, clipboard_button, usb_recover_button, uac_recover_button, uvc_recover_button, power_auto_button, power_on_button, power_off_button, power_detail, input_toggle_button, swap_key_button, diagnostics_copy_button, diagnostics_popout_button, diagnostics_log, diagnostics_label, diagnostics_scroll, console_copy_button, console_popout_button, console_level_combo, session_log_level, status_label, session_log_buffer, session_log_view, preview, } }