T12d Data Display: QrCode, Barcode, Meter, Gauge, UsageBar, Stars, Comparison (review pending: codex 401)
This commit is contained in:
18 files changed
+1161
-46
No files matched your search
@@ -0,0 +1,251 @@
|
||||
use barcoders::sym::{code128::Code128, ean13::EAN13};
|
||||
use gpui::{
|
||||
App, Bounds, Div, IntoElement, ParentElement, Pixels, Refineable, RenderOnce, SharedString,
|
||||
StyleRefinement, Styled, Window, canvas, div, fill, point, size,
|
||||
};
|
||||
|
||||
pub use barcoders::error::Error as BarcodeError;
|
||||
pub use qrcode::types::QrError;
|
||||
|
||||
use crate::theme::{ActiveTheme, Radius, TextSize};
|
||||
|
||||
/// The quiet margin around a QR code, in modules, as the standard asks.
|
||||
const QUIET: usize = 4;
|
||||
|
||||
/// The dark runs in a row of modules, as start and length.
|
||||
fn runs(row: &[bool]) -> Vec<(usize, usize)> {
|
||||
let mut runs = Vec::new();
|
||||
let mut start = None;
|
||||
for (ix, dark) in row.iter().chain([&false]).enumerate() {
|
||||
match (start, *dark) {
|
||||
(None, true) => start = Some(ix),
|
||||
(Some(from), false) => {
|
||||
runs.push((from, ix - from));
|
||||
start = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
runs
|
||||
}
|
||||
|
||||
/// Paints each row's dark runs as bars `module` wide, from `origin`.
|
||||
fn paint_runs(
|
||||
rows: &[Vec<(usize, usize)>],
|
||||
origin: gpui::Point<Pixels>,
|
||||
module: Pixels,
|
||||
height: Pixels,
|
||||
color: gpui::Hsla,
|
||||
window: &mut Window,
|
||||
) {
|
||||
for (y, row) in rows.iter().enumerate() {
|
||||
for (x, length) in row {
|
||||
let corner = point(origin.x + module * *x as f32, origin.y + height * y as f32);
|
||||
window.paint_quad(fill(
|
||||
Bounds::new(corner, size(module * *length as f32, height)),
|
||||
color,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text as a QR code: dark modules on a light tile in both themes, so any camera reads it, inside the standard quiet margin.
|
||||
#[derive(IntoElement)]
|
||||
pub struct QrCode {
|
||||
base: Div,
|
||||
rows: Vec<Vec<(usize, usize)>>,
|
||||
}
|
||||
|
||||
impl QrCode {
|
||||
pub fn new(data: impl AsRef<[u8]>) -> Result<Self, QrError> {
|
||||
let code = qrcode::QrCode::new(data)?;
|
||||
let dark: Vec<bool> = code
|
||||
.to_colors()
|
||||
.into_iter()
|
||||
.map(|color| color == qrcode::Color::Dark)
|
||||
.collect();
|
||||
Ok(Self {
|
||||
base: div(),
|
||||
rows: dark.chunks(code.width()).map(runs).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for QrCode {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for QrCode {
|
||||
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let (paper, ink) = (theme.colors.paper, theme.colors.ink);
|
||||
let rows = self.rows;
|
||||
let mut tile = div()
|
||||
.flex_none()
|
||||
.size(theme.qr_code())
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.bg(paper);
|
||||
tile.style().refine(self.base.style());
|
||||
tile.child(
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |bounds, _, window, _| {
|
||||
let span = (rows.len() + QUIET * 2) as f32;
|
||||
let module = (bounds.size.width.min(bounds.size.height) / span).floor();
|
||||
let inset = (rows.len() as f32 * module) / 2.0;
|
||||
let origin = bounds.center() - point(inset, inset);
|
||||
paint_runs(&rows, origin, module, module, ink, window);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// EAN's check digit: weights of one and three from the left, then what lifts the sum to a multiple of ten.
|
||||
fn check_digit(digits: &str) -> u32 {
|
||||
let sum: u32 = digits
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(ix, digit)| {
|
||||
digit.to_digit(10).expect("barcoders checked the digits")
|
||||
* if ix % 2 == 0 { 1 } else { 3 }
|
||||
})
|
||||
.sum();
|
||||
(10 - sum % 10) % 10
|
||||
}
|
||||
|
||||
/// Text as a barcode, bars on a light tile in both themes with the text under them: Code 128 for printable ASCII, EAN-13 for retail numbers.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Barcode {
|
||||
base: Div,
|
||||
bars: Vec<(usize, usize)>,
|
||||
modules: usize,
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl Barcode {
|
||||
fn new(modules: Vec<u8>, text: String) -> Self {
|
||||
let dark: Vec<bool> = modules.iter().map(|module| *module == 1).collect();
|
||||
Self {
|
||||
base: div(),
|
||||
bars: runs(&dark),
|
||||
modules: dark.len(),
|
||||
text: text.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Code 128 in its set B: letters, digits and printable ASCII marks.
|
||||
pub fn code128(text: &str) -> Result<Self, BarcodeError> {
|
||||
let modules = Code128::new(format!("\u{0181}{text}"))?.encode();
|
||||
Ok(Self::new(modules, text.to_string()))
|
||||
}
|
||||
|
||||
/// EAN-13 from twelve digits, or thirteen whose check digit must match.
|
||||
pub fn ean13(digits: &str) -> Result<Self, BarcodeError> {
|
||||
let modules = EAN13::new(digits)?.encode();
|
||||
let text = match digits.len() {
|
||||
12 => format!("{digits}{}", check_digit(digits)),
|
||||
_ => digits.to_string(),
|
||||
};
|
||||
Ok(Self::new(modules, text))
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Barcode {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Barcode {
|
||||
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let (paper, ink) = (theme.colors.paper, theme.colors.ink);
|
||||
let bars = vec![self.bars];
|
||||
let mut tile = div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.px_4()
|
||||
.pt_3()
|
||||
.pb_2()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.bg(paper)
|
||||
.text_color(ink);
|
||||
tile.style().refine(self.base.style());
|
||||
tile.child(
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |bounds, _, window, _| {
|
||||
let module = bounds.size.width / self.modules as f32;
|
||||
paint_runs(
|
||||
&bars,
|
||||
bounds.origin,
|
||||
module,
|
||||
bounds.size.height,
|
||||
ink,
|
||||
window,
|
||||
);
|
||||
},
|
||||
)
|
||||
.w(theme.barcode_module() * self.modules as f32)
|
||||
.h(theme.barcode_height()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.child(self.text),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Barcode, BarcodeError, check_digit, runs};
|
||||
|
||||
#[test]
|
||||
fn dark_modules_merge_into_runs() {
|
||||
assert_eq!(runs(&[true, true, false, true]), [(0, 2), (3, 1)]);
|
||||
assert_eq!(runs(&[false, false]), []);
|
||||
assert_eq!(runs(&[true]), [(0, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ean_gains_its_check_digit_and_refuses_a_wrong_one() {
|
||||
assert_eq!(check_digit("400638133393"), 1);
|
||||
assert_eq!(check_digit("590123412345"), 7);
|
||||
assert_eq!(
|
||||
Barcode::ean13("400638133393").map(|code| code.text).ok(),
|
||||
Some("4006381333931".into())
|
||||
);
|
||||
assert!(matches!(
|
||||
Barcode::ean13("4006381333932"),
|
||||
Err(BarcodeError::Checksum)
|
||||
));
|
||||
assert!(matches!(
|
||||
Barcode::code128("caf\u{e9}"),
|
||||
Err(BarcodeError::Character)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_128_spends_eleven_modules_a_character() {
|
||||
let code = Barcode::code128("ELY-2419").expect("printable ASCII");
|
||||
assert_eq!(
|
||||
code.modules,
|
||||
11 + 8 * 11 + 11 + 13,
|
||||
"start, eight characters, check, stop"
|
||||
);
|
||||
assert_eq!(
|
||||
code.bars.first(),
|
||||
Some(&(0, 2)),
|
||||
"set B's start opens with two dark modules"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user