#![allow(clippy::needless_range_loop)]
use crate::render::Cell;
use crate::style::Color;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[cfg(feature = "qrcode")]
use qrcode::{EcLevel, QrCode};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum QrStyle {
#[default]
HalfBlock,
FullBlock,
Ascii,
Braille,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ErrorCorrection {
Low,
#[default]
Medium,
Quartile,
High,
}
impl ErrorCorrection {
fn to_ec_level(self) -> EcLevel {
match self {
ErrorCorrection::Low => EcLevel::L,
ErrorCorrection::Medium => EcLevel::M,
ErrorCorrection::Quartile => EcLevel::Q,
ErrorCorrection::High => EcLevel::H,
}
}
}
pub struct QrCodeWidget {
data: String,
style: QrStyle,
fg: Color,
bg: Color,
ec_level: ErrorCorrection,
quiet_zone: u8,
inverted: bool,
props: WidgetProps,
}
impl QrCodeWidget {
pub fn new(data: impl Into<String>) -> Self {
Self {
data: data.into(),
style: QrStyle::default(),
fg: Color::BLACK,
bg: Color::WHITE,
ec_level: ErrorCorrection::default(),
quiet_zone: 1,
inverted: false,
props: WidgetProps::new(),
}
}
pub fn style(mut self, style: QrStyle) -> Self {
self.style = style;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = color;
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = color;
self
}
pub fn error_correction(mut self, level: ErrorCorrection) -> Self {
self.ec_level = level;
self
}
pub fn quiet_zone(mut self, size: u8) -> Self {
self.quiet_zone = size;
self
}
pub fn inverted(mut self, inverted: bool) -> Self {
self.inverted = inverted;
self
}
pub fn set_data(&mut self, data: impl Into<String>) {
self.data = data.into();
}
#[doc(hidden)]
pub fn get_data(&self) -> &str {
&self.data
}
#[doc(hidden)]
pub fn get_style(&self) -> QrStyle {
self.style
}
#[doc(hidden)]
pub fn get_fg(&self) -> Color {
self.fg
}
#[doc(hidden)]
pub fn get_bg(&self) -> Color {
self.bg
}
#[doc(hidden)]
pub fn get_ec_level(&self) -> ErrorCorrection {
self.ec_level
}
#[doc(hidden)]
pub fn get_quiet_zone(&self) -> u8 {
self.quiet_zone
}
#[doc(hidden)]
pub fn get_inverted(&self) -> bool {
self.inverted
}
fn get_matrix(&self) -> Option<Vec<Vec<bool>>> {
let code =
QrCode::with_error_correction_level(&self.data, self.ec_level.to_ec_level()).ok()?;
let size = code.width();
let quiet = self.quiet_zone as usize;
let total_size = size + quiet * 2;
let mut matrix = vec![vec![false; total_size]; total_size];
for y in 0..size {
for x in 0..size {
let dark = code[(x, y)] == qrcode::Color::Dark;
matrix[y + quiet][x + quiet] = if self.inverted { !dark } else { dark };
}
}
Some(matrix)
}
fn render_half_block(&self, ctx: &mut RenderContext, matrix: &[Vec<bool>]) {
let area = ctx.area;
let height = matrix.len();
let width = if height > 0 { matrix[0].len() } else { 0 };
let (fg, bg) = if self.inverted {
(self.bg, self.fg)
} else {
(self.fg, self.bg)
};
for row in 0..height.div_ceil(2) {
if row as u16 >= area.height {
break;
}
for col in 0..width {
if col as u16 >= area.width {
break;
}
let top = matrix
.get(row * 2)
.and_then(|r| r.get(col))
.copied()
.unwrap_or(false);
let bottom = matrix
.get(row * 2 + 1)
.and_then(|r| r.get(col))
.copied()
.unwrap_or(false);
let (ch, cell_fg, cell_bg) = match (top, bottom) {
(true, true) => ('█', Some(fg), Some(bg)),
(true, false) => ('▀', Some(fg), Some(bg)),
(false, true) => ('▄', Some(fg), Some(bg)),
(false, false) => (' ', Some(bg), Some(bg)),
};
let mut cell = Cell::new(ch);
cell.fg = cell_fg;
cell.bg = cell_bg;
ctx.set(col as u16, row as u16, cell);
}
}
}
fn render_full_block(&self, ctx: &mut RenderContext, matrix: &[Vec<bool>]) {
let area = ctx.area;
let height = matrix.len();
let width = if height > 0 { matrix[0].len() } else { 0 };
let (fg, bg) = if self.inverted {
(self.bg, self.fg)
} else {
(self.fg, self.bg)
};
for row in 0..height {
if row as u16 >= area.height {
break;
}
for col in 0..width {
if col as u16 * 2 + 1 >= area.width {
break;
}
let dark = matrix[row][col];
let ch = if dark { '█' } else { ' ' };
let mut cell = Cell::new(ch);
cell.fg = Some(if dark { fg } else { bg });
cell.bg = Some(bg);
ctx.set(col as u16 * 2, row as u16, cell);
ctx.set(col as u16 * 2 + 1, row as u16, cell);
}
}
}
fn render_ascii(&self, ctx: &mut RenderContext, matrix: &[Vec<bool>]) {
let area = ctx.area;
let height = matrix.len();
let width = if height > 0 { matrix[0].len() } else { 0 };
for row in 0..height {
if row as u16 >= area.height {
break;
}
for col in 0..width {
if col as u16 * 2 + 1 >= area.width {
break;
}
let dark = matrix[row][col];
let ch = if dark { '#' } else { ' ' };
let mut cell = Cell::new(ch);
cell.fg = Some(self.fg);
cell.bg = Some(self.bg);
ctx.set(col as u16 * 2, row as u16, cell);
ctx.set(col as u16 * 2 + 1, row as u16, cell);
}
}
}
fn render_braille(&self, ctx: &mut RenderContext, matrix: &[Vec<bool>]) {
let area = ctx.area;
let height = matrix.len();
let width = if height > 0 { matrix[0].len() } else { 0 };
let braille_base: u32 = 0x2800;
for row in 0..height.div_ceil(4) {
if row as u16 >= area.height {
break;
}
for col in 0..width.div_ceil(2) {
if col as u16 >= area.width {
break;
}
let mut dots: u8 = 0;
let get = |r: usize, c: usize| -> bool {
matrix
.get(r)
.and_then(|row| row.get(c))
.copied()
.unwrap_or(false)
};
let base_row = row * 4;
let base_col = col * 2;
if get(base_row, base_col) {
dots |= 0x01;
} if get(base_row + 1, base_col) {
dots |= 0x02;
} if get(base_row + 2, base_col) {
dots |= 0x04;
} if get(base_row, base_col + 1) {
dots |= 0x08;
} if get(base_row + 1, base_col + 1) {
dots |= 0x10;
} if get(base_row + 2, base_col + 1) {
dots |= 0x20;
} if get(base_row + 3, base_col) {
dots |= 0x40;
} if get(base_row + 3, base_col + 1) {
dots |= 0x80;
}
let ch = char::from_u32(braille_base + dots as u32).unwrap_or('⠀');
let mut cell = Cell::new(ch);
cell.fg = Some(self.fg);
cell.bg = Some(self.bg);
ctx.set(col as u16, row as u16, cell);
}
}
}
pub fn required_size(&self) -> Option<(u16, u16)> {
let matrix = self.get_matrix()?;
let height = matrix.len();
let width = if height > 0 { matrix[0].len() } else { 0 };
match self.style {
QrStyle::HalfBlock => Some((width as u16, height.div_ceil(2) as u16)),
QrStyle::FullBlock | QrStyle::Ascii => Some((width as u16 * 2, height as u16)),
QrStyle::Braille => Some((width.div_ceil(2) as u16, height.div_ceil(4) as u16)),
}
}
}
impl View for QrCodeWidget {
fn render(&self, ctx: &mut RenderContext) {
let Some(matrix) = self.get_matrix() else {
ctx.draw_text(0, 0, "QR Error", Color::RED);
return;
};
match self.style {
QrStyle::HalfBlock => self.render_half_block(ctx, &matrix),
QrStyle::FullBlock => self.render_full_block(ctx, &matrix),
QrStyle::Ascii => self.render_ascii(ctx, &matrix),
QrStyle::Braille => self.render_braille(ctx, &matrix),
}
}
crate::impl_view_meta!("QrCodeWidget");
}
impl_styled_view!(QrCodeWidget);
impl_props_builders!(QrCodeWidget);
pub fn qrcode(data: impl Into<String>) -> QrCodeWidget {
QrCodeWidget::new(data)
}
pub fn qrcode_url(url: impl Into<String>) -> QrCodeWidget {
QrCodeWidget::new(url)
}