diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 03f4083..c25c5a9 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -43,6 +43,15 @@ fn set_clipboard(text: &str) -> bool { .is_ok() } +/// Best-effort clipboard read, backing the viewer Paste button and the +/// View-screen prefill. `None` on a flaky/empty clipboard — callers just leave +/// the field untouched. +fn get_clipboard() -> Option { + arboard::Clipboard::new() + .and_then(|mut cb| cb.get_text()) + .ok() +} + /// Decode the host endpoint id from a ticket string, using the exact same /// parse the viewer does (`EndpointTicket::from_str`), so the GUI agrees with /// the child about what's a valid code. `None` means it isn't a pixelpass @@ -233,6 +242,11 @@ impl PixelPassApp { ui.add_space(24.0); ui.heading("PixelPass"); ui.label("P2P screen sharing"); + ui.label( + egui::RichText::new(concat!("v", env!("CARGO_PKG_VERSION"))) + .small() + .weak(), + ); ui.add_space(32.0); if ui .add_sized([260.0, 40.0], egui::Button::new("Host — share my screen")) @@ -249,6 +263,7 @@ impl PixelPassApp { .clicked() { self.screen = Screen::Viewer; + self.prefill_viewer_ticket(); } }); } @@ -580,11 +595,26 @@ impl PixelPassApp { ui.label("Paste the share code you received:"); ui.add_space(4.0); - ui.add( - egui::TextEdit::singleline(&mut self.viewer.ticket_input) - .desired_width(f32::INFINITY) - .hint_text("endpoint…"), - ); + // Field fills the row, with a Paste button pinned to its right — the + // read-side mirror of the host's Copy button (one click grabs the code + // the host just put on the clipboard). + let ticket_resp = ui + .with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("📋 Paste").clicked() + && let Some(text) = get_clipboard() + { + self.viewer.ticket_input = text.trim().to_string(); + } + ui.add( + egui::TextEdit::singleline(&mut self.viewer.ticket_input) + .desired_width(f32::INFINITY) + .hint_text("endpoint…"), + ) + }) + .inner; + // Enter in the field connects (gated on a decodable code below). + let enter_pressed = + ticket_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); // Decode the pasted code live: confirms it's a real ticket and shows // which host it points at, so a stale clipboard paste is caught here @@ -621,14 +651,14 @@ impl PixelPassApp { ui.add_space(16.0); // Only dial a code that actually decodes — no point spawning a child to - // spend 15s timing out against garbage. - if ui + // spend 15s timing out against garbage. Enter takes the same path. + let connect_clicked = ui .add_enabled( decoded_id.is_some(), egui::Button::new("Connect").min_size(egui::vec2(140.0, 36.0)), ) - .clicked() - { + .clicked(); + if decoded_id.is_some() && (connect_clicked || enter_pressed) { self.start_viewer(ui.ctx().clone()); } } @@ -656,6 +686,22 @@ impl PixelPassApp { } } + /// On entering the View screen, drop a clipboard ticket straight into the + /// field — the host's Copy button (and our auto-copy) usually leaves the + /// freshly-shared code right there. Guarded so it only fires when the field + /// is empty and the clipboard holds a *decodable* ticket, so stray + /// clipboard text never lands in the box; the live decode still shows the + /// id for the user to verify. + fn prefill_viewer_ticket(&mut self) { + if self.viewer.ticket_input.trim().is_empty() + && self.viewer.proc.is_none() + && let Some(text) = get_clipboard() + && ticket_endpoint_id(&text).is_some() + { + self.viewer.ticket_input = text.trim().to_string(); + } + } + fn start_viewer(&mut self, ctx: egui::Context) { self.viewer.error = None; self.viewer.url = None;