Skip to main content

agg_gui/widgets/
qr_view.rs

1//! `QrView` — renders a QR code for an arbitrary string.
2//!
3//! Adapted from the Marbles project's `qr_widget` into agg-gui proper so any
4//! app can show a scannable code (per the project rule: missing widgets are
5//! added to agg-gui, not worked around downstream).
6//!
7//! The encoded text can be fixed (`QrView::new`) or pulled live from a shared
8//! `Rc<RefCell<String>>` (`with_text_source`) — handy when the value (e.g. a
9//! LAN URL with a freshly-minted peer id) isn't known until after the widget
10//! tree is built.  An optional visibility cell lets a parent hide the code
11//! once it has served its purpose.
12//!
13//! Opts into agg-gui's per-widget backbuffer so the modules are rasterised
14//! once and reused as a cached bitmap on subsequent frames.
15
16use std::cell::{Cell, RefCell};
17use std::rc::Rc;
18
19use qrcodegen::{QrCode, QrCodeEcc};
20
21use crate::color::Color;
22use crate::draw_ctx::DrawCtx;
23use crate::event::{Event, EventResult};
24use crate::geometry::{Point, Rect, Size};
25use crate::widget::Widget;
26
27pub struct QrView {
28    bounds: Rect,
29    children: Vec<Box<dyn Widget>>, // always empty
30    /// Fixed text; ignored when `text_source` is set.
31    text: String,
32    /// Optional live text source. When present its value wins over `text`.
33    text_source: Option<Rc<RefCell<String>>>,
34    /// Last text the cached bitmap was painted for. If the live text diverges,
35    /// the cache is invalidated so the next paint re-rasterises.
36    rasterised_text: String,
37    cache: crate::widget::BackbufferCache,
38    /// Optional visibility flag; `None` means always visible.
39    visible: Option<Rc<Cell<bool>>>,
40    /// Quiet-zone padding as a fraction of the smaller side (default 0.08).
41    quiet_zone: f64,
42}
43
44impl QrView {
45    /// A QR view with fixed text.
46    pub fn new<S: Into<String>>(text: S) -> Self {
47        Self {
48            bounds: Rect::new(0.0, 0.0, 160.0, 160.0),
49            children: Vec::new(),
50            text: text.into(),
51            text_source: None,
52            rasterised_text: String::new(),
53            cache: crate::widget::BackbufferCache::new(),
54            visible: None,
55            quiet_zone: 0.08,
56        }
57    }
58
59    /// Pull the encoded text from a shared cell each paint. Updates to the
60    /// cell are picked up automatically (the cache invalidates on change).
61    pub fn with_text_source(mut self, source: Rc<RefCell<String>>) -> Self {
62        self.text_source = Some(source);
63        self
64    }
65
66    /// Gate visibility on a shared flag (e.g. hide once connected).
67    pub fn with_visibility(mut self, visible: Rc<Cell<bool>>) -> Self {
68        self.visible = Some(visible);
69        self
70    }
71
72    pub fn with_quiet_zone(mut self, fraction: f64) -> Self {
73        self.quiet_zone = fraction.max(0.0);
74        self
75    }
76
77    /// Replace the fixed text. No effect on the `text_source` path.
78    pub fn set_text(&mut self, text: &str) {
79        if self.text == text {
80            return;
81        }
82        self.text.clear();
83        self.text.push_str(text);
84        self.cache.invalidate();
85    }
86
87    /// The text that should be encoded this frame.
88    fn current_text(&self) -> String {
89        match &self.text_source {
90            Some(src) => src.borrow().clone(),
91            None => self.text.clone(),
92        }
93    }
94
95    fn is_shown(&self) -> bool {
96        self.visible.as_ref().map(|c| c.get()).unwrap_or(true)
97    }
98}
99
100impl Widget for QrView {
101    fn type_name(&self) -> &'static str {
102        "QrView"
103    }
104
105    fn bounds(&self) -> Rect {
106        self.bounds
107    }
108
109    fn set_bounds(&mut self, bounds: Rect) {
110        if (bounds.width - self.bounds.width).abs() > 0.5
111            || (bounds.height - self.bounds.height).abs() > 0.5
112        {
113            self.cache.invalidate();
114        }
115        self.bounds = bounds;
116    }
117
118    fn children(&self) -> &[Box<dyn Widget>] {
119        &self.children
120    }
121
122    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
123        &mut self.children
124    }
125
126    fn hit_test(&self, _local_pos: Point) -> bool {
127        false
128    }
129
130    fn is_visible(&self) -> bool {
131        self.is_shown()
132    }
133
134    fn layout(&mut self, available: Size) -> Size {
135        self.bounds = Rect::new(0.0, 0.0, available.width, available.height);
136        available
137    }
138
139    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
140        let text = self.current_text();
141        if text != self.rasterised_text {
142            self.rasterised_text.clear();
143            self.rasterised_text.push_str(&text);
144            self.cache.invalidate();
145        }
146
147        let w = self.bounds.width;
148        let h = self.bounds.height;
149        if w <= 0.0 || h <= 0.0 {
150            return;
151        }
152
153        // The code is drawn into a centred square so it stays scannable even
154        // when the host area is non-square.
155        let side = w.min(h);
156        let ox = (w - side) * 0.5;
157        let oy = (h - side) * 0.5;
158
159        // White background behind the (potentially) larger area keeps the
160        // quiet zone clean and LCD text caches happy with an opaque dst.
161        ctx.set_fill_color(Color::from_rgb8(255, 255, 255));
162        ctx.begin_path();
163        ctx.rect(ox, oy, side, side);
164        ctx.fill();
165
166        let qr = match QrCode::encode_text(&text, QrCodeEcc::Low) {
167            Ok(qr) => qr,
168            Err(_) => return,
169        };
170        let modules = qr.size();
171        if modules <= 0 {
172            return;
173        }
174
175        let pad = side * self.quiet_zone;
176        let inner = side - 2.0 * pad;
177        if inner <= 0.0 {
178            return;
179        }
180        let module_size = inner / modules as f64;
181        let origin_x = ox + pad;
182        // qrcodegen is row-major top-down; agg-gui is Y-up, so flip the row.
183        let origin_y = oy + pad;
184
185        ctx.set_fill_color(Color::from_rgb8(0, 0, 0));
186        for j in 0..modules {
187            for i in 0..modules {
188                if !qr.get_module(i, j) {
189                    continue;
190                }
191                let x = origin_x + i as f64 * module_size;
192                let y = origin_y + (modules - 1 - j) as f64 * module_size;
193                ctx.begin_path();
194                ctx.rect(x, y, module_size + 0.5, module_size + 0.5);
195                ctx.fill();
196            }
197        }
198    }
199
200    fn on_event(&mut self, _event: &Event) -> EventResult {
201        EventResult::Ignored
202    }
203
204    fn backbuffer_cache_mut(&mut self) -> Option<&mut crate::widget::BackbufferCache> {
205        // Hidden → drop the cached blit so the framework's invisible-widget
206        // branch doesn't composite a stale bitmap.
207        if !self.is_shown() {
208            return None;
209        }
210        Some(&mut self.cache)
211    }
212}