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//! ![Demo](https://vhs.charm.sh/vhs-nUpcmCP1igCcGoJ5iio07.gif)
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}