fractal/contrib/
qr_code.rs

1// Taken from https://gitlab.gnome.org/msandova/trinket/-/blob/master/src/qr_code.rs
2// All credit goes to Maximiliano
3
4use gettextrs::gettext;
5use gtk::{glib, prelude::*, subclass::prelude::*};
6
7pub(crate) mod imp {
8    use std::cell::{Cell, RefCell};
9
10    use gtk::{gdk, graphene};
11
12    use super::*;
13
14    #[derive(Debug, glib::Properties)]
15    #[properties(wrapper_type = super::QRCode)]
16    pub struct QRCode {
17        pub data: RefCell<QRCodeData>,
18        /// The block size of this QR Code.
19        ///
20        /// Determines the size of the widget.
21        #[property(get, set = Self::set_block_size)]
22        pub block_size: Cell<u32>,
23    }
24
25    impl Default for QRCode {
26        fn default() -> Self {
27            Self {
28                data: Default::default(),
29                block_size: Cell::new(6),
30            }
31        }
32    }
33
34    #[glib::object_subclass]
35    impl ObjectSubclass for QRCode {
36        const NAME: &'static str = "TriQRCode";
37        type Type = super::QRCode;
38        type ParentType = gtk::Widget;
39
40        fn class_init(klass: &mut Self::Class) {
41            klass.set_css_name("qrcode");
42            klass.set_accessible_role(gtk::AccessibleRole::Img);
43        }
44    }
45
46    #[glib::derived_properties]
47    impl ObjectImpl for QRCode {
48        fn constructed(&self) {
49            self.parent_constructed();
50
51            self.obj()
52                .update_property(&[gtk::accessible::Property::Label(&gettext("QR Code"))]);
53        }
54    }
55
56    impl WidgetImpl for QRCode {
57        fn snapshot(&self, snapshot: &gtk::Snapshot) {
58            let obj = self.obj();
59            let square_width = obj.width() as f32 / self.data.borrow().width as f32;
60            let square_height = obj.height() as f32 / self.data.borrow().height as f32;
61
62            self.data
63                .borrow()
64                .items
65                .iter()
66                .enumerate()
67                .for_each(|(y, line)| {
68                    line.iter().enumerate().for_each(|(x, is_dark)| {
69                        let color = if *is_dark {
70                            gdk::RGBA::BLACK
71                        } else {
72                            gdk::RGBA::WHITE
73                        };
74                        let position = graphene::Rect::new(
75                            (x as f32) * square_width,
76                            (y as f32) * square_height,
77                            square_width,
78                            square_height,
79                        );
80
81                        snapshot.append_color(&color, &position);
82                    });
83                });
84        }
85
86        fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
87            let stride = i32::try_from(self.obj().block_size()).expect("block size fits into i32");
88
89            let minimum = match orientation {
90                gtk::Orientation::Horizontal => self.data.borrow().width * stride,
91                gtk::Orientation::Vertical => self.data.borrow().height * stride,
92                _ => unreachable!(),
93            };
94            let natural = std::cmp::max(for_size, minimum);
95            (minimum, natural, -1, -1)
96        }
97    }
98
99    impl QRCode {
100        /// Sets the block size of this QR Code.
101        fn set_block_size(&self, block_size: u32) {
102            self.block_size.set(std::cmp::max(block_size, 1));
103
104            let obj = self.obj();
105            obj.queue_draw();
106            obj.queue_resize();
107        }
108    }
109}
110
111glib::wrapper! {
112    /// A widget that display a QR Code.
113    ///
114    /// The QR code of [`QRCode`] is set with the [QRCode::set_bytes()]
115    /// method. It is recommended for a QR Code to have a quiet zone, in most
116    /// contexts, widgets already count with such a margin.
117    ///
118    /// The code can be themed via css, where a recommended quiet-zone
119    /// can be as a padding:
120    ///
121    /// ```css
122    /// qrcode {
123    ///     color: black;
124    ///     background: white;
125    ///     padding: 24px;  /* 4 ⨉ block-size */
126    /// }
127    /// ```
128    pub struct QRCode(ObjectSubclass<imp::QRCode>)
129        @extends gtk::Widget, @implements gtk::Accessible;
130}
131
132impl QRCode {
133    /// Creates a new [`QRCode`].
134    pub fn new() -> Self {
135        glib::Object::new()
136    }
137
138    /// Creates a new [`QRCode`] with a QR code generated from `bytes`.
139    pub fn from_bytes(bytes: &[u8]) -> Self {
140        let qrcode = Self::new();
141        qrcode.set_bytes(bytes);
142
143        qrcode
144    }
145
146    /// Sets the displayed code of `self` to a QR code generated from `bytes`.
147    pub fn set_bytes(&self, bytes: &[u8]) {
148        let data = QRCodeData::try_from(bytes).unwrap_or_else(|_| {
149            glib::g_warning!(None, "Could not load QRCode from bytes");
150            Default::default()
151        });
152        self.imp().data.replace(data);
153
154        self.queue_draw();
155        self.queue_resize();
156    }
157
158    /// Set the `QrCode` to be displayed.
159    pub fn set_qrcode(&self, qrcode: qrcode::QrCode) {
160        self.imp().data.replace(QRCodeData::from(qrcode));
161
162        self.queue_draw();
163        self.queue_resize();
164    }
165}
166
167impl Default for QRCodeData {
168    fn default() -> Self {
169        Self::try_from("".as_bytes()).unwrap()
170    }
171}
172
173#[derive(Debug, Clone)]
174pub struct QRCodeData {
175    pub width: i32,
176    pub height: i32,
177    pub items: Vec<Vec<bool>>,
178}
179
180impl TryFrom<&[u8]> for QRCodeData {
181    type Error = qrcode::types::QrError;
182
183    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
184        Ok(qrcode::QrCode::new(data)?.into())
185    }
186}
187
188impl From<qrcode::QrCode> for QRCodeData {
189    fn from(code: qrcode::QrCode) -> Self {
190        let items = code
191            .render::<char>()
192            .quiet_zone(false)
193            .module_dimensions(1, 1)
194            .build()
195            .split('\n')
196            .map(|line| {
197                line.chars()
198                    .map(|c| !c.is_whitespace())
199                    .collect::<Vec<bool>>()
200            })
201            .collect::<Vec<Vec<bool>>>();
202
203        let size = items
204            .len()
205            .try_into()
206            .expect("count of items fits into i32");
207        Self {
208            width: size,
209            height: size,
210            items,
211        }
212    }
213}