use egui::{Color32, ColorImage, TextureHandle, TextureOptions, Vec2};
const QUIET_ZONE: usize = 4;
const DEFAULT_SIDE: f32 = 200.0;
pub fn image(text: &str) -> Option<ColorImage> {
let code = qrcode::QrCode::new(text.as_bytes()).ok()?;
let width = code.width();
let side = width + 2 * QUIET_ZONE;
let mut pixels = vec![Color32::WHITE; side * side];
for (at, colour) in code.to_colors().into_iter().enumerate() {
if colour == qrcode::Color::Dark {
let (x, y) = (at % width + QUIET_ZONE, at / width + QUIET_ZONE);
pixels[y * side + x] = Color32::BLACK;
}
}
Some(ColorImage::new([side, side], pixels))
}
#[derive(Clone, Debug)]
pub struct QrCode<'a> {
text: &'a str,
side: f32,
}
impl<'a> QrCode<'a> {
pub fn new(text: &'a str) -> QrCode<'a> {
QrCode {
text,
side: DEFAULT_SIDE,
}
}
pub fn side(mut self, side: f32) -> QrCode<'a> {
self.side = side;
self
}
}
#[derive(Clone)]
struct Drawn {
text: String,
texture: Option<TextureHandle>,
}
impl egui::Widget for QrCode<'_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let id = ui.next_auto_id().with("qr-code");
let ctx = ui.ctx().clone();
let drawn = ctx.data_mut(|data| data.get_temp::<Drawn>(id));
let texture = match drawn {
Some(drawn) if drawn.text == self.text => drawn.texture,
_ => {
let texture = image(self.text)
.map(|image| ctx.load_texture("qr-code", image, TextureOptions::NEAREST));
let drawn = Drawn {
text: self.text.to_owned(),
texture: texture.clone(),
};
ctx.data_mut(|data| data.insert_temp(id, drawn));
texture
}
};
match texture {
Some(texture) => ui.add(
egui::Image::new(&texture)
.fit_to_exact_size(Vec2::splat(self.side))
.alt_text(self.text),
),
None => ui.weak(crate::i18n::tr(ui.ctx(), "corundum-qr-too-long")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_code_has_its_quiet_zone_and_dark_finders() {
let image = image("corundum").unwrap();
let [width, height] = image.size;
assert_eq!(width, height);
assert_eq!(width, 21 + 2 * QUIET_ZONE);
let at = |x: usize, y: usize| image.pixels[y * width + x];
assert_eq!(at(0, 0), Color32::WHITE);
assert_eq!(at(QUIET_ZONE - 1, QUIET_ZONE), Color32::WHITE);
assert_eq!(at(QUIET_ZONE, QUIET_ZONE), Color32::BLACK);
}
#[test]
fn longer_text_takes_a_bigger_code() {
let short = image("a").unwrap().size[0];
let long = image(&"a".repeat(200)).unwrap().size[0];
assert!(long > short);
}
#[test]
fn text_too_long_for_any_code_has_none() {
assert!(image(&"x".repeat(8000)).is_none());
}
}