gui: QR-code panel for the host ticket
Encodes the relay-only ticket as a QR with a 4-module quiet zone so a phone (or a second laptop with a webcam) can pick the room up without typing 140+ characters. Built lazily on the first draw after a Ticket event, NEAREST-filtered, 200x200 logical; cleared on session start and stop. Pulls `qrcode` 0.14 with `default-features = false` so the heavy `image` crate tree is skipped — we render modules straight to an `egui::ColorImage` ourselves. Reapplies the idea from Gemini's stale `feat/gemini-branch-qrcode` (`7f07583`) against the post-hand-rolled-loop GUI; the original commit no longer cherry-picks because gui/mod.rs was rewritten for the true-Wayland-window-hide work. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+54
-23
@@ -323,8 +323,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 +385,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 +470,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 +496,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 +783,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.
|
||||
@@ -1116,14 +1115,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 +1172,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 +1205,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 +1272,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 +1523,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