{ 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(18); server_entry.set_text(server_addr); server_entry.set_tooltip_text(Some("Relay host address.")); let relay_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); relay_row.set_halign(gtk::Align::Fill); relay_row.set_hexpand(true); relay_row.append(&server_entry); let start_button = gtk::Button::with_label("Connect"); start_button.add_css_class("suggested-action"); start_button.set_hexpand(false); stabilize_button(&start_button, 108); relay_row.append(&start_button); connection_body.append(&relay_row); let live_actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); live_actions_row.set_homogeneous(true); let clipboard_button = gtk::Button::with_label("Send Clipboard"); clipboard_button.set_hexpand(true); stabilize_button(&clipboard_button, 108); clipboard_button.set_tooltip_text(Some("Type clipboard remotely.")); let probe_button = gtk::Button::with_label("Copy Gate Probe"); probe_button.set_hexpand(true); stabilize_button(&probe_button, 108); probe_button.set_tooltip_text(Some("Copy quality probe.")); let usb_recover_button = gtk::Button::with_label("Recover USB"); usb_recover_button.set_hexpand(true); stabilize_button(&usb_recover_button, 108); usb_recover_button.set_tooltip_text(Some("Re-enumerate remote USB.")); live_actions_row.append(&clipboard_button); live_actions_row.append(&probe_button); live_actions_row.append(&usb_recover_button); connection_body.append(&live_actions_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(false); diagnostics_label.set_halign(gtk::Align::Start); diagnostics_label.set_valign(gtk::Align::Start); diagnostics_label.set_hexpand(true); 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_MIN_HEIGHT) .child(&diagnostics_shell) .build(); diagnostics_body.append(&diagnostics_toolbar); diagnostics_body.append(&diagnostics_scroll); operations.append(&diagnostics_panel); let (console_panel, console_body) = build_panel("Session Console"); 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_MIN_HEIGHT) .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); }); } 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, probe_button, usb_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, } }