feat(gui): scaffold egui window behind the gui feature

Adds the opt-in graphical front-end (pixelpass --gui), default-off via the
`gui` cargo feature so the headless build never pulls the toolkit tree.
eframe 0.34 on the glow/OpenGL backend (no wgpu); 69 feature-gated crates,
vetted. --gui on a headless build errors with a rebuild hint.

This commit is just the shell: a window with a Host/View menu and back
navigation. The shell-out child-spawning + JSON event parsing that drives
real host/viewer controls come next. Window verified to open and render
cleanly on Wayland (glow).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 16:26:56 -04:00
parent e7ded10db8
commit 6f0fd088f6
5 changed files with 1805 additions and 67 deletions
+106
View File
@@ -0,0 +1,106 @@
//! Graphical front-end (`pixelpass --gui`), compiled only with the `gui`
//! feature.
//!
//! Architecture: this window is a thin **shell-out** driver. It never touches
//! the capture / portal / gst / iroh machinery directly — instead it re-execs
//! this same binary in headless mode (`pixelpass --host --output json …` or
//! `pixelpass <ticket> --output json`) as a child process and parses the
//! child's JSON event stream (see [`crate::common::output`]) to drive what it
//! shows. That keeps the fragile capture stack sealed in a separate process:
//! the GUI can be closed or crash without taking a live stream down.
use eframe::egui;
/// Launch the GUI event loop. Blocks until the window is closed. Runs on the
/// main thread (a winit requirement), which is where `main` calls it from.
pub fn run() -> anyhow::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([520.0, 420.0])
.with_min_inner_size([420.0, 320.0])
.with_title("PixelPass"),
..Default::default()
};
eframe::run_native(
"PixelPass",
options,
Box::new(|_cc| Ok(Box::new(PixelPassApp::default()))),
)
.map_err(|e| anyhow::anyhow!("GUI failed to start: {e}"))
}
/// Which screen the single window is currently showing.
#[derive(Default, PartialEq)]
enum Screen {
#[default]
Menu,
Host,
Viewer,
}
#[derive(Default)]
struct PixelPassApp {
screen: Screen,
}
impl eframe::App for PixelPassApp {
// eframe 0.34 hands us the central-panel `ui` directly (it wraps this in a
// CentralPanel for us).
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
match self.screen {
Screen::Menu => self.menu(ui),
Screen::Host => self.host(ui),
Screen::Viewer => self.viewer(ui),
}
}
}
impl PixelPassApp {
fn menu(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.add_space(24.0);
ui.heading("PixelPass");
ui.label("P2P screen sharing");
ui.add_space(32.0);
if ui
.add_sized([240.0, 40.0], egui::Button::new("Host — share my screen"))
.clicked()
{
self.screen = Screen::Host;
}
ui.add_space(8.0);
if ui
.add_sized(
[240.0, 40.0],
egui::Button::new("View — watch someone's screen"),
)
.clicked()
{
self.screen = Screen::Viewer;
}
});
}
fn host(&mut self, ui: &mut egui::Ui) {
self.back_bar(ui, "Host");
ui.separator();
ui.label("Host controls land in the next step.");
}
fn viewer(&mut self, ui: &mut egui::Ui) {
self.back_bar(ui, "View");
ui.separator();
ui.label("Viewer controls land in a later step.");
}
/// Title row with a back-to-menu button, shared by the host/viewer screens.
fn back_bar(&mut self, ui: &mut egui::Ui, title: &str) {
ui.horizontal(|ui| {
if ui.button("← Menu").clicked() {
self.screen = Screen::Menu;
}
ui.heading(title);
});
}
}