adui_dioxus/components/
qrcode.rs

1//! QRCode component for generating and displaying QR codes.
2//!
3//! Ported from Ant Design 6.x QRCode component.
4
5use dioxus::prelude::*;
6
7/// QR code rendering type.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum QRCodeType {
10    /// Render as SVG (default, better for scaling).
11    #[default]
12    Svg,
13    /// Render as Canvas (better for large QR codes).
14    Canvas,
15}
16
17/// QR code status.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub enum QRCodeStatus {
20    /// Active and scannable.
21    #[default]
22    Active,
23    /// Expired, needs refresh.
24    Expired,
25    /// Loading state.
26    Loading,
27    /// Already scanned.
28    Scanned,
29}
30
31impl QRCodeStatus {
32    fn as_class(&self) -> &'static str {
33        match self {
34            QRCodeStatus::Active => "adui-qrcode-active",
35            QRCodeStatus::Expired => "adui-qrcode-expired",
36            QRCodeStatus::Loading => "adui-qrcode-loading",
37            QRCodeStatus::Scanned => "adui-qrcode-scanned",
38        }
39    }
40}
41
42/// Error correction level for QR codes.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub enum QRCodeErrorLevel {
45    /// ~7% error correction.
46    L,
47    /// ~15% error correction (default).
48    #[default]
49    M,
50    /// ~25% error correction.
51    Q,
52    /// ~30% error correction.
53    H,
54}
55
56/// Props for the QRCode component.
57#[derive(Props, Clone, PartialEq)]
58pub struct QRCodeProps {
59    /// The value/content to encode in the QR code.
60    pub value: String,
61
62    /// Rendering type (SVG or Canvas). Defaults to SVG.
63    #[props(default)]
64    pub r#type: QRCodeType,
65
66    /// Size of the QR code in pixels. Defaults to 160.
67    #[props(default = 160)]
68    pub size: u32,
69
70    /// Icon URL to display in the center.
71    #[props(optional)]
72    pub icon: Option<String>,
73
74    /// Size of the icon. Can be a single number or width/height.
75    #[props(optional)]
76    pub icon_size: Option<u32>,
77
78    /// Foreground color of the QR code. Defaults to current text color.
79    #[props(optional)]
80    pub color: Option<String>,
81
82    /// Background color of the QR code. Defaults to transparent.
83    #[props(optional)]
84    pub bg_color: Option<String>,
85
86    /// Error correction level. Defaults to M.
87    #[props(default)]
88    pub error_level: QRCodeErrorLevel,
89
90    /// Current status of the QR code.
91    #[props(default)]
92    pub status: QRCodeStatus,
93
94    /// Whether to show border. Defaults to true.
95    #[props(default = true)]
96    pub bordered: bool,
97
98    /// Callback when refresh is clicked (for expired status).
99    #[props(optional)]
100    pub on_refresh: Option<EventHandler<()>>,
101
102    /// Extra class name for the container.
103    #[props(optional)]
104    pub class: Option<String>,
105
106    /// Extra class name for the root element.
107    #[props(optional)]
108    pub root_class: Option<String>,
109
110    /// Inline style for the container.
111    #[props(optional)]
112    pub style: Option<String>,
113}
114
115/// QRCode component that generates and displays QR codes.
116///
117/// # Example
118///
119/// ```rust,ignore
120/// rsx! {
121///     QRCode {
122///         value: "https://example.com",
123///         size: 200,
124///         icon: "https://example.com/logo.png",
125///     }
126/// }
127/// ```
128#[component]
129pub fn QRCode(props: QRCodeProps) -> Element {
130    let QRCodeProps {
131        value,
132        r#type,
133        size,
134        icon,
135        icon_size,
136        color,
137        bg_color,
138        error_level,
139        status,
140        bordered,
141        on_refresh,
142        class,
143        root_class,
144        style,
145    } = props;
146
147    // Return empty if no value provided
148    if value.is_empty() {
149        return rsx! {};
150    }
151
152    let fg_color = color.unwrap_or_else(|| "currentColor".to_string());
153    let bg_color = bg_color.unwrap_or_else(|| "transparent".to_string());
154    let icon_size = icon_size.unwrap_or(40);
155
156    // Build class list
157    let mut class_list = vec!["adui-qrcode".to_string()];
158    class_list.push(status.as_class().to_string());
159    if !bordered {
160        class_list.push("adui-qrcode-borderless".to_string());
161    }
162    if let Some(extra) = class {
163        class_list.push(extra);
164    }
165    let class_attr = class_list.join(" ");
166
167    let mut root_class_list = vec!["adui-qrcode-wrapper".to_string()];
168    if let Some(extra) = root_class {
169        root_class_list.push(extra);
170    }
171    let root_class_attr = root_class_list.join(" ");
172
173    let container_style = format!(
174        "width: {}px; height: {}px; background-color: {}; {}",
175        size,
176        size,
177        bg_color,
178        style.unwrap_or_default()
179    );
180
181    // Generate QR code data
182    let qr_modules = generate_qr_modules(&value, error_level);
183    let module_count = qr_modules.len();
184
185    // Render based on type
186    let qr_content = match r#type {
187        QRCodeType::Svg => {
188            render_svg_qrcode(&qr_modules, module_count, size, &fg_color, &icon, icon_size)
189        }
190        QRCodeType::Canvas => {
191            // Canvas rendering would require web_sys; fallback to SVG for now
192            render_svg_qrcode(&qr_modules, module_count, size, &fg_color, &icon, icon_size)
193        }
194    };
195
196    // Status overlay
197    let status_overlay = match status {
198        QRCodeStatus::Active => None,
199        QRCodeStatus::Expired => {
200            let handler = on_refresh;
201            Some(rsx! {
202                div { class: "adui-qrcode-cover",
203                    div { class: "adui-qrcode-status",
204                        span { class: "adui-qrcode-status-text", "QR code expired" }
205                        button {
206                            class: "adui-qrcode-refresh-btn",
207                            onclick: move |_| {
208                                if let Some(cb) = handler.as_ref() {
209                                    cb.call(());
210                                }
211                            },
212                            "Refresh"
213                        }
214                    }
215                }
216            })
217        }
218        QRCodeStatus::Loading => Some(rsx! {
219            div { class: "adui-qrcode-cover",
220                div { class: "adui-qrcode-status",
221                    div { class: "adui-qrcode-spinner" }
222                    span { class: "adui-qrcode-status-text", "Loading..." }
223                }
224            }
225        }),
226        QRCodeStatus::Scanned => Some(rsx! {
227            div { class: "adui-qrcode-cover",
228                div { class: "adui-qrcode-status",
229                    span { class: "adui-qrcode-status-icon", "✓" }
230                    span { class: "adui-qrcode-status-text", "Scanned" }
231                }
232            }
233        }),
234    };
235
236    rsx! {
237        div {
238            class: "{root_class_attr}",
239            div {
240                class: "{class_attr}",
241                style: "{container_style}",
242                {qr_content}
243                {status_overlay}
244            }
245        }
246    }
247}
248
249/// Generate QR code modules (simplified version).
250/// For a production implementation, use a proper QR code library.
251fn generate_qr_modules(value: &str, error_level: QRCodeErrorLevel) -> Vec<Vec<bool>> {
252    // This is a simplified QR code generation that creates a deterministic pattern
253    // based on the input. For production use, integrate a proper QR library like `qrcode`.
254
255    // Calculate version based on content length and error level
256    let version = calculate_version(value.len(), error_level);
257    let module_count = 21 + (version - 1) * 4;
258
259    let mut modules = vec![vec![false; module_count]; module_count];
260
261    // Add finder patterns (the three large squares in corners)
262    add_finder_pattern(&mut modules, 0, 0);
263    add_finder_pattern(&mut modules, module_count - 7, 0);
264    add_finder_pattern(&mut modules, 0, module_count - 7);
265
266    // Add timing patterns
267    add_timing_patterns(&mut modules, module_count);
268
269    // Add alignment pattern for version > 1
270    if version > 1 {
271        add_alignment_pattern(&mut modules, module_count);
272    }
273
274    // Fill data area with pattern derived from value
275    fill_data_pattern(&mut modules, value, module_count);
276
277    modules
278}
279
280/// Calculate QR code version based on content length.
281fn calculate_version(content_len: usize, _error_level: QRCodeErrorLevel) -> usize {
282    // Simplified version calculation
283    if content_len <= 17 {
284        1
285    } else if content_len <= 32 {
286        2
287    } else if content_len <= 53 {
288        3
289    } else if content_len <= 78 {
290        4
291    } else if content_len <= 106 {
292        5
293    } else {
294        6.min((content_len / 20).max(1))
295    }
296}
297
298/// Add finder pattern at specified position.
299fn add_finder_pattern(modules: &mut [Vec<bool>], row: usize, col: usize) {
300    for r in 0..7 {
301        for c in 0..7 {
302            let is_border = r == 0 || r == 6 || c == 0 || c == 6;
303            let is_inner = (2..=4).contains(&r) && (2..=4).contains(&c);
304            if row + r < modules.len() && col + c < modules[0].len() {
305                modules[row + r][col + c] = is_border || is_inner;
306            }
307        }
308    }
309
310    // Add separator (white border around finder)
311    for i in 0..8 {
312        if row + 7 < modules.len() && col + i < modules[0].len() {
313            modules[row + 7][col + i] = false;
314        }
315        if row + i < modules.len() && col + 7 < modules[0].len() {
316            modules[row + i][col + 7] = false;
317        }
318    }
319}
320
321/// Add timing patterns.
322fn add_timing_patterns(modules: &mut [Vec<bool>], size: usize) {
323    for i in 8..size - 8 {
324        let pattern = i % 2 == 0;
325        modules[6][i] = pattern;
326        modules[i][6] = pattern;
327    }
328}
329
330/// Add alignment pattern.
331fn add_alignment_pattern(modules: &mut [Vec<bool>], size: usize) {
332    let center = size - 7;
333    for r in 0..5 {
334        for c in 0..5 {
335            let is_border = r == 0 || r == 4 || c == 0 || c == 4;
336            let is_center = r == 2 && c == 2;
337            let row = center - 2 + r;
338            let col = center - 2 + c;
339            if row < size && col < size {
340                modules[row][col] = is_border || is_center;
341            }
342        }
343    }
344}
345
346/// Fill data area with pattern derived from value.
347fn fill_data_pattern(modules: &mut [Vec<bool>], value: &str, size: usize) {
348    // Simple hash-based pattern generation
349    let hash = simple_hash(value);
350    let mut seed = hash;
351
352    for row in 0..size {
353        for col in 0..size {
354            // Skip finder patterns and timing patterns
355            if is_function_module(row, col, size) {
356                continue;
357            }
358
359            // Generate pseudo-random pattern based on position and hash
360            seed = (seed.wrapping_mul(1103515245).wrapping_add(12345)) & 0x7fffffff;
361            modules[row][col] = (seed % 3) != 0;
362        }
363    }
364}
365
366/// Check if position is a function module (finder, timing, etc.).
367fn is_function_module(row: usize, col: usize, size: usize) -> bool {
368    // Top-left finder
369    if row < 9 && col < 9 {
370        return true;
371    }
372    // Top-right finder
373    if row < 9 && col >= size - 8 {
374        return true;
375    }
376    // Bottom-left finder
377    if row >= size - 8 && col < 9 {
378        return true;
379    }
380    // Timing patterns
381    if row == 6 || col == 6 {
382        return true;
383    }
384    false
385}
386
387/// Simple hash function for generating deterministic patterns.
388fn simple_hash(s: &str) -> u32 {
389    let mut hash: u32 = 5381;
390    for byte in s.bytes() {
391        hash = hash.wrapping_mul(33).wrapping_add(byte as u32);
392    }
393    hash
394}
395
396/// Render QR code as SVG.
397fn render_svg_qrcode(
398    modules: &[Vec<bool>],
399    module_count: usize,
400    size: u32,
401    color: &str,
402    icon: &Option<String>,
403    icon_size: u32,
404) -> Element {
405    let module_size = size as f32 / module_count as f32;
406
407    // Generate path data for filled modules
408    let mut path_data = String::new();
409    for (row, row_modules) in modules.iter().enumerate() {
410        for (col, &is_dark) in row_modules.iter().enumerate() {
411            if is_dark {
412                let x = col as f32 * module_size;
413                let y = row as f32 * module_size;
414                path_data.push_str(&format!(
415                    "M{:.2},{:.2}h{:.2}v{:.2}h-{:.2}z",
416                    x, y, module_size, module_size, module_size
417                ));
418            }
419        }
420    }
421
422    let icon_element = icon.as_ref().map(|url| {
423        let icon_x = (size - icon_size) / 2;
424        let icon_y = (size - icon_size) / 2;
425        rsx! {
426            g {
427                rect {
428                    x: "{icon_x}",
429                    y: "{icon_y}",
430                    width: "{icon_size}",
431                    height: "{icon_size}",
432                    fill: "white",
433                    rx: "4",
434                }
435                image {
436                    href: "{url}",
437                    x: "{icon_x}",
438                    y: "{icon_y}",
439                    width: "{icon_size}",
440                    height: "{icon_size}",
441                }
442            }
443        }
444    });
445
446    rsx! {
447        svg {
448            xmlns: "http://www.w3.org/2000/svg",
449            width: "{size}",
450            height: "{size}",
451            view_box: "0 0 {size} {size}",
452            shape_rendering: "crispEdges",
453            path {
454                fill: "{color}",
455                d: "{path_data}",
456            }
457            {icon_element}
458        }
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn status_class_mapping_is_stable() {
468        assert_eq!(QRCodeStatus::Active.as_class(), "adui-qrcode-active");
469        assert_eq!(QRCodeStatus::Expired.as_class(), "adui-qrcode-expired");
470        assert_eq!(QRCodeStatus::Loading.as_class(), "adui-qrcode-loading");
471        assert_eq!(QRCodeStatus::Scanned.as_class(), "adui-qrcode-scanned");
472    }
473
474    #[test]
475    fn calculate_version_increases_with_content_length() {
476        assert_eq!(calculate_version(10, QRCodeErrorLevel::M), 1);
477        assert_eq!(calculate_version(30, QRCodeErrorLevel::M), 2);
478        assert_eq!(calculate_version(50, QRCodeErrorLevel::M), 3);
479    }
480
481    #[test]
482    fn simple_hash_is_deterministic() {
483        let hash1 = simple_hash("test");
484        let hash2 = simple_hash("test");
485        let hash3 = simple_hash("different");
486
487        assert_eq!(hash1, hash2);
488        assert_ne!(hash1, hash3);
489    }
490
491    #[test]
492    fn generate_qr_modules_creates_correct_size() {
493        let modules = generate_qr_modules("test", QRCodeErrorLevel::M);
494        // Version 1 should be 21x21
495        assert_eq!(modules.len(), 21);
496        assert_eq!(modules[0].len(), 21);
497    }
498
499    #[test]
500    fn finder_pattern_is_added_correctly() {
501        let mut modules = vec![vec![false; 21]; 21];
502        add_finder_pattern(&mut modules, 0, 0);
503
504        // Check corners of finder pattern
505        assert!(modules[0][0]); // top-left
506        assert!(modules[0][6]); // top-right
507        assert!(modules[6][0]); // bottom-left
508        assert!(modules[6][6]); // bottom-right
509
510        // Check center
511        assert!(modules[3][3]);
512    }
513}