Misc · Codes and checks: a QR code scanner and a captcha

misc::QrCodeScanner reads QR codes with rqrr 0.11 from the frames the
host hands in, one at a time off the main thread, and hands each code
to the owner once. misc::Captcha shows the owner's picture over an
answer field that empties and keeps focus with each answer; the owner
says how it stood. BarcodeDisplay, Signature and Rating Widget point
to their homes.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-28 01:40:15 -04:00
1 parent dc4911a68f
commit 692fae6244
14 files changed
+785 -10

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+99
View File
@@ -0,0 +1,99 @@
use std::{path::PathBuf, sync::Arc};
use ely_gpui_component::misc::{Captcha, CaptchaState, QrCodeScanner};
use gpui::{App, IntoElement, ParentElement, RenderImage, Styled, Window, div, px};
use crate::probe::probe;
use crate::ui::section;
const ANSWERS: [&str; 2] = ["W7XK", "Q3NP"];
const PICTURES: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/gallery/assets/captcha"
);
/// The frame a camera would hand in: a QR code on a card over a dim scene, in gpui's BGRA pixels.
fn scene() -> Arc<RenderImage> {
let code = qrcode::QrCode::new("https://example.com/ely").expect("the link fits a QR code");
let (modules, scale, quiet) = (code.width() as u32, 4, 4);
let (width, height, side) = (360, 240, (modules + quiet * 2) * scale);
let (left, top) = ((width - side) / 2, (height - side) / 2);
let mut pixels = image::RgbaImage::from_fn(width, height, |x, y| {
let shade = (60 + (x + y) / 10) as u8;
image::Rgba([shade + 12, shade + 6, shade, 255])
});
let dark: Vec<bool> = code
.to_colors()
.into_iter()
.map(|color| color == qrcode::Color::Dark)
.collect();
for (x, y) in (0..side).flat_map(|x| (0..side).map(move |y| (x, y))) {
let module = (x / scale)
.checked_sub(quiet)
.zip((y / scale).checked_sub(quiet));
let ink = module
.filter(|(mx, my)| *mx < modules && *my < modules)
.is_some_and(|(mx, my)| dark[(my * modules + mx) as usize]);
let shade = if ink { 20 } else { 245 };
pixels.put_pixel(left + x, top + y, image::Rgba([shade, shade, shade, 255]));
}
Arc::new(RenderImage::new(vec![image::Frame::new(pixels)]))
}
pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let frame = window.use_keyed_state("scanner-scene", cx, |_, _| scene());
let desk = window.use_keyed_state("captcha-desk", cx, |_, _| (0, CaptchaState::Asking));
let (ix, state) = *desk.read(cx);
let answer = ANSWERS[ix];
let (checked, refreshed) = (desk.clone(), desk);
let picture = PathBuf::from(format!("{PICTURES}/{}.png", answer.to_lowercase()));
div()
.child(
section(
"QRCodeScanner",
"A camera's view with a square to aim at, reading QR codes off the main thread from frames the host hands in. gpui has no camera, so the gallery hands in one frame of its own.",
cx,
)
.child(
div().w(px(280.0)).child(
QrCodeScanner::new("scanner", 1.5)
.frame(frame.read(cx).clone())
.on_scan(|text, _, _| log::info!("gallery: scanned {text}")),
),
),
)
.child(
section(
"Captcha",
"A picture of characters to copy, from the owner, who checks the answer and says how it stood; the refresh asks for another picture.",
cx,
)
.child(probe(
"captcha",
div().w(px(280.0)).child(
Captcha::new("captcha", picture, 3.0)
.state(state)
.on_answer(move |text, _, cx| {
checked.update(cx, |desk, cx| {
desk.1 = match text.eq_ignore_ascii_case(answer) {
true => CaptchaState::Passed,
false => CaptchaState::Wrong,
};
cx.notify();
})
})
.on_refresh(move |_, cx| {
refreshed.update(cx, |desk, cx| {
*desk = ((desk.0 + 1) % ANSWERS.len(), CaptchaState::Asking);
cx.notify();
})
}),
),
)),
)
.child(section(
"BarcodeDisplay · Signature · Rating Widget",
"Each has one home, shown on its own chapter's page: BarcodeDisplay is data_display::Barcode, Signature is forms::SignaturePad, and Rating Widget is forms::Rating.",
cx,
))
}
+13 -2
View File
@@ -7,11 +7,13 @@ use crate::probe::probe;
use crate::script::Step;
use crate::ui::{section, specimen, specimens};
mod codes;
pub const PAGE: Page = Page {
number: 42,
slug: "misc",
title: "Misc",
summary: "Small tools that stand alone: clocks, a stopwatch, a calculator, a unit converter.",
summary: "Small tools that stand alone: clocks, a stopwatch, a calculator, a unit converter, a QR code scanner, a captcha.",
render,
script: &[
Step::DownAt("stopwatch-start", 30.0, 62.0),
@@ -32,6 +34,14 @@ pub const PAGE: Page = Page {
Step::Key("cmd-a"),
Step::Type("1000000000000000"),
Step::Shot("converter-long"),
Step::DownAt("captcha", 24.0, 104.0),
Step::UpAt("captcha", 24.0, 104.0),
Step::Type("abcd"),
Step::Key("enter"),
Step::Shot("captcha-wrong"),
Step::Type("w7xk"),
Step::Key("enter"),
Step::Shot("captcha-passed"),
],
};
@@ -39,12 +49,13 @@ fn zone(name: &str) -> TimeZone {
TimeZone::get(name).unwrap_or_else(|error| panic!("the zone {name}: {error}"))
}
fn render(_: &mut Window, cx: &mut App) -> AnyElement {
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(clocks(cx))
.child(stopwatch(cx))
.child(calculator(cx))
.child(converter(cx))
.child(codes::render(window, cx))
.into_any_element()
}