use egui::{
Color32, Context, Frame, Key, LayerId, Order, Rect, RichText, Stroke, StrokeKind,
ViewportBuilder, ViewportClass, ViewportCommand, ViewportId, pos2, vec2,
};
use enigo::Mouse;
use rqrr::PreparedImage;
use secure_types::{SecureString, Zeroize};
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicI32, Ordering},
};
use std::time::Duration;
use xcap::{Monitor, image::DynamicImage};
type Error = Box<dyn std::error::Error>;
const VIEWPORT_ID: &str = "qr_scanner";
#[derive(Clone)]
pub struct QRScanner {
open: Arc<AtomicBool>,
capture_size: Arc<AtomicI32>,
capture_is_valid: Arc<AtomicBool>,
result: Arc<Mutex<Option<SecureString>>>,
last_error: Arc<Mutex<Option<String>>>,
current_monitor: Option<xcap::Monitor>,
}
impl QRScanner {
pub fn new() -> Self {
Self {
open: Arc::new(AtomicBool::new(false)),
capture_size: Arc::new(AtomicI32::new(300)),
capture_is_valid: Arc::new(AtomicBool::new(false)),
result: Arc::new(Mutex::new(None)),
last_error: Arc::new(Mutex::new(None)),
current_monitor: None,
}
}
pub fn open(&self, ctx: Context) {
if !self.is_open() {
let open = self.open.clone();
std::thread::spawn(move || repaint_thread(ctx, open));
}
self.open.store(true, Ordering::Relaxed);
}
pub fn close(&self) {
self.open.store(false, Ordering::Relaxed);
}
pub fn reset(&mut self) {
*self = Self::new();
}
pub fn is_open(&self) -> bool {
self.open.load(Ordering::Relaxed)
}
pub fn show(&mut self, ctx: &egui::Context) {
if !self.is_open() {
return;
}
if self.current_monitor.is_none() {
let enigo = enigo::Enigo::new(&enigo::Settings::default()).unwrap();
let (mouse_x, mouse_y) = enigo.location().unwrap();
if let Ok(monitor) = xcap::Monitor::from_point(mouse_x, mouse_y) {
self.current_monitor = Some(monitor);
} else {
self.open.store(false, Ordering::Relaxed);
return;
}
}
let monitor = self.current_monitor.as_ref().unwrap();
let mon_x_px = monitor.x().unwrap() as f32;
let mon_y_px = monitor.y().unwrap() as f32;
let mon_width_px = monitor.width().unwrap() as f32;
let mon_height_px = monitor.height().unwrap() as f32;
let ppp = ctx.pixels_per_point();
let open = self.open.clone();
let capture_size = self.capture_size.clone();
let capture_is_valid = self.capture_is_valid.clone();
let last_error = self.last_error.clone();
let result = self.result.clone();
let monitor_clone = monitor.clone();
ctx.show_viewport_deferred(
ViewportId::from_hash_of(VIEWPORT_ID),
ViewportBuilder::default()
.with_title("QR Scan Overlay")
.with_transparent(true)
.with_decorations(false)
.with_always_on_top()
.with_mouse_passthrough(true)
.with_position(pos2(mon_x_px / ppp, mon_y_px / ppp))
.with_inner_size(vec2(mon_width_px / ppp, mon_height_px / ppp))
.with_active(true),
move |ctx, class| {
if class == ViewportClass::EmbeddedWindow {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("This viewport is embedded.");
});
return;
}
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::Focus);
}
ctx.input_mut(|i| {
if i.consume_key(egui::Modifiers::NONE, Key::Plus)
|| i.consume_key(egui::Modifiers::NONE, Key::Equals)
{
capture_size.fetch_add(10, Ordering::Relaxed);
}
if i.consume_key(egui::Modifiers::NONE, Key::Minus) {
let new = capture_size.load(Ordering::Relaxed) - 10;
capture_size.store(new, Ordering::Relaxed);
}
if i.consume_key(egui::Modifiers::NONE, Key::Escape) {
open.store(false, Ordering::Relaxed);
}
});
match capture_and_decode(
capture_size.load(Ordering::Relaxed),
&monitor_clone,
) {
Ok(res) => {
capture_is_valid.store(true, Ordering::Relaxed);
*last_error.lock().unwrap() = None;
*result.lock().unwrap() = Some(res);
open.store(false, Ordering::Relaxed);
}
Err(e) => {
capture_is_valid.store(false, Ordering::Relaxed);
*last_error.lock().unwrap() = Some(e.to_string());
}
}
let ppp = ctx.pixels_per_point();
let (mouse_x_px, mouse_y_px) =
enigo::Enigo::new(&enigo::Settings::default()).unwrap().location().unwrap();
let capture_size_px = capture_size.load(Ordering::Relaxed);
let half_px = capture_size_px / 2;
let mon_x_px = monitor_clone.x().unwrap();
let mon_y_px = monitor_clone.y().unwrap();
let mon_width_px = monitor_clone.width().unwrap() as i32;
let mon_height_px = monitor_clone.height().unwrap() as i32;
let cap_x_px = (mouse_x_px - half_px)
.max(mon_x_px)
.min(mon_x_px + mon_width_px - capture_size_px);
let cap_y_px = (mouse_y_px - half_px)
.max(mon_y_px)
.min(mon_y_px + mon_height_px - capture_size_px);
let cap_width_px = capture_size_px.min(mon_x_px + mon_width_px - cap_x_px);
let cap_height_px = capture_size_px.min(mon_y_px + mon_height_px - cap_y_px);
let rel_x_px = (cap_x_px - mon_x_px) as f32;
let rel_y_px = (cap_y_px - mon_y_px) as f32;
let rel_x_pt = rel_x_px / ppp;
let rel_y_pt = rel_y_px / ppp;
let cap_width_pt = cap_width_px as f32 / ppp;
let cap_height_pt = cap_height_px as f32 / ppp;
let rect = Rect::from_min_size(
pos2(rel_x_pt, rel_y_pt),
vec2(cap_width_pt, cap_height_pt),
);
let painter = ctx.layer_painter(LayerId::new(
Order::Foreground,
egui::Id::new("qr_border_layer"),
));
let color = if capture_is_valid.load(Ordering::Relaxed) {
Color32::GREEN
} else {
Color32::RED
};
let stroke = Stroke::new(3.0, color);
let stroke_kind = StrokeKind::Outside;
painter.rect_stroke(rect, 0.0, stroke, stroke_kind);
let help_pos = pos2(rel_x_pt, rel_y_pt - 100.0); let last_error = last_error.lock().unwrap();
egui::Area::new("qr_help".into()).fixed_pos(help_pos).show(&ctx, |ui| {
let frame = Frame::window(ui.style());
frame.show(ui, |ui| {
let text = RichText::new("Use the mouse to target the QR code.").size(14.0);
ui.label(text);
let text = RichText::new(
"Press + or - to increase or decrease the size of the captured area.",
)
.size(14.0);
ui.label(text);
let text = RichText::new("Press Esc to cancel.").size(14.0);
ui.label(text);
let mut text = String::new();
if let Some(e) = last_error.as_ref().map(|e| e.as_str()) {
text = e.to_string();
}
ui.label(RichText::new(text).size(14.0).color(Color32::RED));
});
});
if ctx.input(|i| i.viewport().close_requested()) {
open.store(false, Ordering::Relaxed);
}
},
);
}
pub fn get_result(&self) -> Option<SecureString> {
self.result.lock().unwrap().clone()
}
}
fn repaint_thread(ctx: Context, open: Arc<AtomicBool>) {
loop {
if !open.load(Ordering::Relaxed) {
ctx.request_repaint();
return;
}
ctx.request_repaint();
std::thread::sleep(Duration::from_millis(16));
}
}
pub fn capture_and_decode(capture_size: i32, monitor: &Monitor) -> Result<SecureString, Error> {
let enigo = enigo::Enigo::new(&enigo::Settings::default())?;
let (mouse_x, mouse_y) = enigo.location()?;
let half = capture_size / 2;
let mon_x = monitor.x()?;
let mon_y = monitor.y()?;
let mon_width = monitor.width()? as i32;
let mon_height = monitor.height()? as i32;
let cap_x = (mouse_x - half).max(mon_x).min(mon_x + mon_width - capture_size);
let cap_y = (mouse_y - half).max(mon_y).min(mon_y + mon_height - capture_size);
let cap_width = capture_size.min(mon_x + mon_width - cap_x);
let cap_height = capture_size.min(mon_y + mon_height - cap_y);
let cap_width: u32 = cap_width.try_into()?;
let cap_height: u32 = cap_height.try_into()?;
let rel_x: u32 = (cap_x - mon_x).try_into()?;
let rel_y: u32 = (cap_y - mon_y).try_into()?;
let image = monitor.capture_region(rel_x, rel_y, cap_width, cap_height)?;
let mut img = DynamicImage::ImageRgba8(image);
let luma = img.to_luma8();
debug_assert!(img.as_rgba8().is_some());
if let Some(img) = img.as_mut_rgba8() {
img.zeroize();
}
let mut prepared = PreparedImage::prepare(luma);
let grids = prepared.detect_grids();
if grids.is_empty() {
return Err(format!("No QR grids detected (try adjusting size/position)").into());
}
let (_, content) = grids[0].decode()?;
let sec_string = SecureString::from(content);
Ok(sec_string)
}