tui_qrcode/lib.rs
1//! A [Ratatui] widget to render crisp, scan-happy QR codes in the terminal. Part of the
2//! [tui-widgets] suite by [Joshka].
3//!
4//! 
5//!
6//! [![Crate badge]][Crate]
7//! [![Docs Badge]][Docs]
8//! [![Deps Badge]][Dependency Status]
9//! [![License Badge]][License]
10//! [![Coverage Badge]][Coverage]
11//! [![Discord Badge]][Ratatui Discord]
12//!
13//! [GitHub Repository] · [API Docs] · [Examples] · [Changelog] · [Contributing]
14//!
15//! # Installation
16//!
17//! Add qrcode and tui-qrcode to your Cargo.toml. You can disable the default features of qrcode as
18//! we don't need the code which renders the QR code to an image.
19//!
20//! ```shell
21//! cargo add qrcode tui-qrcode --no-default-features
22//! ```
23//!
24//! # Usage
25//!
26//! This example can be found in the `examples` directory of the repository.
27//!
28//! ```no_run
29//! use qrcode::QrCode;
30//! use ratatui::crossterm::event;
31//! use ratatui::{DefaultTerminal, Frame};
32//! use tui_qrcode::{Colors, QrCodeWidget};
33//!
34//! fn main() -> color_eyre::Result<()> {
35//! color_eyre::install()?;
36//! let terminal = ratatui::init();
37//! let result = run(terminal);
38//! ratatui::restore();
39//! result
40//! }
41//!
42//! fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
43//! loop {
44//! terminal.draw(render)?;
45//! if matches!(event::read()?, event::Event::Key(_)) {
46//! break Ok(());
47//! }
48//! }
49//! }
50//!
51//! fn render(frame: &mut Frame) {
52//! let qr_code = QrCode::new("https://ratatui.rs").expect("failed to create QR code");
53//! let widget = QrCodeWidget::new(qr_code).colors(Colors::Inverted);
54//! frame.render_widget(widget, frame.area());
55//! }
56//! ```
57//!
58//! Renders the following QR code:
59//!
60//! ```text
61//! █████████████████████████████████
62//! █████████████████████████████████
63//! ████ ▄▄▄▄▄ █▄ ▄▄▄ ████ ▄▄▄▄▄ ████
64//! ████ █ █ █▄▄▄█▀▄██ █ █ █ ████
65//! ████ █▄▄▄█ █▀ ▄▀ ███ █▄▄▄█ ████
66//! ████▄▄▄▄▄▄▄█▄▀▄█ ▀▄▀ █▄▄▄▄▄▄▄████
67//! ████ █▄▀▀▀▄▄▀▄▄ ▄█▀▄█▀ █▀▄▀ ████
68//! ██████▀█ ▄▀▄▄▀▀ ▄ ▄█ ▄▄█ ▄█▄████
69//! ████▄▀▀▀▄▄▄▄▀█▄▄█ ▀ ▀ ▀███▀ ████
70//! ████▄▄ ▀█▄▄▀▄▄ ▄█▀█▄▀█▄▀▀ ▄█▄████
71//! ████▄▄█▄██▄█ ▄▀▄ ▄█ ▄▄▄ ██▄▀████
72//! ████ ▄▄▄▄▄ █▄▄▄▀ ▄ ▀ █▄█ ███ ████
73//! ████ █ █ ██ ███ ▄▄ ▄▄ █▀ ▄████
74//! ████ █▄▄▄█ █▄▀ ▄█▀█▀ ▄█ ▄█▄▄████
75//! ████▄▄▄▄▄▄▄█▄▄█▄▄▄██▄█▄██▄██▄████
76//! █████████████████████████████████
77//! ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
78//! ```
79//!
80//! # More widgets
81//!
82//! For the full suite of widgets, see [tui-widgets].
83//!
84//! [Ratatui]: https://crates.io/crates/ratatui
85//! [Crate]: https://crates.io/crates/tui-qrcode
86//! [Docs]: https://docs.rs/tui-qrcode/
87//! [Dependency Status]: https://deps.rs/repo/github/joshka/tui-widgets
88//! [Coverage]: https://app.codecov.io/gh/joshka/tui-widgets
89//! [Ratatui Discord]: https://discord.gg/pMCEU9hNEj
90//! [Crate badge]: https://img.shields.io/crates/v/tui-qrcode.svg?logo=rust&style=flat
91//! [Docs Badge]: https://img.shields.io/docsrs/tui-qrcode?logo=rust&style=flat
92//! [Deps Badge]: https://deps.rs/repo/github/joshka/tui-widgets/status.svg?style=flat
93//! [License Badge]: https://img.shields.io/crates/l/tui-qrcode?style=flat
94//! [License]: https://github.com/joshka/tui-widgets/blob/main/LICENSE-MIT
95//! [Coverage Badge]:
96//! https://img.shields.io/codecov/c/github/joshka/tui-widgets?logo=codecov&style=flat
97//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?logo=discord&style=flat
98//! [GitHub Repository]: https://github.com/joshka/tui-widgets
99//! [API Docs]: https://docs.rs/tui-qrcode/
100//! [Examples]: https://github.com/joshka/tui-widgets/tree/main/tui-qrcode/examples
101//! [Changelog]: https://github.com/joshka/tui-widgets/blob/main/tui-qrcode/CHANGELOG.md
102//! [Contributing]: https://github.com/joshka/tui-widgets/blob/main/CONTRIBUTING.md
103//!
104//! [Joshka]: https://github.com/joshka
105//! [tui-widgets]: https://crates.io/crates/tui-widgets
106
107use qrcode::render::unicode::Dense1x2;
108use qrcode::QrCode;
109use ratatui_core::buffer::Buffer;
110use ratatui_core::layout::{Rect, Size};
111use ratatui_core::style::{Style, Styled};
112use ratatui_core::text::Text;
113use ratatui_core::widgets::Widget;
114
115/// A [Ratatui] widget that renders a QR code.
116///
117/// This widget can be used to render a QR code in a terminal. It uses the [qrcode] crate to
118/// generate the QR code.
119///
120/// # Examples
121///
122/// ```no_run
123/// use qrcode::QrCode;
124/// use tui_qrcode::QrCodeWidget;
125///
126/// let qr_code = QrCode::new("https://ratatui.rs").expect("failed to create QR code");
127/// let widget = QrCodeWidget::new(qr_code);
128/// ```
129///
130/// The widget can be customized using the `quiet_zone`, `scaling`, `colors`, and `style` methods.
131/// Additionally, the widget implements the [`Styled`] trait, so all the methods from Ratatui's
132/// [`ratatui_core::style::Stylize`] trait can be used.
133///
134/// ```no_run
135/// use qrcode::QrCode;
136/// use ratatui::style::{Style, Stylize};
137/// use ratatui::Frame;
138/// use tui_qrcode::{Colors, QrCodeWidget, QuietZone, Scaling};
139///
140/// fn render(frame: &mut Frame) {
141/// let qr_code = QrCode::new("https://ratatui.rs").expect("failed to create QR code");
142/// let widget = QrCodeWidget::new(qr_code)
143/// .quiet_zone(QuietZone::Disabled)
144/// .scaling(Scaling::Max)
145/// .colors(Colors::Inverted)
146/// .red()
147/// .on_light_yellow();
148/// frame.render_widget(widget, frame.area());
149/// }
150/// ```
151///
152/// [Ratatui]: https://crates.io/crates/ratatui
153pub struct QrCodeWidget {
154 qr_code: QrCode,
155 quiet_zone: QuietZone,
156 scaling: Scaling,
157 colors: Colors,
158 style: Style,
159}
160
161/// The quiet zone (border) of a QR code.
162#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
163pub enum QuietZone {
164 /// The quiet zone is enabled.
165 #[default]
166 Enabled,
167 /// The quiet zone is disabled.
168 Disabled,
169}
170
171#[derive(Debug, Clone, Copy, Eq, PartialEq)]
172pub enum Scaling {
173 /// The QR code will be scaled to at least the size of the widget.
174 ///
175 /// Note that this usually results in a QR code that is larger than the widget, which is not
176 /// ideal.
177 Min,
178
179 /// The QR code will be scaled to be at most the size of the widget.
180 ///
181 /// Note that this may result in a QR code which is scaled more horizontally or vertically than
182 /// the other, which may not be ideal.
183 Max,
184
185 /// The QR code will be scaled so each pixel is the size of the given dimensions.
186 ///
187 /// The minimum dimensions are 1x1 (width x height).
188 Exact(u16, u16),
189}
190
191impl Default for Scaling {
192 fn default() -> Self {
193 Self::Exact(1, 1)
194 }
195}
196
197#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
198pub enum Colors {
199 /// The default colors. (Black on white)
200 #[default]
201 Normal,
202
203 /// The colors are inverted. (White on black)
204 Inverted,
205}
206
207impl QrCodeWidget {
208 /// Create a new QR code widget.
209 #[must_use]
210 pub fn new(qr_code: QrCode) -> Self {
211 Self {
212 qr_code,
213 quiet_zone: QuietZone::default(),
214 scaling: Scaling::default(),
215 colors: Colors::default(),
216 style: Style::default(),
217 }
218 }
219
220 /// Set whether the QR code should have a quiet zone.
221 ///
222 /// This is the white border around the QR code. By default, the quiet zone is enabled.
223 ///
224 /// # Example
225 ///
226 /// ```
227 /// use qrcode::QrCode;
228 /// use tui_qrcode::{QrCodeWidget, QuietZone};
229 ///
230 /// let qr_code = QrCode::new("https://ratatui.rs").expect("failed to create QR code");
231 /// let widget = QrCodeWidget::new(qr_code).quiet_zone(QuietZone::Disabled);
232 /// ```
233 #[must_use]
234 pub const fn quiet_zone(mut self, quiet_zone: QuietZone) -> Self {
235 self.quiet_zone = quiet_zone;
236 self
237 }
238
239 /// Set how the QR code should be scaled.
240 ///
241 /// By default, the QR code will be scaled so each pixel is 1x1.
242 ///
243 /// The `Min` variant will scale the QR code so it is at least the size of the widget. This may
244 /// result in a QR code that is larger than the widget, which is not ideal. The `Max` variant
245 /// will scale the QR code so it is at most the size of the widget. This may result in a QR code
246 /// which is scaled more horizontally or vertically than the other, which may not be ideal. The
247 /// `Exact` variant will scale the QR code so each pixel is the size of the given dimensions.
248 /// The minimum scaling is 1x1 (width x height).
249 ///
250 /// # Example
251 ///
252 /// ```
253 /// use qrcode::QrCode;
254 /// use tui_qrcode::{QrCodeWidget, Scaling};
255 ///
256 /// let qr_code = QrCode::new("https://ratatui.rs").expect("failed to create QR code");
257 /// let widget = QrCodeWidget::new(qr_code).scaling(Scaling::Max);
258 /// ```
259 #[must_use]
260 pub const fn scaling(mut self, scaling: Scaling) -> Self {
261 self.scaling = scaling;
262 self
263 }
264
265 /// Set the colors of the QR code.
266 ///
267 /// By default, the colors are normal (light on dark).
268 ///
269 /// The `Normal` variant will use the default colors. The `Inverted` variant will invert the
270 /// colors (dark on light).
271 ///
272 /// To set the foreground and background colors of the widget, use the `style` method.
273 ///
274 /// # Example
275 ///
276 /// ```
277 /// use qrcode::QrCode;
278 /// use tui_qrcode::{Colors, QrCodeWidget};
279 ///
280 /// let qr_code = QrCode::new("https://ratatui.rs").expect("failed to create QR code");
281 /// let widget = QrCodeWidget::new(qr_code).colors(Colors::Inverted);
282 /// ```
283 #[must_use]
284 pub const fn colors(mut self, colors: Colors) -> Self {
285 self.colors = colors;
286 self
287 }
288
289 /// Set the style of the widget.
290 ///
291 /// This will set the foreground and background colors of the widget.
292 ///
293 /// # Example
294 ///
295 /// ```
296 /// use qrcode::QrCode;
297 /// use ratatui::style::{Style, Stylize};
298 /// use tui_qrcode::QrCodeWidget;
299 ///
300 /// let qr_code = QrCode::new("https://ratatui.rs").expect("failed to create QR code");
301 /// let style = Style::new().red().on_light_yellow();
302 /// let widget = QrCodeWidget::new(qr_code).style(style);
303 /// ```
304 #[must_use]
305 pub fn style(mut self, style: impl Into<Style>) -> Self {
306 self.style = style.into();
307 self
308 }
309
310 /// The theoretical size of the QR code if rendered into `area`.
311 ///
312 /// Note that if the QR code does not fit into `area`, the resulting [`Size`] might be larger
313 /// than the size of `area`.
314 ///
315 /// # Example
316 /// ```
317 /// use qrcode::QrCode;
318 /// use ratatui::layout::Rect;
319 /// use tui_qrcode::{QrCodeWidget, Scaling};
320 ///
321 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
322 /// let qr_code = QrCode::new("https://ratatui.rs")?;
323 /// let widget = QrCodeWidget::new(qr_code).scaling(Scaling::Min);
324 /// let area = Rect::new(0, 0, 50, 50);
325 /// let widget_size = widget.size(area);
326 ///
327 /// assert_eq!(widget_size.width, 66);
328 /// assert_eq!(widget_size.height, 66);
329 /// # Ok(())
330 /// # }
331 /// ```
332 #[must_use]
333 pub fn size(&self, area: Rect) -> Size {
334 let qr_width: u16 = match self.quiet_zone {
335 QuietZone::Enabled => 8,
336 QuietZone::Disabled => 0,
337 } + self.qr_code.width() as u16;
338
339 let (x, y) = match self.scaling {
340 Scaling::Exact(x, y) => (x, y),
341 Scaling::Min => {
342 let x = area.width.div_ceil(qr_width);
343 let y = (area.height * 2).div_ceil(qr_width);
344 (x, y)
345 }
346 Scaling::Max => {
347 let x = area.width / qr_width;
348 let y = (area.height * 2) / qr_width;
349 (x, y)
350 }
351 };
352 let (x, y) = (x.max(1), y.max(1));
353 let width = qr_width * x;
354 let height = (qr_width * y).div_ceil(2);
355 Size::new(width, height)
356 }
357}
358
359impl Styled for QrCodeWidget {
360 type Item = Self;
361
362 fn style(&self) -> Style {
363 self.style
364 }
365
366 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
367 self.style(style)
368 }
369}
370
371impl Widget for QrCodeWidget {
372 fn render(self, area: Rect, buf: &mut Buffer) {
373 (&self).render(area, buf);
374 }
375}
376
377impl Widget for &QrCodeWidget {
378 fn render(self, area: Rect, buf: &mut Buffer) {
379 let mut renderer = self.qr_code.render::<Dense1x2>();
380 match self.quiet_zone {
381 QuietZone::Enabled => renderer.quiet_zone(true),
382 QuietZone::Disabled => renderer.quiet_zone(false),
383 };
384 match self.scaling {
385 Scaling::Min => renderer.min_dimensions(area.width as u32, area.height as u32 * 2),
386 Scaling::Max => renderer.max_dimensions(area.width as u32, area.height as u32 * 2),
387 Scaling::Exact(width, height) => {
388 renderer.module_dimensions(width as u32, height as u32)
389 }
390 };
391 match self.colors {
392 Colors::Normal => renderer
393 .dark_color(Dense1x2::Dark)
394 .light_color(Dense1x2::Light),
395 Colors::Inverted => renderer
396 .dark_color(Dense1x2::Light)
397 .light_color(Dense1x2::Dark),
398 };
399 Text::raw(renderer.build())
400 .style(self.style)
401 .render(area, buf);
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use rstest::{fixture, rstest};
408
409 use super::*;
410
411 /// Creates an empty QR code widget. The basic dimensions of the QR code are 21x21 or 29x29 with
412 /// a quiet zone.
413 #[fixture]
414 fn empty_widget() -> QrCodeWidget {
415 let empty_qr = QrCode::new("").expect("failed to create QR code");
416 QrCodeWidget::new(empty_qr).quiet_zone(QuietZone::Disabled)
417 }
418
419 #[rstest]
420 /// Cases where the QR code is smaller (21x10.5) than the area (22, 12)
421 #[case::smaller_exact((22,12), Scaling::Exact(1, 1), (21, 11))]
422 #[case::smaller_max((22, 12), Scaling::Max, (21, 11))]
423 #[case::smaller_min((22,12),Scaling::Min, (42, 21))]
424 /// Cases where the QR code is the same size (21x10.5) as the area (21, 11)
425 #[case::same_exact((21, 11), Scaling::Exact(1, 1), (21, 11))]
426 #[case::same_max((21, 11), Scaling::Max, (21, 11))]
427 /// Exception: height would be 10.5, so height is doubled to 21
428 #[case::same_min((21, 11), Scaling::Min, (21, 21))]
429 /// Cases where the QR code is larger (21x10.5) than the area (20, 10)
430 #[rstest]
431 #[case::larger_exact((20, 10), Scaling::Exact(1, 1), (21, 11))]
432 #[case::larger_max((20, 10), Scaling::Max, (21, 11))]
433 #[case::larger_min((20, 10), Scaling::Min, (21, 11))]
434 /// Cases where the QR code is much smaller (21x10.5) than the area (71, 71).
435 #[rstest]
436 #[case::huge_exact((71, 71), Scaling::Exact(1, 1), (21, 11))]
437 #[case::huge_max((71, 71), Scaling::Max,(63, 63))]
438 #[case::huge_min((71, 71), Scaling::Min, (84, 74))]
439 fn size(
440 empty_widget: QrCodeWidget,
441 #[case] rect: (u16, u16),
442 #[case] scaling: Scaling,
443 #[case] expected: (u16, u16),
444 ) {
445 let rect = Rect::new(0, 0, rect.0, rect.1);
446 let widget = empty_widget.scaling(scaling);
447 assert_eq!(widget.size(rect), Size::from(expected));
448 }
449
450 /// Testing that a QR code with a quiet zone (29x14.5) is scaled correctly into a large area
451 /// (71x71).
452 #[rstest]
453 #[case::huge_exact(Scaling::Exact(1, 1), (29, 15))]
454 #[case::huge_max(Scaling::Max, (58, 58))]
455 #[case::huge_min(Scaling::Min, (87, 73))]
456 fn size_with_quiet_zone(
457 empty_widget: QrCodeWidget,
458 #[case] scaling: Scaling,
459 #[case] expected: (u16, u16),
460 ) {
461 let rect = Rect::new(0, 0, 71, 71);
462 let widget = empty_widget.quiet_zone(QuietZone::Enabled).scaling(scaling);
463 assert_eq!(widget.size(rect), Size::from(expected));
464 }
465
466 /// The QR code fits into the area without scaling
467 #[rstest]
468 fn render_exact_into_fitting_area(empty_widget: QrCodeWidget) {
469 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 11));
470 empty_widget.render(buf.area, &mut buf);
471 assert_eq!(
472 buf,
473 Buffer::with_lines([
474 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀█",
475 "█ ███ █ █▀▀ ▀ █ ███ █",
476 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ █",
477 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀▀",
478 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄ ",
479 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄ ",
480 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
481 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀█",
482 "█ ███ █ █▀██▄█▄ ▀█▀▀▀",
483 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
484 "▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ",
485 ])
486 );
487 }
488
489 /// The QR code fits into the area without scaling
490 #[rstest]
491 fn render_max_into_fitting_area(empty_widget: QrCodeWidget) {
492 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 11));
493 empty_widget
494 .scaling(Scaling::Max)
495 .render(buf.area, &mut buf);
496 assert_eq!(
497 buf,
498 Buffer::with_lines([
499 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀█",
500 "█ ███ █ █▀▀ ▀ █ ███ █",
501 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ █",
502 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀▀",
503 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄ ",
504 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄ ",
505 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
506 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀█",
507 "█ ███ █ █▀██▄█▄ ▀█▀▀▀",
508 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
509 "▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ",
510 ])
511 );
512 }
513
514 // The QR code is doubled vertically as the min scaling means this needs to render at least
515 // 21x10.5 but the buffer is 21x11
516 ///
517 /// Note: this is an instance where the square aspect ratio of the QR code is not preserved
518 /// correctly. This doesn't align with the documentation of the qrcode crate.
519 #[rstest]
520 fn render_min_into_fitting_area(empty_widget: QrCodeWidget) {
521 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 11));
522 empty_widget
523 .scaling(Scaling::Min)
524 .render(buf.area, &mut buf);
525 assert_eq!(
526 buf,
527 Buffer::with_lines([
528 "███████ ██ █ ███████",
529 "█ █ ██ █ █",
530 "█ ███ █ ███ █ █ ███ █",
531 "█ ███ █ █ █ ███ █",
532 "█ ███ █ ██ █ █ ███ █",
533 "█ █ ████ █ █",
534 "███████ █ █ █ ███████",
535 " ██ ",
536 "█ █████ ███ █████ ",
537 " █ █ █ █████ █ █ ",
538 " ████ █ ██ ",
539 ])
540 );
541 }
542
543 /// The QR code fits into the area without scaling
544 #[rstest]
545 fn render_exact_into_larger_area(empty_widget: QrCodeWidget) {
546 let mut buf = Buffer::empty(Rect::new(0, 0, 22, 12));
547 empty_widget.render(buf.area, &mut buf);
548 assert_eq!(
549 buf,
550 Buffer::with_lines([
551 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀█ ",
552 "█ ███ █ █▀▀ ▀ █ ███ █ ",
553 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ █ ",
554 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀▀ ",
555 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄ ",
556 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄ ",
557 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
558 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀█ ",
559 "█ ███ █ █▀██▄█▄ ▀█▀▀▀ ",
560 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
561 "▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ",
562 " ",
563 ])
564 );
565 }
566
567 /// The QR code fits into the area without scaling
568 #[rstest]
569 fn render_max_into_larger_area(empty_widget: QrCodeWidget) {
570 let mut buf = Buffer::empty(Rect::new(0, 0, 22, 12));
571 empty_widget
572 .scaling(Scaling::Max)
573 .render(buf.area, &mut buf);
574 assert_eq!(
575 buf,
576 Buffer::with_lines([
577 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀█ ",
578 "█ ███ █ █▀▀ ▀ █ ███ █ ",
579 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ █ ",
580 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀▀ ",
581 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄ ",
582 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄ ",
583 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
584 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀█ ",
585 "█ ███ █ █▀██▄█▄ ▀█▀▀▀ ",
586 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
587 "▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ",
588 " ",
589 ])
590 );
591 }
592
593 /// The QR code is doubled vertically and horizontall as the min scaling means this needs to
594 /// render at least 21x10.5 but the buffer is 22x12
595 #[rstest]
596 fn render_min_into_larger_area(empty_widget: QrCodeWidget) {
597 let mut buf = Buffer::empty(Rect::new(0, 0, 22, 12));
598 empty_widget
599 .scaling(Scaling::Min)
600 .render(buf.area, &mut buf);
601 assert_eq!(
602 buf,
603 Buffer::with_lines([
604 "██████████████ ████",
605 "██ ██ ",
606 "██ ██████ ██ ██████",
607 "██ ██████ ██ ██ ",
608 "██ ██████ ██ ████ ",
609 "██ ██ ██████",
610 "██████████████ ██ ██",
611 " ████ ",
612 "██ ██████████ ████",
613 " ██ ██ ██ ██",
614 " ████████ ",
615 "██████ ████ ██",
616 ])
617 );
618 }
619
620 /// The QR code is truncated as the area is smaller than the QR code
621 #[rstest]
622 fn render_exact_into_smaler_area(empty_widget: QrCodeWidget) {
623 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 10));
624 empty_widget.render(buf.area, &mut buf);
625 assert_eq!(
626 buf,
627 Buffer::with_lines([
628 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀",
629 "█ ███ █ █▀▀ ▀ █ ███ ",
630 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ ",
631 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀",
632 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄",
633 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄",
634 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
635 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀",
636 "█ ███ █ █▀██▄█▄ ▀█▀▀",
637 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
638 ])
639 );
640 }
641
642 /// The QR code is truncated as the max scaling means this needs to render at most 21x10.5 but
643 /// the buffer is 20x10
644 #[rstest]
645 fn render_max_into_smaller_area(empty_widget: QrCodeWidget) {
646 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 10));
647 empty_widget
648 .scaling(Scaling::Max)
649 .render(buf.area, &mut buf);
650 assert_eq!(
651 buf,
652 Buffer::with_lines([
653 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀",
654 "█ ███ █ █▀▀ ▀ █ ███ ",
655 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ ",
656 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀",
657 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄",
658 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄",
659 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
660 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀",
661 "█ ███ █ █▀██▄█▄ ▀█▀▀",
662 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
663 ])
664 );
665 }
666
667 /// The QR code is truncated as the min scaling means this needs to render at least 21x10.5 but
668 /// the buffer is already too small
669 #[rstest]
670 fn render_min_into_smaller_area(empty_widget: QrCodeWidget) {
671 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 10));
672 empty_widget
673 .scaling(Scaling::Min)
674 .render(buf.area, &mut buf);
675 assert_eq!(
676 buf,
677 Buffer::with_lines([
678 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀",
679 "█ ███ █ █▀▀ ▀ █ ███ ",
680 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ ",
681 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀",
682 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄",
683 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄",
684 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
685 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀",
686 "█ ███ █ █▀██▄█▄ ▀█▀▀",
687 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
688 ])
689 );
690 }
691
692 /// Exact scaling doesn't scale the QR code
693 #[rstest]
694 fn render_exact_double_height(empty_widget: QrCodeWidget) {
695 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 21));
696 empty_widget.render(buf.area, &mut buf);
697 assert_eq!(
698 buf,
699 Buffer::with_lines([
700 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀█",
701 "█ ███ █ █▀▀ ▀ █ ███ █",
702 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ █",
703 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀▀",
704 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄ ",
705 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄ ",
706 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
707 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀█",
708 "█ ███ █ █▀██▄█▄ ▀█▀▀▀",
709 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
710 "▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ",
711 " ",
712 " ",
713 " ",
714 " ",
715 " ",
716 " ",
717 " ",
718 " ",
719 " ",
720 " ",
721 ])
722 );
723 }
724
725 /// The QR code is doubled vertically
726 ///
727 /// Note: this is an instance where the square aspect ratio of the QR code is not preserved
728 /// correctly. This doesn't align with the documentation of the qrcode crate.
729 #[rstest]
730 fn render_max_double_height(empty_widget: QrCodeWidget) {
731 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 21));
732 empty_widget
733 .scaling(Scaling::Max)
734 .render(buf.area, &mut buf);
735 assert_eq!(
736 buf,
737 Buffer::with_lines([
738 "███████ ██ █ ███████",
739 "█ █ ██ █ █",
740 "█ ███ █ ███ █ █ ███ █",
741 "█ ███ █ █ █ ███ █",
742 "█ ███ █ ██ █ █ ███ █",
743 "█ █ ████ █ █",
744 "███████ █ █ █ ███████",
745 " ██ ",
746 "█ █████ ███ █████ ",
747 " █ █ █ █████ █ █ ",
748 " ████ █ ██ ",
749 "███ ██ █████ █ █ ",
750 "█ █ ███ █ █ █ █ █ ",
751 " ███ █ █ █ ",
752 "███████ ███ █ █████",
753 "█ █ ███ ██ █ █",
754 "█ ███ █ ████ █ █████",
755 "█ ███ █ █ █████ █ ",
756 "█ ███ █ █ █ ██ ",
757 "█ █ ████ █ ",
758 "███████ █ █ █ █ ",
759 ])
760 );
761 }
762
763 /// The QR code is doubled vertically
764 ///
765 /// Note: this is an instance where the square aspect ratio of the QR code is not preserved
766 /// correctly. This doesn't align with the documentation of the qrcode crate.
767 #[rstest]
768 fn render_min_double_height(empty_widget: QrCodeWidget) {
769 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 21));
770 empty_widget
771 .scaling(Scaling::Min)
772 .render(buf.area, &mut buf);
773 assert_eq!(
774 buf,
775 Buffer::with_lines([
776 "███████ ██ █ ███████",
777 "█ █ ██ █ █",
778 "█ ███ █ ███ █ █ ███ █",
779 "█ ███ █ █ █ ███ █",
780 "█ ███ █ ██ █ █ ███ █",
781 "█ █ ████ █ █",
782 "███████ █ █ █ ███████",
783 " ██ ",
784 "█ █████ ███ █████ ",
785 " █ █ █ █████ █ █ ",
786 " ████ █ ██ ",
787 "███ ██ █████ █ █ ",
788 "█ █ ███ █ █ █ █ █ ",
789 " ███ █ █ █ ",
790 "███████ ███ █ █████",
791 "█ █ ███ ██ █ █",
792 "█ ███ █ ████ █ █████",
793 "█ ███ █ █ █████ █ ",
794 "█ ███ █ █ █ ██ ",
795 "█ █ ████ █ ",
796 "███████ █ █ █ █ ",
797 ])
798 );
799 }
800
801 #[rstest]
802 fn render_exact_double_width(empty_widget: QrCodeWidget) {
803 let mut buf = Buffer::empty(Rect::new(0, 0, 42, 11));
804 empty_widget.render(buf.area, &mut buf);
805 assert_eq!(
806 buf,
807 Buffer::with_lines([
808 "█▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀█ ",
809 "█ ███ █ █▀▀ ▀ █ ███ █ ",
810 "█ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ █ ",
811 "▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀▀ ",
812 "▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄ ",
813 "▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄ ",
814 "▀ ▀ ▀▀▀ █▄█ █ █ █ ",
815 "█▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀█ ",
816 "█ ███ █ █▀██▄█▄ ▀█▀▀▀ ",
817 "█ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
818 "▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ",
819 ])
820 );
821 }
822
823 /// The QR code is doubled horizontally as the max scaling means this needs to render at most
824 /// 42x10.5 but the buffer is 42x11
825 ///
826 /// Note: this is an instance where the square aspect ratio of the QR code is not preserved
827 /// correctly. This doesn't align with the documentation of the qrcode crate.
828 #[rstest]
829 fn render_max_double_width(empty_widget: QrCodeWidget) {
830 let mut buf = Buffer::empty(Rect::new(0, 0, 42, 11));
831 empty_widget
832 .scaling(Scaling::Max)
833 .render(buf.area, &mut buf);
834 assert_eq!(
835 buf,
836 Buffer::with_lines([
837 "██▀▀▀▀▀▀▀▀▀▀██ ▀▀▀▀▄▄██ ██▀▀▀▀▀▀▀▀▀▀██",
838 "██ ██████ ██ ██▀▀▀▀ ▀▀ ██ ██████ ██",
839 "██ ▀▀▀▀▀▀ ██ ████▄▄▄▄▀▀ ██ ▀▀▀▀▀▀ ██",
840 "▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ██▄▄▀▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
841 "▀▀ ▀▀██▀▀██▀▀▄▄ ▀▀████▄▄▄▄██▀▀▀▀██▀▀▄▄ ",
842 "▄▄▄▄▄▄ ▀▀████▀▀▄▄▄▄██▄▄██▀▀ ▄▄ ▄▄ ",
843 "▀▀ ▀▀ ▀▀▀▀▀▀ ██▄▄██ ██ ██ ██ ",
844 "██▀▀▀▀▀▀▀▀▀▀██ ▄▄████▀▀ ▀▀ ▄▄██▀▀██▀▀██",
845 "██ ██████ ██ ██▀▀████▄▄██▄▄ ▀▀██▀▀▀▀▀▀",
846 "██ ▀▀▀▀▀▀ ██ ▀▀ ▄▄██▄▄██▀▀ ▄▄ ",
847 "▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ",
848 ])
849 );
850 }
851
852 /// Both the width and height are doubled because the min scaling means the QR code needs to be
853 /// at least 42x10.5 but the buffer is 42x11
854 #[rstest]
855 fn render_min_double_width(empty_widget: QrCodeWidget) {
856 let mut buf = Buffer::empty(Rect::new(0, 0, 42, 11));
857 empty_widget
858 .scaling(Scaling::Min)
859 .render(buf.area, &mut buf);
860 assert_eq!(
861 buf,
862 Buffer::with_lines([
863 "██████████████ ████ ██ ██████████████",
864 "██ ██ ████ ██ ██",
865 "██ ██████ ██ ██████ ██ ██ ██████ ██",
866 "██ ██████ ██ ██ ██ ██████ ██",
867 "██ ██████ ██ ████ ██ ██ ██████ ██",
868 "██ ██ ████████ ██ ██",
869 "██████████████ ██ ██ ██ ██████████████",
870 " ████ ",
871 "██ ██████████ ██████ ██████████ ",
872 " ██ ██ ██ ██████████ ██ ██ ",
873 " ████████ ██ ████ ",
874 ])
875 );
876 }
877
878 #[rstest]
879 fn render_inverted(empty_widget: QrCodeWidget) {
880 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 11));
881 empty_widget
882 .colors(Colors::Inverted)
883 .render(buf.area, &mut buf);
884 assert_eq!(
885 buf,
886 Buffer::with_lines([
887 " ▄▄▄▄▄ ██▄▄▀ █ ▄▄▄▄▄ ",
888 " █ █ █ ▄▄█▄█ █ █ ",
889 " █▄▄▄█ █ ▀▀▄█ █▄▄▄█ ",
890 "▄▄▄▄▄▄▄█ ▀▄█▄█▄▄▄▄▄▄▄",
891 "▄█▄ ▄ ▄▀█▄ ▀▀ ▄▄ ▄▀█",
892 "▀▀▀███▄ ▄▀▀ ▀ ▄█▀█▀█",
893 "▄█▄█▄▄▄█ ▀ █ ██ ██ ██",
894 " ▄▄▄▄▄ █▀ ▄█▄█▀ ▄ ▄ ",
895 " █ █ █ ▄ ▀ ▀█▄ ▄▄▄",
896 " █▄▄▄█ █▄██▀ ▀ ▄█▀███",
897 " ▀ ▀▀▀ ▀▀ ▀▀ ▀▀",
898 ])
899 );
900 }
901
902 #[rstest]
903 fn render_with_quiet_zone(empty_widget: QrCodeWidget) {
904 let mut buf = Buffer::empty(Rect::new(0, 0, 29, 15));
905 empty_widget
906 .quiet_zone(QuietZone::Enabled)
907 .render(buf.area, &mut buf);
908 assert_eq!(
909 buf,
910 Buffer::with_lines([
911 " ",
912 " ",
913 " █▀▀▀▀▀█ ▀▀▄█ █▀▀▀▀▀█ ",
914 " █ ███ █ █▀▀ ▀ █ ███ █ ",
915 " █ ▀▀▀ █ ██▄▄▀ █ ▀▀▀ █ ",
916 " ▀▀▀▀▀▀▀ █▄▀ ▀ ▀▀▀▀▀▀▀ ",
917 " ▀ ▀█▀█▀▄ ▀██▄▄█▀▀█▀▄ ",
918 " ▄▄▄ ▀██▀▄▄█▄█▀ ▄ ▄ ",
919 " ▀ ▀ ▀▀▀ █▄█ █ █ █ ",
920 " █▀▀▀▀▀█ ▄██▀ ▀ ▄█▀█▀█ ",
921 " █ ███ █ █▀██▄█▄ ▀█▀▀▀ ",
922 " █ ▀▀▀ █ ▀ ▄█▄█▀ ▄ ",
923 " ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ",
924 " ",
925 " ",
926 ])
927 );
928 }
929
930 #[rstest]
931 fn render_with_quiet_zone_and_inverted(empty_widget: QrCodeWidget) {
932 let mut buf = Buffer::empty(Rect::new(0, 0, 29, 15));
933 empty_widget
934 .quiet_zone(QuietZone::Enabled)
935 .colors(Colors::Inverted)
936 .render(buf.area, &mut buf);
937 assert_eq!(
938 buf,
939 Buffer::with_lines([
940 "█████████████████████████████",
941 "█████████████████████████████",
942 "████ ▄▄▄▄▄ ██▄▄▀ █ ▄▄▄▄▄ ████",
943 "████ █ █ █ ▄▄█▄█ █ █ ████",
944 "████ █▄▄▄█ █ ▀▀▄█ █▄▄▄█ ████",
945 "████▄▄▄▄▄▄▄█ ▀▄█▄█▄▄▄▄▄▄▄████",
946 "████▄█▄ ▄ ▄▀█▄ ▀▀ ▄▄ ▄▀█████",
947 "████▀▀▀███▄ ▄▀▀ ▀ ▄█▀█▀█████",
948 "████▄█▄█▄▄▄█ ▀ █ ██ ██ ██████",
949 "████ ▄▄▄▄▄ █▀ ▄█▄█▀ ▄ ▄ ████",
950 "████ █ █ █ ▄ ▀ ▀█▄ ▄▄▄████",
951 "████ █▄▄▄█ █▄██▀ ▀ ▄█▀███████",
952 "████▄▄▄▄▄▄▄█▄███▄██▄██▄██████",
953 "█████████████████████████████",
954 "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
955 ])
956 );
957 }
958}