Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7ea1fd9df | ||
|
|
7a03dee12f |
Generated
+7
@@ -4176,6 +4176,7 @@ dependencies = [
|
||||
"nix 0.30.1",
|
||||
"notify-rust",
|
||||
"pipewire",
|
||||
"qrcode",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
@@ -4420,6 +4421,12 @@ version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
|
||||
+6
-1
@@ -55,6 +55,11 @@ egui_glow = { version = "0.34.2", default-features = false, features = ["winit",
|
||||
winit = { version = "0.30", default-features = false, features = ["rwh_06", "x11", "wayland", "wayland-dlopen"], optional = true }
|
||||
glutin = { version = "0.32", optional = true }
|
||||
glutin-winit = { version = "0.5", optional = true }
|
||||
# QR-encode the host ticket so a phone (or a second laptop with a webcam) can
|
||||
# pick it up without typing 140 chars. default-features = false to skip the
|
||||
# `image` crate dep tree — we render the modules to an `egui::ColorImage`
|
||||
# directly.
|
||||
qrcode = { version = "0.14", default-features = false, optional = true }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
@@ -64,4 +69,4 @@ strip = "symbols"
|
||||
[features]
|
||||
# Opt-in graphical front-end (pixelpass --gui). Default-off so the headless
|
||||
# build never pulls the GUI toolkit tree.
|
||||
gui = ["dep:eframe", "dep:notify-rust", "dep:ksni", "dep:egui_glow", "dep:winit", "dep:glutin", "dep:glutin-winit"]
|
||||
gui = ["dep:eframe", "dep:notify-rust", "dep:ksni", "dep:egui_glow", "dep:winit", "dep:glutin", "dep:glutin-winit", "dep:qrcode"]
|
||||
|
||||
+69
-30
@@ -66,8 +66,11 @@ use winit::window::{Window, WindowAttributes, WindowId};
|
||||
use self::child::{ChildEvent, ChildProc};
|
||||
use self::tray::{TrayAction, TrayHandle, TrayStatus};
|
||||
|
||||
/// Initial / minimum window size, in logical points.
|
||||
const INNER_SIZE: [f64; 2] = [520.0, 480.0];
|
||||
/// Initial / minimum window size, in logical points. Initial height fits the
|
||||
/// host screen (ticket + Copy + QR + Stop) without needing to scroll on a 1080p
|
||||
/// display; the host screen still wraps in a ScrollArea for the case where the
|
||||
/// user shrinks the window below MIN_INNER_SIZE proportions.
|
||||
const INNER_SIZE: [f64; 2] = [520.0, 640.0];
|
||||
const MIN_INNER_SIZE: [f64; 2] = [460.0, 380.0];
|
||||
|
||||
/// Events delivered to the winit loop from off the UI thread (or from egui).
|
||||
@@ -323,8 +326,11 @@ impl Gfx {
|
||||
fn show(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
|
||||
match std::mem::replace(&mut self.win, WinState::Between) {
|
||||
WinState::Hidden { context } => {
|
||||
let window =
|
||||
glutin_winit::finalize_window(event_loop, window_attributes(), &self.gl_config)?;
|
||||
let window = glutin_winit::finalize_window(
|
||||
event_loop,
|
||||
window_attributes(),
|
||||
&self.gl_config,
|
||||
)?;
|
||||
let surface = create_surface(&self.gl_display, &self.gl_config, &window)?;
|
||||
let context = context
|
||||
.make_current(&surface)
|
||||
@@ -382,7 +388,8 @@ impl Gfx {
|
||||
viewport_output,
|
||||
} = eg.egui_ctx.run_ui(raw_input, run_ui);
|
||||
|
||||
eg.egui_winit.handle_platform_output(window, platform_output);
|
||||
eg.egui_winit
|
||||
.handle_platform_output(window, platform_output);
|
||||
|
||||
// Apply any window commands egui emitted (we issue none directly, but
|
||||
// egui may request e.g. IME changes) and read the next repaint delay.
|
||||
@@ -466,11 +473,7 @@ impl App {
|
||||
/// actually present, otherwise quit.
|
||||
fn on_close(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let hide = self.state.close_to_tray
|
||||
&& self
|
||||
.state
|
||||
.tray
|
||||
.as_ref()
|
||||
.is_some_and(TrayHandle::registered);
|
||||
&& self.state.tray.as_ref().is_some_and(TrayHandle::registered);
|
||||
if hide {
|
||||
if let Some(gfx) = self.gfx.as_mut() {
|
||||
gfx.hide();
|
||||
@@ -496,12 +499,7 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
if matches!(event, WindowEvent::CloseRequested) {
|
||||
self.on_close(event_loop);
|
||||
return;
|
||||
@@ -788,6 +786,10 @@ struct HostState {
|
||||
/// Endpoint ids of the currently-connected viewers, in arrival order.
|
||||
/// Drives the per-viewer list and its Kick buttons.
|
||||
viewers: Vec<String>,
|
||||
/// QR-code rendering of the current ticket, lazily built on the first draw
|
||||
/// after a Ticket event lands. Cleared on session start/stop so the next
|
||||
/// session's ticket regenerates it.
|
||||
qr_texture: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
/// The host config summary echoed back by the child's `host_info` event.
|
||||
@@ -960,11 +962,16 @@ impl PixelPassApp {
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
if running {
|
||||
self.host_running(ui);
|
||||
} else {
|
||||
self.host_form(ui);
|
||||
}
|
||||
// Scroll the body, not the header — Menu/Host stay pinned at the top.
|
||||
// Needed once the QR panel landed: ticket + Copy + QR + Stop overflows a
|
||||
// shrunken window.
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
if running {
|
||||
self.host_running(ui);
|
||||
} else {
|
||||
self.host_form(ui);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn host_form(&mut self, ui: &mut egui::Ui) {
|
||||
@@ -1116,14 +1123,42 @@ impl PixelPassApp {
|
||||
.weak(),
|
||||
);
|
||||
}
|
||||
|
||||
// Lazy QR build: first draw after a Ticket event has `qr_texture =
|
||||
// None`, so we encode the ticket and load the texture once. The
|
||||
// 4-module quiet zone (white border) matters — phone scanners reject
|
||||
// QR codes flush against a non-white edge.
|
||||
if self.host.qr_texture.is_none()
|
||||
&& let Ok(code) = qrcode::QrCode::new(ticket.as_bytes())
|
||||
{
|
||||
let w = code.width();
|
||||
let quiet = 4;
|
||||
let full = w + 2 * quiet;
|
||||
let colors = code.into_colors();
|
||||
let mut pixels = vec![egui::Color32::WHITE; full * full];
|
||||
for y in 0..w {
|
||||
for x in 0..w {
|
||||
if colors[y * w + x] == qrcode::Color::Dark {
|
||||
pixels[(y + quiet) * full + (x + quiet)] = egui::Color32::BLACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
let img = egui::ColorImage::new([full, full], pixels);
|
||||
self.host.qr_texture = Some(ui.ctx().load_texture(
|
||||
"host_qr",
|
||||
img,
|
||||
egui::TextureOptions::NEAREST,
|
||||
));
|
||||
}
|
||||
if let Some(tex) = &self.host.qr_texture {
|
||||
ui.add_space(8.0);
|
||||
ui.add(egui::Image::new(tex).fit_to_exact_size(egui::vec2(200.0, 200.0)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(reason) = &self.host.last_refusal {
|
||||
ui.add_space(8.0);
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(220, 160, 60),
|
||||
format!("⚠ {reason}"),
|
||||
);
|
||||
ui.colored_label(egui::Color32::from_rgb(220, 160, 60), format!("⚠ {reason}"));
|
||||
}
|
||||
|
||||
ui.add_space(16.0);
|
||||
@@ -1145,6 +1180,7 @@ impl PixelPassApp {
|
||||
self.host.capturing = false;
|
||||
self.host.copied = false;
|
||||
self.host.viewers.clear();
|
||||
self.host.qr_texture = None;
|
||||
|
||||
let mut args = vec![
|
||||
"--host".to_string(),
|
||||
@@ -1177,6 +1213,7 @@ impl PixelPassApp {
|
||||
self.host.ticket = None;
|
||||
self.host.copied = false;
|
||||
self.host.viewers.clear();
|
||||
self.host.qr_texture = None;
|
||||
}
|
||||
|
||||
/// Drain the host child's event channel into state, and detect an
|
||||
@@ -1243,7 +1280,10 @@ impl PixelPassApp {
|
||||
if !self.host.viewers.contains(&id) {
|
||||
notify(
|
||||
"PixelPass — viewer connected",
|
||||
format!("endpoint {} is now watching ({active}/{max})", short_id(&id)),
|
||||
format!(
|
||||
"endpoint {} is now watching ({active}/{max})",
|
||||
short_id(&id)
|
||||
),
|
||||
);
|
||||
self.host.viewers.push(id);
|
||||
}
|
||||
@@ -1491,10 +1531,9 @@ mod tests {
|
||||
fn sample_ticket() -> (String, String) {
|
||||
let sk = iroh::SecretKey::from_bytes(&[7u8; 32]);
|
||||
let id = sk.public().to_string();
|
||||
let ticket = iroh_tickets::endpoint::EndpointTicket::new(iroh::EndpointAddr::new(
|
||||
sk.public(),
|
||||
))
|
||||
.to_string();
|
||||
let ticket =
|
||||
iroh_tickets::endpoint::EndpointTicket::new(iroh::EndpointAddr::new(sk.public()))
|
||||
.to_string();
|
||||
(ticket, id)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user