Skip to main content

par_term_render/cell_renderer/
block_chars.rs

1//! Block character detection and geometric rendering utilities.
2//!
3//! This module provides utilities for detecting Unicode block drawing characters
4//! and rendering them geometrically to avoid gaps between adjacent cells.
5//!
6//! The box drawing implementation uses a 7-position grid system inspired by iTerm2,
7//! where lines share exact endpoint coordinates at corners and junctions.
8
9/// Unicode ranges for block drawing and related characters
10pub mod ranges {
11    /// Box Drawing characters (U+2500–U+257F)
12    pub const BOX_DRAWING_START: u32 = 0x2500;
13    pub const BOX_DRAWING_END: u32 = 0x257F;
14
15    /// Block Elements (U+2580–U+259F)
16    pub const BLOCK_ELEMENTS_START: u32 = 0x2580;
17    pub const BLOCK_ELEMENTS_END: u32 = 0x259F;
18
19    /// Geometric Shapes (U+25A0–U+25FF)
20    pub const GEOMETRIC_SHAPES_START: u32 = 0x25A0;
21    pub const GEOMETRIC_SHAPES_END: u32 = 0x25FF;
22
23    /// Powerline symbols (Private Use Area)
24    pub const POWERLINE_START: u32 = 0xE0A0;
25    pub const POWERLINE_END: u32 = 0xE0D4;
26
27    /// Braille Patterns (U+2800–U+28FF)
28    pub const BRAILLE_START: u32 = 0x2800;
29    pub const BRAILLE_END: u32 = 0x28FF;
30
31    /// Miscellaneous Symbols (U+2600–U+26FF) — includes ballot boxes (☐☑☒)
32    pub const MISC_SYMBOLS_START: u32 = 0x2600;
33    pub const MISC_SYMBOLS_END: u32 = 0x26FF;
34
35    /// Dingbats (U+2700–U+27BF) — includes check marks (✓✔✗✘)
36    pub const DINGBATS_START: u32 = 0x2700;
37    pub const DINGBATS_END: u32 = 0x27BF;
38}
39
40/// 7-position grid system for box drawing (normalized 0.0-1.0)
41///
42/// This grid provides precise positioning for light, heavy, and double lines:
43/// - Light lines use the center position (D/V4)
44/// - Heavy lines use two parallel strokes at C+E / V3+V5
45/// - Double lines use two strokes at B+F / V2+V6
46///
47/// ```text
48///     A     B     C     D     E     F     G
49///    0.0  0.25  0.40  0.50  0.60  0.75  1.0
50///    left  |     |   center  |     |   right
51///          heavy-outer       heavy-outer
52///                heavy-inner
53/// ```
54mod grid {
55    // Horizontal positions
56    pub const A: f32 = 0.0; // Left edge
57    pub const C: f32 = 0.40; // Heavy/double inner left
58    pub const D: f32 = 0.50; // Center (light lines)
59    pub const E: f32 = 0.60; // Heavy/double inner right
60    pub const G: f32 = 1.0; // Right edge
61
62    // Vertical positions (same values)
63    pub const V1: f32 = 0.0; // Top edge
64    pub const V3: f32 = 0.40; // Heavy/double inner top
65    pub const V4: f32 = 0.50; // Center (light lines)
66    pub const V5: f32 = 0.60; // Heavy/double inner bottom
67    pub const V7: f32 = 1.0; // Bottom edge
68
69    // Line thicknesses (as fraction of cell dimension)
70    pub const LIGHT_THICKNESS: f32 = 0.12;
71    pub const HEAVY_THICKNESS: f32 = 0.20;
72    pub const DOUBLE_THICKNESS: f32 = 0.08;
73}
74
75/// Classification of block characters for rendering optimization
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum BlockCharType {
78    /// Not a block character - render normally
79    None,
80    /// Box drawing lines (─, │, ┌, ┐, etc.) - snap to cell boundaries
81    BoxDrawing,
82    /// Solid block elements (█, ▄, ▀, etc.) - render geometrically
83    SolidBlock,
84    /// Partial block elements (▌, ▐, ▖, etc.) - render geometrically
85    PartialBlock,
86    /// Shade characters (░, ▒, ▓) - use font glyph with snapping
87    Shade,
88    /// Geometric shapes (■, □, etc.) - snap to boundaries
89    Geometric,
90    /// Powerline symbols - snap to boundaries
91    Powerline,
92    /// Braille patterns - use font glyph
93    Braille,
94    /// Miscellaneous symbols (ballot boxes, check marks, etc.) - snap to boundaries
95    Symbol,
96}
97
98/// Classify a character for rendering optimization
99pub fn classify_char(ch: char) -> BlockCharType {
100    let code = ch as u32;
101
102    // Box Drawing (U+2500–U+257F)
103    if (ranges::BOX_DRAWING_START..=ranges::BOX_DRAWING_END).contains(&code) {
104        return BlockCharType::BoxDrawing;
105    }
106
107    // Block Elements (U+2580–U+259F)
108    if (ranges::BLOCK_ELEMENTS_START..=ranges::BLOCK_ELEMENTS_END).contains(&code) {
109        return classify_block_element(ch);
110    }
111
112    // Geometric Shapes (U+25A0–U+25FF)
113    if (ranges::GEOMETRIC_SHAPES_START..=ranges::GEOMETRIC_SHAPES_END).contains(&code) {
114        return BlockCharType::Geometric;
115    }
116
117    // Powerline symbols
118    if (ranges::POWERLINE_START..=ranges::POWERLINE_END).contains(&code) {
119        return BlockCharType::Powerline;
120    }
121
122    // Braille patterns
123    if (ranges::BRAILLE_START..=ranges::BRAILLE_END).contains(&code) {
124        return BlockCharType::Braille;
125    }
126
127    // Miscellaneous Symbols (ballot boxes, etc.)
128    if (ranges::MISC_SYMBOLS_START..=ranges::MISC_SYMBOLS_END).contains(&code) {
129        return BlockCharType::Symbol;
130    }
131
132    // Dingbats (check marks, etc.)
133    if (ranges::DINGBATS_START..=ranges::DINGBATS_END).contains(&code) {
134        return BlockCharType::Symbol;
135    }
136
137    BlockCharType::None
138}
139
140/// Classify block elements into solid, partial, or shade
141fn classify_block_element(ch: char) -> BlockCharType {
142    match ch {
143        // Shade characters
144        '\u{2591}' | '\u{2592}' | '\u{2593}' => BlockCharType::Shade,
145
146        // Full block
147        '\u{2588}' => BlockCharType::SolidBlock,
148
149        // Partial blocks (half blocks, quadrants, eighth blocks)
150        '\u{2580}'..='\u{2590}' | '\u{2594}'..='\u{259F}' => BlockCharType::PartialBlock,
151
152        _ => BlockCharType::PartialBlock,
153    }
154}
155
156/// Check if a character should have its glyph snapped to cell boundaries
157pub fn should_snap_to_boundaries(char_type: BlockCharType) -> bool {
158    matches!(
159        char_type,
160        BlockCharType::BoxDrawing
161            | BlockCharType::SolidBlock
162            | BlockCharType::PartialBlock
163            | BlockCharType::Geometric
164            | BlockCharType::Powerline
165            | BlockCharType::Symbol
166    )
167}
168
169/// Check if a character should be rendered geometrically instead of using font glyphs
170pub fn should_render_geometrically(char_type: BlockCharType) -> bool {
171    matches!(
172        char_type,
173        BlockCharType::SolidBlock
174            | BlockCharType::PartialBlock
175            | BlockCharType::BoxDrawing
176            | BlockCharType::Geometric
177    )
178}
179
180/// A line segment for box drawing
181/// Coordinates are normalized (0.0-1.0) within the cell
182#[derive(Debug, Clone, Copy)]
183struct LineSegment {
184    x1: f32,
185    y1: f32,
186    x2: f32,
187    y2: f32,
188    thickness: f32,
189}
190
191impl LineSegment {
192    const fn new(x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32) -> Self {
193        Self {
194            x1,
195            y1,
196            x2,
197            y2,
198            thickness,
199        }
200    }
201
202    /// Create a horizontal line segment
203    const fn horizontal(y: f32, x1: f32, x2: f32, thickness: f32) -> Self {
204        Self::new(x1, y, x2, y, thickness)
205    }
206
207    /// Create a vertical line segment
208    const fn vertical(x: f32, y1: f32, y2: f32, thickness: f32) -> Self {
209        Self::new(x, y1, x, y2, thickness)
210    }
211
212    /// Convert to a geometric block (rectangle)
213    fn to_block(self, aspect_ratio: f32) -> GeometricBlock {
214        let is_horizontal = (self.y1 - self.y2).abs() < 0.001;
215        let is_vertical = (self.x1 - self.x2).abs() < 0.001;
216
217        if is_horizontal {
218            // Horizontal line: thickness applies to height
219            let x = self.x1.min(self.x2);
220            let width = (self.x2 - self.x1).abs();
221            let height = self.thickness;
222            let y = self.y1 - height / 2.0;
223            GeometricBlock::new(x, y, width, height)
224        } else if is_vertical {
225            // Vertical line: thickness adjusted by aspect ratio for visual consistency
226            let y = self.y1.min(self.y2);
227            let height = (self.y2 - self.y1).abs();
228            let width = self.thickness * aspect_ratio;
229            let x = self.x1 - width / 2.0;
230            GeometricBlock::new(x, y, width, height)
231        } else {
232            // Diagonal or other - treat as rectangle from corner to corner
233            let x = self.x1.min(self.x2);
234            let y = self.y1.min(self.y2);
235            let width = (self.x2 - self.x1).abs().max(self.thickness);
236            let height = (self.y2 - self.y1).abs().max(self.thickness);
237            GeometricBlock::new(x, y, width, height)
238        }
239    }
240}
241
242/// Represents line segments for box drawing characters
243#[derive(Debug, Clone)]
244pub struct BoxDrawingGeometry {
245    pub segments: Vec<GeometricBlock>,
246}
247
248impl BoxDrawingGeometry {
249    fn from_lines(lines: &[LineSegment], aspect_ratio: f32) -> Self {
250        Self {
251            segments: lines.iter().map(|l| l.to_block(aspect_ratio)).collect(),
252        }
253    }
254}
255
256/// Get geometric representation of a box drawing character
257/// aspect_ratio = cell_height / cell_width (used to make lines visually equal thickness)
258/// Returns None if the character should use font rendering
259pub fn get_box_drawing_geometry(ch: char, aspect_ratio: f32) -> Option<BoxDrawingGeometry> {
260    use grid::*;
261
262    let lt = LIGHT_THICKNESS;
263    let ht = HEAVY_THICKNESS;
264    let dt = DOUBLE_THICKNESS;
265
266    let lines: &[LineSegment] = match ch {
267        // ═══════════════════════════════════════════════════════════════════
268        // LIGHT LINES
269        // ═══════════════════════════════════════════════════════════════════
270
271        // ─ Light horizontal
272        '\u{2500}' => &[LineSegment::horizontal(V4, A, G, lt)],
273
274        // │ Light vertical
275        '\u{2502}' => &[LineSegment::vertical(D, V1, V7, lt)],
276
277        // ┌ Light down and right - lines meet at center
278        '\u{250C}' => &[
279            LineSegment::horizontal(V4, D, G, lt),
280            LineSegment::vertical(D, V4, V7, lt),
281        ],
282
283        // ┐ Light down and left
284        '\u{2510}' => &[
285            LineSegment::horizontal(V4, A, D, lt),
286            LineSegment::vertical(D, V4, V7, lt),
287        ],
288
289        // └ Light up and right
290        '\u{2514}' => &[
291            LineSegment::horizontal(V4, D, G, lt),
292            LineSegment::vertical(D, V1, V4, lt),
293        ],
294
295        // ┘ Light up and left
296        '\u{2518}' => &[
297            LineSegment::horizontal(V4, A, D, lt),
298            LineSegment::vertical(D, V1, V4, lt),
299        ],
300
301        // ├ Light vertical and right
302        '\u{251C}' => &[
303            LineSegment::vertical(D, V1, V7, lt),
304            LineSegment::horizontal(V4, D, G, lt),
305        ],
306
307        // ┤ Light vertical and left
308        '\u{2524}' => &[
309            LineSegment::vertical(D, V1, V7, lt),
310            LineSegment::horizontal(V4, A, D, lt),
311        ],
312
313        // ┬ Light down and horizontal
314        '\u{252C}' => &[
315            LineSegment::horizontal(V4, A, G, lt),
316            LineSegment::vertical(D, V4, V7, lt),
317        ],
318
319        // ┴ Light up and horizontal
320        '\u{2534}' => &[
321            LineSegment::horizontal(V4, A, G, lt),
322            LineSegment::vertical(D, V1, V4, lt),
323        ],
324
325        // ┼ Light vertical and horizontal
326        '\u{253C}' => &[
327            LineSegment::horizontal(V4, A, G, lt),
328            LineSegment::vertical(D, V1, V7, lt),
329        ],
330
331        // ═══════════════════════════════════════════════════════════════════
332        // HEAVY LINES (two parallel strokes)
333        // ═══════════════════════════════════════════════════════════════════
334
335        // ━ Heavy horizontal
336        '\u{2501}' => &[LineSegment::horizontal(V4, A, G, ht)],
337
338        // ┃ Heavy vertical
339        '\u{2503}' => &[LineSegment::vertical(D, V1, V7, ht)],
340
341        // ┏ Heavy down and right
342        '\u{250F}' => &[
343            LineSegment::horizontal(V4, D, G, ht),
344            LineSegment::vertical(D, V4, V7, ht),
345        ],
346
347        // ┓ Heavy down and left
348        '\u{2513}' => &[
349            LineSegment::horizontal(V4, A, D, ht),
350            LineSegment::vertical(D, V4, V7, ht),
351        ],
352
353        // ┗ Heavy up and right
354        '\u{2517}' => &[
355            LineSegment::horizontal(V4, D, G, ht),
356            LineSegment::vertical(D, V1, V4, ht),
357        ],
358
359        // ┛ Heavy up and left
360        '\u{251B}' => &[
361            LineSegment::horizontal(V4, A, D, ht),
362            LineSegment::vertical(D, V1, V4, ht),
363        ],
364
365        // ┣ Heavy vertical and right
366        '\u{2523}' => &[
367            LineSegment::vertical(D, V1, V7, ht),
368            LineSegment::horizontal(V4, D, G, ht),
369        ],
370
371        // ┫ Heavy vertical and left
372        '\u{252B}' => &[
373            LineSegment::vertical(D, V1, V7, ht),
374            LineSegment::horizontal(V4, A, D, ht),
375        ],
376
377        // ┳ Heavy down and horizontal
378        '\u{2533}' => &[
379            LineSegment::horizontal(V4, A, G, ht),
380            LineSegment::vertical(D, V4, V7, ht),
381        ],
382
383        // ┻ Heavy up and horizontal
384        '\u{253B}' => &[
385            LineSegment::horizontal(V4, A, G, ht),
386            LineSegment::vertical(D, V1, V4, ht),
387        ],
388
389        // ╋ Heavy vertical and horizontal
390        '\u{254B}' => &[
391            LineSegment::horizontal(V4, A, G, ht),
392            LineSegment::vertical(D, V1, V7, ht),
393        ],
394
395        // ═══════════════════════════════════════════════════════════════════
396        // MIXED LIGHT/HEAVY LINES
397        // ═══════════════════════════════════════════════════════════════════
398
399        // ┍ Down light and right heavy
400        '\u{250D}' => &[
401            LineSegment::horizontal(V4, D, G, ht),
402            LineSegment::vertical(D, V4, V7, lt),
403        ],
404
405        // ┎ Down heavy and right light
406        '\u{250E}' => &[
407            LineSegment::horizontal(V4, D, G, lt),
408            LineSegment::vertical(D, V4, V7, ht),
409        ],
410
411        // ┑ Down light and left heavy
412        '\u{2511}' => &[
413            LineSegment::horizontal(V4, A, D, ht),
414            LineSegment::vertical(D, V4, V7, lt),
415        ],
416
417        // ┒ Down heavy and left light
418        '\u{2512}' => &[
419            LineSegment::horizontal(V4, A, D, lt),
420            LineSegment::vertical(D, V4, V7, ht),
421        ],
422
423        // ┕ Up light and right heavy
424        '\u{2515}' => &[
425            LineSegment::horizontal(V4, D, G, ht),
426            LineSegment::vertical(D, V1, V4, lt),
427        ],
428
429        // ┖ Up heavy and right light
430        '\u{2516}' => &[
431            LineSegment::horizontal(V4, D, G, lt),
432            LineSegment::vertical(D, V1, V4, ht),
433        ],
434
435        // ┙ Up light and left heavy
436        '\u{2519}' => &[
437            LineSegment::horizontal(V4, A, D, ht),
438            LineSegment::vertical(D, V1, V4, lt),
439        ],
440
441        // ┚ Up heavy and left light
442        '\u{251A}' => &[
443            LineSegment::horizontal(V4, A, D, lt),
444            LineSegment::vertical(D, V1, V4, ht),
445        ],
446
447        // ┝ Vertical light and right heavy
448        '\u{251D}' => &[
449            LineSegment::vertical(D, V1, V7, lt),
450            LineSegment::horizontal(V4, D, G, ht),
451        ],
452
453        // ┞ Up heavy and right down light
454        '\u{251E}' => &[
455            LineSegment::vertical(D, V1, V4, ht),
456            LineSegment::vertical(D, V4, V7, lt),
457            LineSegment::horizontal(V4, D, G, lt),
458        ],
459
460        // ┟ Down heavy and right up light
461        '\u{251F}' => &[
462            LineSegment::vertical(D, V1, V4, lt),
463            LineSegment::vertical(D, V4, V7, ht),
464            LineSegment::horizontal(V4, D, G, lt),
465        ],
466
467        // ┠ Vertical heavy and right light
468        '\u{2520}' => &[
469            LineSegment::vertical(D, V1, V7, ht),
470            LineSegment::horizontal(V4, D, G, lt),
471        ],
472
473        // ┡ Down light and right up heavy
474        '\u{2521}' => &[
475            LineSegment::vertical(D, V1, V4, ht),
476            LineSegment::vertical(D, V4, V7, lt),
477            LineSegment::horizontal(V4, D, G, ht),
478        ],
479
480        // ┢ Up light and right down heavy
481        '\u{2522}' => &[
482            LineSegment::vertical(D, V1, V4, lt),
483            LineSegment::vertical(D, V4, V7, ht),
484            LineSegment::horizontal(V4, D, G, ht),
485        ],
486
487        // ┥ Vertical light and left heavy
488        '\u{2525}' => &[
489            LineSegment::vertical(D, V1, V7, lt),
490            LineSegment::horizontal(V4, A, D, ht),
491        ],
492
493        // ┦ Up heavy and left down light
494        '\u{2526}' => &[
495            LineSegment::vertical(D, V1, V4, ht),
496            LineSegment::vertical(D, V4, V7, lt),
497            LineSegment::horizontal(V4, A, D, lt),
498        ],
499
500        // ┧ Down heavy and left up light
501        '\u{2527}' => &[
502            LineSegment::vertical(D, V1, V4, lt),
503            LineSegment::vertical(D, V4, V7, ht),
504            LineSegment::horizontal(V4, A, D, lt),
505        ],
506
507        // ┨ Vertical heavy and left light
508        '\u{2528}' => &[
509            LineSegment::vertical(D, V1, V7, ht),
510            LineSegment::horizontal(V4, A, D, lt),
511        ],
512
513        // ┩ Down light and left up heavy
514        '\u{2529}' => &[
515            LineSegment::vertical(D, V1, V4, ht),
516            LineSegment::vertical(D, V4, V7, lt),
517            LineSegment::horizontal(V4, A, D, ht),
518        ],
519
520        // ┪ Up light and left down heavy
521        '\u{252A}' => &[
522            LineSegment::vertical(D, V1, V4, lt),
523            LineSegment::vertical(D, V4, V7, ht),
524            LineSegment::horizontal(V4, A, D, ht),
525        ],
526
527        // ┭ Left heavy and right down light
528        '\u{252D}' => &[
529            LineSegment::horizontal(V4, A, D, ht),
530            LineSegment::horizontal(V4, D, G, lt),
531            LineSegment::vertical(D, V4, V7, lt),
532        ],
533
534        // ┮ Right heavy and left down light
535        '\u{252E}' => &[
536            LineSegment::horizontal(V4, A, D, lt),
537            LineSegment::horizontal(V4, D, G, ht),
538            LineSegment::vertical(D, V4, V7, lt),
539        ],
540
541        // ┯ Down light and horizontal heavy
542        '\u{252F}' => &[
543            LineSegment::horizontal(V4, A, G, ht),
544            LineSegment::vertical(D, V4, V7, lt),
545        ],
546
547        // ┰ Down heavy and horizontal light
548        '\u{2530}' => &[
549            LineSegment::horizontal(V4, A, G, lt),
550            LineSegment::vertical(D, V4, V7, ht),
551        ],
552
553        // ┱ Right light and left down heavy
554        '\u{2531}' => &[
555            LineSegment::horizontal(V4, A, D, ht),
556            LineSegment::horizontal(V4, D, G, lt),
557            LineSegment::vertical(D, V4, V7, ht),
558        ],
559
560        // ┲ Left light and right down heavy
561        '\u{2532}' => &[
562            LineSegment::horizontal(V4, A, D, lt),
563            LineSegment::horizontal(V4, D, G, ht),
564            LineSegment::vertical(D, V4, V7, ht),
565        ],
566
567        // ┵ Left heavy and right up light
568        '\u{2535}' => &[
569            LineSegment::horizontal(V4, A, D, ht),
570            LineSegment::horizontal(V4, D, G, lt),
571            LineSegment::vertical(D, V1, V4, lt),
572        ],
573
574        // ┶ Right heavy and left up light
575        '\u{2536}' => &[
576            LineSegment::horizontal(V4, A, D, lt),
577            LineSegment::horizontal(V4, D, G, ht),
578            LineSegment::vertical(D, V1, V4, lt),
579        ],
580
581        // ┷ Up light and horizontal heavy
582        '\u{2537}' => &[
583            LineSegment::horizontal(V4, A, G, ht),
584            LineSegment::vertical(D, V1, V4, lt),
585        ],
586
587        // ┸ Up heavy and horizontal light
588        '\u{2538}' => &[
589            LineSegment::horizontal(V4, A, G, lt),
590            LineSegment::vertical(D, V1, V4, ht),
591        ],
592
593        // ┹ Right light and left up heavy
594        '\u{2539}' => &[
595            LineSegment::horizontal(V4, A, D, ht),
596            LineSegment::horizontal(V4, D, G, lt),
597            LineSegment::vertical(D, V1, V4, ht),
598        ],
599
600        // ┺ Left light and right up heavy
601        '\u{253A}' => &[
602            LineSegment::horizontal(V4, A, D, lt),
603            LineSegment::horizontal(V4, D, G, ht),
604            LineSegment::vertical(D, V1, V4, ht),
605        ],
606
607        // ┽ Left heavy and right vertical light
608        '\u{253D}' => &[
609            LineSegment::horizontal(V4, A, D, ht),
610            LineSegment::horizontal(V4, D, G, lt),
611            LineSegment::vertical(D, V1, V7, lt),
612        ],
613
614        // ┾ Right heavy and left vertical light
615        '\u{253E}' => &[
616            LineSegment::horizontal(V4, A, D, lt),
617            LineSegment::horizontal(V4, D, G, ht),
618            LineSegment::vertical(D, V1, V7, lt),
619        ],
620
621        // ┿ Vertical light and horizontal heavy
622        '\u{253F}' => &[
623            LineSegment::horizontal(V4, A, G, ht),
624            LineSegment::vertical(D, V1, V7, lt),
625        ],
626
627        // ╀ Up heavy and down horizontal light
628        '\u{2540}' => &[
629            LineSegment::horizontal(V4, A, G, lt),
630            LineSegment::vertical(D, V1, V4, ht),
631            LineSegment::vertical(D, V4, V7, lt),
632        ],
633
634        // ╁ Down heavy and up horizontal light
635        '\u{2541}' => &[
636            LineSegment::horizontal(V4, A, G, lt),
637            LineSegment::vertical(D, V1, V4, lt),
638            LineSegment::vertical(D, V4, V7, ht),
639        ],
640
641        // ╂ Vertical heavy and horizontal light
642        '\u{2542}' => &[
643            LineSegment::horizontal(V4, A, G, lt),
644            LineSegment::vertical(D, V1, V7, ht),
645        ],
646
647        // ╃ Left up heavy and right down light
648        '\u{2543}' => &[
649            LineSegment::horizontal(V4, A, D, ht),
650            LineSegment::horizontal(V4, D, G, lt),
651            LineSegment::vertical(D, V1, V4, ht),
652            LineSegment::vertical(D, V4, V7, lt),
653        ],
654
655        // ╄ Right up heavy and left down light
656        '\u{2544}' => &[
657            LineSegment::horizontal(V4, A, D, lt),
658            LineSegment::horizontal(V4, D, G, ht),
659            LineSegment::vertical(D, V1, V4, ht),
660            LineSegment::vertical(D, V4, V7, lt),
661        ],
662
663        // ╅ Left down heavy and right up light
664        '\u{2545}' => &[
665            LineSegment::horizontal(V4, A, D, ht),
666            LineSegment::horizontal(V4, D, G, lt),
667            LineSegment::vertical(D, V1, V4, lt),
668            LineSegment::vertical(D, V4, V7, ht),
669        ],
670
671        // ╆ Right down heavy and left up light
672        '\u{2546}' => &[
673            LineSegment::horizontal(V4, A, D, lt),
674            LineSegment::horizontal(V4, D, G, ht),
675            LineSegment::vertical(D, V1, V4, lt),
676            LineSegment::vertical(D, V4, V7, ht),
677        ],
678
679        // ╇ Down light and up horizontal heavy
680        '\u{2547}' => &[
681            LineSegment::horizontal(V4, A, G, ht),
682            LineSegment::vertical(D, V1, V4, ht),
683            LineSegment::vertical(D, V4, V7, lt),
684        ],
685
686        // ╈ Up light and down horizontal heavy
687        '\u{2548}' => &[
688            LineSegment::horizontal(V4, A, G, ht),
689            LineSegment::vertical(D, V1, V4, lt),
690            LineSegment::vertical(D, V4, V7, ht),
691        ],
692
693        // ╉ Right light and left vertical heavy
694        '\u{2549}' => &[
695            LineSegment::horizontal(V4, A, D, ht),
696            LineSegment::horizontal(V4, D, G, lt),
697            LineSegment::vertical(D, V1, V7, ht),
698        ],
699
700        // ╊ Left light and right vertical heavy
701        '\u{254A}' => &[
702            LineSegment::horizontal(V4, A, D, lt),
703            LineSegment::horizontal(V4, D, G, ht),
704            LineSegment::vertical(D, V1, V7, ht),
705        ],
706
707        // ═══════════════════════════════════════════════════════════════════
708        // DOUBLE LINES (two parallel strokes at 1/4 and 3/4)
709        // ═══════════════════════════════════════════════════════════════════
710
711        // ═ Double horizontal
712        '\u{2550}' => &[
713            LineSegment::horizontal(V3, A, G, dt),
714            LineSegment::horizontal(V5, A, G, dt),
715        ],
716
717        // ║ Double vertical
718        '\u{2551}' => &[
719            LineSegment::vertical(C, V1, V7, dt),
720            LineSegment::vertical(E, V1, V7, dt),
721        ],
722
723        // ╔ Double down and right
724        '\u{2554}' => &[
725            LineSegment::horizontal(V3, E, G, dt),
726            LineSegment::horizontal(V5, C, G, dt),
727            LineSegment::vertical(C, V3, V7, dt),
728            LineSegment::vertical(E, V5, V7, dt),
729        ],
730
731        // ╗ Double down and left
732        '\u{2557}' => &[
733            LineSegment::horizontal(V3, A, C, dt),
734            LineSegment::horizontal(V5, A, E, dt),
735            LineSegment::vertical(C, V5, V7, dt),
736            LineSegment::vertical(E, V3, V7, dt),
737        ],
738
739        // ╚ Double up and right
740        '\u{255A}' => &[
741            LineSegment::horizontal(V3, C, G, dt),
742            LineSegment::horizontal(V5, E, G, dt),
743            LineSegment::vertical(C, V1, V3, dt),
744            LineSegment::vertical(E, V1, V5, dt),
745        ],
746
747        // ╝ Double up and left
748        '\u{255D}' => &[
749            LineSegment::horizontal(V3, A, E, dt),
750            LineSegment::horizontal(V5, A, C, dt),
751            LineSegment::vertical(C, V1, V5, dt),
752            LineSegment::vertical(E, V1, V3, dt),
753        ],
754
755        // ╠ Double vertical and right
756        '\u{2560}' => &[
757            LineSegment::vertical(C, V1, V7, dt),
758            LineSegment::vertical(E, V1, V3, dt),
759            LineSegment::vertical(E, V5, V7, dt),
760            LineSegment::horizontal(V3, E, G, dt),
761            LineSegment::horizontal(V5, E, G, dt),
762        ],
763
764        // ╣ Double vertical and left
765        '\u{2563}' => &[
766            LineSegment::vertical(E, V1, V7, dt),
767            LineSegment::vertical(C, V1, V3, dt),
768            LineSegment::vertical(C, V5, V7, dt),
769            LineSegment::horizontal(V3, A, C, dt),
770            LineSegment::horizontal(V5, A, C, dt),
771        ],
772
773        // ╦ Double down and horizontal
774        '\u{2566}' => &[
775            LineSegment::horizontal(V3, A, G, dt),
776            LineSegment::horizontal(V5, A, C, dt),
777            LineSegment::horizontal(V5, E, G, dt),
778            LineSegment::vertical(C, V5, V7, dt),
779            LineSegment::vertical(E, V5, V7, dt),
780        ],
781
782        // ╩ Double up and horizontal
783        '\u{2569}' => &[
784            LineSegment::horizontal(V5, A, G, dt),
785            LineSegment::horizontal(V3, A, C, dt),
786            LineSegment::horizontal(V3, E, G, dt),
787            LineSegment::vertical(C, V1, V3, dt),
788            LineSegment::vertical(E, V1, V3, dt),
789        ],
790
791        // ╬ Double vertical and horizontal
792        '\u{256C}' => &[
793            LineSegment::horizontal(V3, A, C, dt),
794            LineSegment::horizontal(V3, E, G, dt),
795            LineSegment::horizontal(V5, A, C, dt),
796            LineSegment::horizontal(V5, E, G, dt),
797            LineSegment::vertical(C, V1, V3, dt),
798            LineSegment::vertical(C, V5, V7, dt),
799            LineSegment::vertical(E, V1, V3, dt),
800            LineSegment::vertical(E, V5, V7, dt),
801        ],
802
803        // ═══════════════════════════════════════════════════════════════════
804        // MIXED SINGLE/DOUBLE LINES
805        // ═══════════════════════════════════════════════════════════════════
806
807        // ╒ Down single and right double
808        '\u{2552}' => &[
809            LineSegment::horizontal(V3, D, G, dt),
810            LineSegment::horizontal(V5, D, G, dt),
811            LineSegment::vertical(D, V4, V7, lt),
812        ],
813
814        // ╓ Down double and right single
815        '\u{2553}' => &[
816            LineSegment::horizontal(V4, D, G, lt),
817            LineSegment::vertical(C, V4, V7, dt),
818            LineSegment::vertical(E, V4, V7, dt),
819        ],
820
821        // ╕ Down single and left double
822        '\u{2555}' => &[
823            LineSegment::horizontal(V3, A, D, dt),
824            LineSegment::horizontal(V5, A, D, dt),
825            LineSegment::vertical(D, V4, V7, lt),
826        ],
827
828        // ╖ Down double and left single
829        '\u{2556}' => &[
830            LineSegment::horizontal(V4, A, D, lt),
831            LineSegment::vertical(C, V4, V7, dt),
832            LineSegment::vertical(E, V4, V7, dt),
833        ],
834
835        // ╘ Up single and right double
836        '\u{2558}' => &[
837            LineSegment::horizontal(V3, D, G, dt),
838            LineSegment::horizontal(V5, D, G, dt),
839            LineSegment::vertical(D, V1, V4, lt),
840        ],
841
842        // ╙ Up double and right single
843        '\u{2559}' => &[
844            LineSegment::horizontal(V4, D, G, lt),
845            LineSegment::vertical(C, V1, V4, dt),
846            LineSegment::vertical(E, V1, V4, dt),
847        ],
848
849        // ╛ Up single and left double
850        '\u{255B}' => &[
851            LineSegment::horizontal(V3, A, D, dt),
852            LineSegment::horizontal(V5, A, D, dt),
853            LineSegment::vertical(D, V1, V4, lt),
854        ],
855
856        // ╜ Up double and left single
857        '\u{255C}' => &[
858            LineSegment::horizontal(V4, A, D, lt),
859            LineSegment::vertical(C, V1, V4, dt),
860            LineSegment::vertical(E, V1, V4, dt),
861        ],
862
863        // ╞ Vertical single and right double
864        '\u{255E}' => &[
865            LineSegment::vertical(D, V1, V7, lt),
866            LineSegment::horizontal(V3, D, G, dt),
867            LineSegment::horizontal(V5, D, G, dt),
868        ],
869
870        // ╟ Vertical double and right single
871        '\u{255F}' => &[
872            LineSegment::vertical(C, V1, V7, dt),
873            LineSegment::vertical(E, V1, V7, dt),
874            LineSegment::horizontal(V4, E, G, lt),
875        ],
876
877        // ╡ Vertical single and left double
878        '\u{2561}' => &[
879            LineSegment::vertical(D, V1, V7, lt),
880            LineSegment::horizontal(V3, A, D, dt),
881            LineSegment::horizontal(V5, A, D, dt),
882        ],
883
884        // ╢ Vertical double and left single
885        '\u{2562}' => &[
886            LineSegment::vertical(C, V1, V7, dt),
887            LineSegment::vertical(E, V1, V7, dt),
888            LineSegment::horizontal(V4, A, C, lt),
889        ],
890
891        // ╤ Down single and horizontal double
892        '\u{2564}' => &[
893            LineSegment::horizontal(V3, A, G, dt),
894            LineSegment::horizontal(V5, A, G, dt),
895            LineSegment::vertical(D, V5, V7, lt),
896        ],
897
898        // ╥ Down double and horizontal single
899        '\u{2565}' => &[
900            LineSegment::horizontal(V4, A, G, lt),
901            LineSegment::vertical(C, V4, V7, dt),
902            LineSegment::vertical(E, V4, V7, dt),
903        ],
904
905        // ╧ Up single and horizontal double
906        '\u{2567}' => &[
907            LineSegment::horizontal(V3, A, G, dt),
908            LineSegment::horizontal(V5, A, G, dt),
909            LineSegment::vertical(D, V1, V3, lt),
910        ],
911
912        // ╨ Up double and horizontal single
913        '\u{2568}' => &[
914            LineSegment::horizontal(V4, A, G, lt),
915            LineSegment::vertical(C, V1, V4, dt),
916            LineSegment::vertical(E, V1, V4, dt),
917        ],
918
919        // ╪ Vertical single and horizontal double
920        '\u{256A}' => &[
921            LineSegment::horizontal(V3, A, G, dt),
922            LineSegment::horizontal(V5, A, G, dt),
923            LineSegment::vertical(D, V1, V7, lt),
924        ],
925
926        // ╫ Vertical double and horizontal single
927        '\u{256B}' => &[
928            LineSegment::vertical(C, V1, V7, dt),
929            LineSegment::vertical(E, V1, V7, dt),
930            LineSegment::horizontal(V4, A, G, lt),
931        ],
932
933        // ═══════════════════════════════════════════════════════════════════
934        // DASHED AND DOTTED LINES
935        // ═══════════════════════════════════════════════════════════════════
936
937        // ┄ Light triple dash horizontal
938        '\u{2504}' => &[LineSegment::horizontal(V4, A, G, lt)],
939
940        // ┅ Heavy triple dash horizontal
941        '\u{2505}' => &[LineSegment::horizontal(V4, A, G, ht)],
942
943        // ┆ Light triple dash vertical
944        '\u{2506}' => &[LineSegment::vertical(D, V1, V7, lt)],
945
946        // ┇ Heavy triple dash vertical
947        '\u{2507}' => &[LineSegment::vertical(D, V1, V7, ht)],
948
949        // ┈ Light quadruple dash horizontal
950        '\u{2508}' => &[LineSegment::horizontal(V4, A, G, lt)],
951
952        // ┉ Heavy quadruple dash horizontal
953        '\u{2509}' => &[LineSegment::horizontal(V4, A, G, ht)],
954
955        // ┊ Light quadruple dash vertical
956        '\u{250A}' => &[LineSegment::vertical(D, V1, V7, lt)],
957
958        // ┋ Heavy quadruple dash vertical
959        '\u{250B}' => &[LineSegment::vertical(D, V1, V7, ht)],
960
961        // ═══════════════════════════════════════════════════════════════════
962        // ROUNDED CORNERS (rendered as sharp corners for now)
963        // ═══════════════════════════════════════════════════════════════════
964
965        // ╭ Light arc down and right
966        '\u{256D}' => &[
967            LineSegment::horizontal(V4, D, G, lt),
968            LineSegment::vertical(D, V4, V7, lt),
969        ],
970
971        // ╮ Light arc down and left
972        '\u{256E}' => &[
973            LineSegment::horizontal(V4, A, D, lt),
974            LineSegment::vertical(D, V4, V7, lt),
975        ],
976
977        // ╯ Light arc up and left
978        '\u{256F}' => &[
979            LineSegment::horizontal(V4, A, D, lt),
980            LineSegment::vertical(D, V1, V4, lt),
981        ],
982
983        // ╰ Light arc up and right
984        '\u{2570}' => &[
985            LineSegment::horizontal(V4, D, G, lt),
986            LineSegment::vertical(D, V1, V4, lt),
987        ],
988
989        // ═══════════════════════════════════════════════════════════════════
990        // DIAGONAL LINES
991        // ═══════════════════════════════════════════════════════════════════
992
993        // ╱ Light diagonal upper right to lower left
994        '\u{2571}' => &[LineSegment::new(G, V1, A, V7, lt)],
995
996        // ╲ Light diagonal upper left to lower right
997        '\u{2572}' => &[LineSegment::new(A, V1, G, V7, lt)],
998
999        // ╳ Light diagonal cross
1000        '\u{2573}' => &[
1001            LineSegment::new(A, V1, G, V7, lt),
1002            LineSegment::new(G, V1, A, V7, lt),
1003        ],
1004
1005        // ═══════════════════════════════════════════════════════════════════
1006        // HALF LINES
1007        // ═══════════════════════════════════════════════════════════════════
1008
1009        // ╴ Light left
1010        '\u{2574}' => &[LineSegment::horizontal(V4, A, D, lt)],
1011
1012        // ╵ Light up
1013        '\u{2575}' => &[LineSegment::vertical(D, V1, V4, lt)],
1014
1015        // ╶ Light right
1016        '\u{2576}' => &[LineSegment::horizontal(V4, D, G, lt)],
1017
1018        // ╷ Light down
1019        '\u{2577}' => &[LineSegment::vertical(D, V4, V7, lt)],
1020
1021        // ╸ Heavy left
1022        '\u{2578}' => &[LineSegment::horizontal(V4, A, D, ht)],
1023
1024        // ╹ Heavy up
1025        '\u{2579}' => &[LineSegment::vertical(D, V1, V4, ht)],
1026
1027        // ╺ Heavy right
1028        '\u{257A}' => &[LineSegment::horizontal(V4, D, G, ht)],
1029
1030        // ╻ Heavy down
1031        '\u{257B}' => &[LineSegment::vertical(D, V4, V7, ht)],
1032
1033        // ╼ Light left and heavy right
1034        '\u{257C}' => &[
1035            LineSegment::horizontal(V4, A, D, lt),
1036            LineSegment::horizontal(V4, D, G, ht),
1037        ],
1038
1039        // ╽ Light up and heavy down
1040        '\u{257D}' => &[
1041            LineSegment::vertical(D, V1, V4, lt),
1042            LineSegment::vertical(D, V4, V7, ht),
1043        ],
1044
1045        // ╾ Heavy left and light right
1046        '\u{257E}' => &[
1047            LineSegment::horizontal(V4, A, D, ht),
1048            LineSegment::horizontal(V4, D, G, lt),
1049        ],
1050
1051        // ╿ Heavy up and light down
1052        '\u{257F}' => &[
1053            LineSegment::vertical(D, V1, V4, ht),
1054            LineSegment::vertical(D, V4, V7, lt),
1055        ],
1056
1057        _ => return None,
1058    };
1059
1060    if lines.is_empty() {
1061        None
1062    } else {
1063        Some(BoxDrawingGeometry::from_lines(lines, aspect_ratio))
1064    }
1065}
1066
1067/// Represents a geometric block that can be rendered as a colored rectangle
1068#[derive(Debug, Clone, Copy)]
1069pub struct GeometricBlock {
1070    /// Normalized X position within cell (0.0 = left edge, 1.0 = right edge)
1071    pub x: f32,
1072    /// Normalized Y position within cell (0.0 = top edge, 1.0 = bottom edge)
1073    pub y: f32,
1074    /// Normalized width within cell (0.0 to 1.0)
1075    pub width: f32,
1076    /// Normalized height within cell (0.0 to 1.0)
1077    pub height: f32,
1078}
1079
1080impl GeometricBlock {
1081    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
1082        Self {
1083            x,
1084            y,
1085            width,
1086            height,
1087        }
1088    }
1089
1090    /// Full cell block
1091    pub const fn full() -> Self {
1092        Self::new(0.0, 0.0, 1.0, 1.0)
1093    }
1094
1095    /// Convert to pixel coordinates given cell bounds
1096    pub fn to_pixel_rect(self, cell_x: f32, cell_y: f32, cell_w: f32, cell_h: f32) -> PixelRect {
1097        PixelRect {
1098            x: cell_x + self.x * cell_w,
1099            y: cell_y + self.y * cell_h,
1100            width: self.width * cell_w,
1101            height: self.height * cell_h,
1102        }
1103    }
1104}
1105
1106/// Pixel rectangle for rendering
1107#[derive(Debug, Clone, Copy)]
1108pub struct PixelRect {
1109    pub x: f32,
1110    pub y: f32,
1111    pub width: f32,
1112    pub height: f32,
1113}
1114
1115/// Get the geometric representation of a block element character
1116/// Returns None if the character should use font rendering
1117pub fn get_geometric_block(ch: char) -> Option<GeometricBlock> {
1118    match ch {
1119        // Full block
1120        '\u{2588}' => Some(GeometricBlock::full()),
1121
1122        // Upper half block
1123        '\u{2580}' => Some(GeometricBlock::new(0.0, 0.0, 1.0, 0.5)),
1124
1125        // Lower one eighth block to lower seven eighths block
1126        '\u{2581}' => Some(GeometricBlock::new(0.0, 0.875, 1.0, 0.125)),
1127        '\u{2582}' => Some(GeometricBlock::new(0.0, 0.75, 1.0, 0.25)),
1128        '\u{2583}' => Some(GeometricBlock::new(0.0, 0.625, 1.0, 0.375)),
1129        '\u{2584}' => Some(GeometricBlock::new(0.0, 0.5, 1.0, 0.5)), // Lower half
1130        '\u{2585}' => Some(GeometricBlock::new(0.0, 0.375, 1.0, 0.625)),
1131        '\u{2586}' => Some(GeometricBlock::new(0.0, 0.25, 1.0, 0.75)),
1132        '\u{2587}' => Some(GeometricBlock::new(0.0, 0.125, 1.0, 0.875)),
1133
1134        // Left blocks (one eighth to seven eighths)
1135        '\u{2589}' => Some(GeometricBlock::new(0.0, 0.0, 0.875, 1.0)),
1136        '\u{258A}' => Some(GeometricBlock::new(0.0, 0.0, 0.75, 1.0)),
1137        '\u{258B}' => Some(GeometricBlock::new(0.0, 0.0, 0.625, 1.0)),
1138        '\u{258C}' => Some(GeometricBlock::new(0.0, 0.0, 0.5, 1.0)), // Left half
1139        '\u{258D}' => Some(GeometricBlock::new(0.0, 0.0, 0.375, 1.0)),
1140        '\u{258E}' => Some(GeometricBlock::new(0.0, 0.0, 0.25, 1.0)),
1141        '\u{258F}' => Some(GeometricBlock::new(0.0, 0.0, 0.125, 1.0)),
1142
1143        // Right half block
1144        '\u{2590}' => Some(GeometricBlock::new(0.5, 0.0, 0.5, 1.0)),
1145
1146        // Upper one eighth block
1147        '\u{2594}' => Some(GeometricBlock::new(0.0, 0.0, 1.0, 0.125)),
1148
1149        // Right one eighth block
1150        '\u{2595}' => Some(GeometricBlock::new(0.875, 0.0, 0.125, 1.0)),
1151
1152        // Quadrant blocks
1153        '\u{2596}' => Some(GeometricBlock::new(0.0, 0.5, 0.5, 0.5)), // Lower left
1154        '\u{2597}' => Some(GeometricBlock::new(0.5, 0.5, 0.5, 0.5)), // Lower right
1155        '\u{2598}' => Some(GeometricBlock::new(0.0, 0.0, 0.5, 0.5)), // Upper left
1156        '\u{259D}' => Some(GeometricBlock::new(0.5, 0.0, 0.5, 0.5)), // Upper right
1157
1158        // Combined quadrants - these need multiple rectangles, handled separately
1159        // For now, return None to use font rendering with snapping
1160        '\u{2599}'..='\u{259C}' | '\u{259E}' | '\u{259F}' => None,
1161
1162        _ => None,
1163    }
1164}
1165
1166/// Get pixel-perfect rectangle for geometric shape characters (U+25A0–U+25FF).
1167///
1168/// Unlike block elements which fill the cell, geometric shapes like squares
1169/// preserve their aspect ratio by using `cell_w` as the base dimension and
1170/// centering vertically within the cell. Returns `None` for outline/hollow
1171/// shapes, circles, triangles, and other characters that can't be represented
1172/// as simple filled rectangles — those fall through to font rendering.
1173pub fn get_geometric_shape_rect(
1174    ch: char,
1175    cell_x: f32,
1176    cell_y: f32,
1177    cell_w: f32,
1178    cell_h: f32,
1179) -> Option<PixelRect> {
1180    match ch {
1181        // ■ U+25A0 BLACK SQUARE — full cell width square
1182        '\u{25A0}' => {
1183            let size = cell_w;
1184            Some(PixelRect {
1185                x: cell_x,
1186                y: cell_y + (cell_h - size) / 2.0,
1187                width: size,
1188                height: size,
1189            })
1190        }
1191        // ▪ U+25AA BLACK SMALL SQUARE — 0.5× cell width
1192        '\u{25AA}' => {
1193            let size = cell_w * 0.5;
1194            Some(PixelRect {
1195                x: cell_x + (cell_w - size) / 2.0,
1196                y: cell_y + (cell_h - size) / 2.0,
1197                width: size,
1198                height: size,
1199            })
1200        }
1201        // ▬ U+25AC BLACK RECTANGLE — horizontal rectangle, full width, 1/3 height
1202        '\u{25AC}' => {
1203            let h = cell_h * 0.33;
1204            Some(PixelRect {
1205                x: cell_x,
1206                y: cell_y + (cell_h - h) / 2.0,
1207                width: cell_w,
1208                height: h,
1209            })
1210        }
1211        // ▮ U+25AE BLACK VERTICAL RECTANGLE — half width, full height
1212        '\u{25AE}' => {
1213            let w = cell_w * 0.5;
1214            Some(PixelRect {
1215                x: cell_x + (cell_w - w) / 2.0,
1216                y: cell_y,
1217                width: w,
1218                height: cell_h,
1219            })
1220        }
1221        // ◼ U+25FC BLACK MEDIUM SQUARE — 0.75× cell width
1222        '\u{25FC}' => {
1223            let size = cell_w * 0.75;
1224            Some(PixelRect {
1225                x: cell_x + (cell_w - size) / 2.0,
1226                y: cell_y + (cell_h - size) / 2.0,
1227                width: size,
1228                height: size,
1229            })
1230        }
1231        // ◾ U+25FE BLACK MEDIUM SMALL SQUARE — 0.625× cell width
1232        '\u{25FE}' => {
1233            let size = cell_w * 0.625;
1234            Some(PixelRect {
1235                x: cell_x + (cell_w - size) / 2.0,
1236                y: cell_y + (cell_h - size) / 2.0,
1237                width: size,
1238                height: size,
1239            })
1240        }
1241        _ => None,
1242    }
1243}
1244
1245/// Calculate snapped glyph bounds for block characters
1246///
1247/// This function adjusts glyph position and size to align with cell boundaries,
1248/// preventing gaps between adjacent block characters.
1249///
1250/// # Arguments
1251/// * `glyph_left` - Original glyph left position in pixels
1252/// * `glyph_top` - Original glyph top position in pixels
1253/// * `render_w` - Original glyph render width in pixels
1254/// * `render_h` - Original glyph render height in pixels
1255/// * `cell_x0` - Cell left boundary in pixels
1256/// * `cell_y0` - Cell top boundary in pixels
1257/// * `cell_x1` - Cell right boundary in pixels
1258/// * `cell_y1` - Cell bottom boundary in pixels
1259/// * `snap_threshold` - Distance in pixels to consider "close enough" to snap
1260/// * `extension` - Amount to extend beyond boundaries to prevent gaps
1261///
1262/// # Returns
1263/// Tuple of (new_left, new_top, new_width, new_height)
1264#[allow(clippy::too_many_arguments)]
1265pub fn snap_glyph_to_cell(
1266    glyph_left: f32,
1267    glyph_top: f32,
1268    render_w: f32,
1269    render_h: f32,
1270    cell_x0: f32,
1271    cell_y0: f32,
1272    cell_x1: f32,
1273    cell_y1: f32,
1274    snap_threshold: f32,
1275    extension: f32,
1276) -> (f32, f32, f32, f32) {
1277    let mut new_left = glyph_left;
1278    let mut new_top = glyph_top;
1279    let mut new_w = render_w;
1280    let mut new_h = render_h;
1281
1282    let glyph_right = glyph_left + render_w;
1283    let glyph_bottom = glyph_top + render_h;
1284
1285    // Snap left edge
1286    if (glyph_left - cell_x0).abs() < snap_threshold {
1287        new_left = cell_x0 - extension;
1288        new_w = glyph_right - new_left;
1289    }
1290
1291    // Snap right edge
1292    if (glyph_right - cell_x1).abs() < snap_threshold {
1293        new_w = cell_x1 + extension - new_left;
1294    }
1295
1296    // Snap top edge
1297    if (glyph_top - cell_y0).abs() < snap_threshold {
1298        new_top = cell_y0 - extension;
1299        new_h = glyph_bottom - new_top;
1300    }
1301
1302    // Snap bottom edge
1303    if (glyph_bottom - cell_y1).abs() < snap_threshold {
1304        new_h = cell_y1 + extension - new_top;
1305    }
1306
1307    // Also snap to middle boundaries for half-block characters
1308    let cell_cx = (cell_x0 + cell_x1) / 2.0;
1309    let cell_cy = (cell_y0 + cell_y1) / 2.0;
1310
1311    // Vertical middle snap
1312    if (glyph_bottom - cell_cy).abs() < snap_threshold {
1313        new_h = cell_cy - new_top;
1314    } else if (glyph_top - cell_cy).abs() < snap_threshold {
1315        let bottom = new_top + new_h;
1316        new_top = cell_cy;
1317        new_h = bottom - new_top;
1318    }
1319
1320    // Horizontal middle snap
1321    if (glyph_right - cell_cx).abs() < snap_threshold {
1322        new_w = cell_cx - new_left;
1323    } else if (glyph_left - cell_cx).abs() < snap_threshold {
1324        let right = new_left + new_w;
1325        new_left = cell_cx;
1326        new_w = right - new_left;
1327    }
1328
1329    (new_left, new_top, new_w, new_h)
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334    use super::*;
1335
1336    #[test]
1337    fn test_classify_box_drawing() {
1338        // Horizontal lines
1339        assert_eq!(classify_char('─'), BlockCharType::BoxDrawing);
1340        assert_eq!(classify_char('━'), BlockCharType::BoxDrawing);
1341        assert_eq!(classify_char('═'), BlockCharType::BoxDrawing);
1342
1343        // Vertical lines
1344        assert_eq!(classify_char('│'), BlockCharType::BoxDrawing);
1345        assert_eq!(classify_char('┃'), BlockCharType::BoxDrawing);
1346        assert_eq!(classify_char('║'), BlockCharType::BoxDrawing);
1347
1348        // Corners
1349        assert_eq!(classify_char('┌'), BlockCharType::BoxDrawing);
1350        assert_eq!(classify_char('┐'), BlockCharType::BoxDrawing);
1351        assert_eq!(classify_char('└'), BlockCharType::BoxDrawing);
1352        assert_eq!(classify_char('┘'), BlockCharType::BoxDrawing);
1353
1354        // Double line corners
1355        assert_eq!(classify_char('╔'), BlockCharType::BoxDrawing);
1356        assert_eq!(classify_char('╗'), BlockCharType::BoxDrawing);
1357        assert_eq!(classify_char('╚'), BlockCharType::BoxDrawing);
1358        assert_eq!(classify_char('╝'), BlockCharType::BoxDrawing);
1359    }
1360
1361    #[test]
1362    fn test_classify_block_elements() {
1363        // Full block
1364        assert_eq!(classify_char('█'), BlockCharType::SolidBlock);
1365
1366        // Half blocks
1367        assert_eq!(classify_char('▀'), BlockCharType::PartialBlock);
1368        assert_eq!(classify_char('▄'), BlockCharType::PartialBlock);
1369        assert_eq!(classify_char('▌'), BlockCharType::PartialBlock);
1370        assert_eq!(classify_char('▐'), BlockCharType::PartialBlock);
1371
1372        // Shade characters
1373        assert_eq!(classify_char('░'), BlockCharType::Shade);
1374        assert_eq!(classify_char('▒'), BlockCharType::Shade);
1375        assert_eq!(classify_char('▓'), BlockCharType::Shade);
1376
1377        // Quadrants
1378        assert_eq!(classify_char('▖'), BlockCharType::PartialBlock);
1379        assert_eq!(classify_char('▗'), BlockCharType::PartialBlock);
1380        assert_eq!(classify_char('▘'), BlockCharType::PartialBlock);
1381        assert_eq!(classify_char('▝'), BlockCharType::PartialBlock);
1382    }
1383
1384    #[test]
1385    fn test_classify_geometric_shapes() {
1386        assert_eq!(classify_char('■'), BlockCharType::Geometric);
1387        assert_eq!(classify_char('□'), BlockCharType::Geometric);
1388        assert_eq!(classify_char('▪'), BlockCharType::Geometric);
1389        assert_eq!(classify_char('▫'), BlockCharType::Geometric);
1390    }
1391
1392    #[test]
1393    fn test_classify_regular_chars() {
1394        assert_eq!(classify_char('a'), BlockCharType::None);
1395        assert_eq!(classify_char('Z'), BlockCharType::None);
1396        assert_eq!(classify_char('0'), BlockCharType::None);
1397        assert_eq!(classify_char(' '), BlockCharType::None);
1398        assert_eq!(classify_char('!'), BlockCharType::None);
1399    }
1400
1401    #[test]
1402    fn test_should_snap_to_boundaries() {
1403        assert!(should_snap_to_boundaries(BlockCharType::BoxDrawing));
1404        assert!(should_snap_to_boundaries(BlockCharType::SolidBlock));
1405        assert!(should_snap_to_boundaries(BlockCharType::PartialBlock));
1406        assert!(should_snap_to_boundaries(BlockCharType::Geometric));
1407        assert!(should_snap_to_boundaries(BlockCharType::Powerline));
1408
1409        assert!(!should_snap_to_boundaries(BlockCharType::None));
1410        assert!(!should_snap_to_boundaries(BlockCharType::Shade));
1411        assert!(!should_snap_to_boundaries(BlockCharType::Braille));
1412    }
1413
1414    #[test]
1415    fn test_should_render_geometrically() {
1416        assert!(should_render_geometrically(BlockCharType::SolidBlock));
1417        assert!(should_render_geometrically(BlockCharType::PartialBlock));
1418        assert!(should_render_geometrically(BlockCharType::BoxDrawing));
1419        assert!(should_render_geometrically(BlockCharType::Geometric));
1420
1421        assert!(!should_render_geometrically(BlockCharType::None));
1422        assert!(!should_render_geometrically(BlockCharType::Shade));
1423        assert!(!should_render_geometrically(BlockCharType::Powerline));
1424        assert!(!should_render_geometrically(BlockCharType::Braille));
1425    }
1426
1427    #[test]
1428    fn test_geometric_block_full() {
1429        let block = get_geometric_block('█').unwrap();
1430        assert_eq!(block.x, 0.0);
1431        assert_eq!(block.y, 0.0);
1432        assert_eq!(block.width, 1.0);
1433        assert_eq!(block.height, 1.0);
1434    }
1435
1436    #[test]
1437    fn test_geometric_block_halves() {
1438        // Upper half
1439        let block = get_geometric_block('▀').unwrap();
1440        assert_eq!(block.x, 0.0);
1441        assert_eq!(block.y, 0.0);
1442        assert_eq!(block.width, 1.0);
1443        assert_eq!(block.height, 0.5);
1444
1445        // Lower half
1446        let block = get_geometric_block('▄').unwrap();
1447        assert_eq!(block.x, 0.0);
1448        assert_eq!(block.y, 0.5);
1449        assert_eq!(block.width, 1.0);
1450        assert_eq!(block.height, 0.5);
1451
1452        // Left half
1453        let block = get_geometric_block('▌').unwrap();
1454        assert_eq!(block.x, 0.0);
1455        assert_eq!(block.y, 0.0);
1456        assert_eq!(block.width, 0.5);
1457        assert_eq!(block.height, 1.0);
1458
1459        // Right half
1460        let block = get_geometric_block('▐').unwrap();
1461        assert_eq!(block.x, 0.5);
1462        assert_eq!(block.y, 0.0);
1463        assert_eq!(block.width, 0.5);
1464        assert_eq!(block.height, 1.0);
1465    }
1466
1467    #[test]
1468    fn test_geometric_block_quadrants() {
1469        // Lower left quadrant
1470        let block = get_geometric_block('▖').unwrap();
1471        assert_eq!(block.x, 0.0);
1472        assert_eq!(block.y, 0.5);
1473        assert_eq!(block.width, 0.5);
1474        assert_eq!(block.height, 0.5);
1475
1476        // Lower right quadrant
1477        let block = get_geometric_block('▗').unwrap();
1478        assert_eq!(block.x, 0.5);
1479        assert_eq!(block.y, 0.5);
1480        assert_eq!(block.width, 0.5);
1481        assert_eq!(block.height, 0.5);
1482
1483        // Upper left quadrant
1484        let block = get_geometric_block('▘').unwrap();
1485        assert_eq!(block.x, 0.0);
1486        assert_eq!(block.y, 0.0);
1487        assert_eq!(block.width, 0.5);
1488        assert_eq!(block.height, 0.5);
1489
1490        // Upper right quadrant
1491        let block = get_geometric_block('▝').unwrap();
1492        assert_eq!(block.x, 0.5);
1493        assert_eq!(block.y, 0.0);
1494        assert_eq!(block.width, 0.5);
1495        assert_eq!(block.height, 0.5);
1496    }
1497
1498    #[test]
1499    fn test_geometric_block_to_pixel_rect() {
1500        let block = GeometricBlock::new(0.0, 0.5, 1.0, 0.5); // Lower half
1501        let rect = block.to_pixel_rect(10.0, 20.0, 8.0, 16.0);
1502
1503        assert_eq!(rect.x, 10.0);
1504        assert_eq!(rect.y, 28.0); // 20.0 + 0.5 * 16.0
1505        assert_eq!(rect.width, 8.0);
1506        assert_eq!(rect.height, 8.0);
1507    }
1508
1509    #[test]
1510    fn test_box_drawing_light_horizontal() {
1511        let geo = get_box_drawing_geometry('─', 2.0).unwrap();
1512        assert_eq!(geo.segments.len(), 1);
1513        let seg = &geo.segments[0];
1514        assert_eq!(seg.x, 0.0);
1515        assert!(seg.width > 0.99); // Full width
1516    }
1517
1518    #[test]
1519    fn test_box_drawing_light_corner() {
1520        let geo = get_box_drawing_geometry('┌', 2.0).unwrap();
1521        assert_eq!(geo.segments.len(), 2);
1522        // Should have horizontal and vertical segments meeting at center
1523    }
1524
1525    #[test]
1526    fn test_box_drawing_double_lines() {
1527        let geo = get_box_drawing_geometry('═', 2.0).unwrap();
1528        assert_eq!(geo.segments.len(), 2);
1529        // Two parallel horizontal lines
1530    }
1531
1532    #[test]
1533    fn test_snap_glyph_to_cell_basic() {
1534        // Glyph that's close to cell boundaries should snap
1535        let (left, top, w, h) = snap_glyph_to_cell(
1536            10.5, 20.5, // glyph position (slightly off from cell)
1537            7.8, 15.8, // glyph size (slightly smaller than cell)
1538            10.0, 20.0, // cell top-left
1539            18.0, 36.0, // cell bottom-right
1540            3.0,  // snap threshold
1541            0.5,  // extension
1542        );
1543
1544        // Should snap left to cell boundary minus extension
1545        assert!((left - 9.5).abs() < 0.01);
1546        // Should snap top to cell boundary minus extension
1547        assert!((top - 19.5).abs() < 0.01);
1548        // Width should extend to right cell boundary plus extension
1549        assert!((left + w - 18.5).abs() < 0.01);
1550        // Height should extend to bottom cell boundary plus extension
1551        assert!((top + h - 36.5).abs() < 0.01);
1552    }
1553
1554    #[test]
1555    fn test_snap_glyph_no_snap_when_far() {
1556        // Glyph that's far from cell boundaries and midpoints should not snap
1557        // Cell: x=[10, 20], y=[20, 40], middle_x=15, middle_y=30
1558        // Glyph: x=[12, 17], y=[24, 34] - all edges >2 pixels from boundaries/midpoints
1559        let (left, top, w, h) = snap_glyph_to_cell(
1560            12.0, 24.0, // glyph position (away from edges and midpoints)
1561            5.0, 10.0, // glyph size (ends at x=17, y=34)
1562            10.0, 20.0, // cell top-left
1563            20.0, 40.0, // cell bottom-right (midpoints at 15, 30)
1564            1.5,  // snap threshold (narrow)
1565            0.5,  // extension
1566        );
1567
1568        // Should not change anything since glyph is far from boundaries
1569        assert_eq!(left, 12.0);
1570        assert_eq!(top, 24.0);
1571        assert_eq!(w, 5.0);
1572        assert_eq!(h, 10.0);
1573    }
1574
1575    #[test]
1576    fn test_snap_glyph_middle_snap() {
1577        // Test snapping to middle boundaries (for half-block characters)
1578        let (_left, top, _w, h) = snap_glyph_to_cell(
1579            10.0, 20.0, // glyph at cell corner
1580            8.0, 9.8, // glyph ends near vertical middle (30 - 0.2 = 29.8)
1581            10.0, 20.0, // cell top-left
1582            18.0, 40.0, // cell bottom-right (middle at y=30)
1583            1.0,  // snap threshold
1584            0.0,  // no extension for this test
1585        );
1586
1587        // Height should snap to middle
1588        assert!((top + h - 30.0).abs() < 0.01);
1589    }
1590
1591    #[test]
1592    fn test_geometric_shape_rect_black_square() {
1593        // ■ U+25A0 — full cell_w square, centered vertically
1594        let rect = get_geometric_shape_rect('\u{25A0}', 10.0, 20.0, 8.0, 16.0).unwrap();
1595        assert_eq!(rect.x, 10.0);
1596        assert_eq!(rect.y, 24.0); // 20 + (16 - 8) / 2
1597        assert_eq!(rect.width, 8.0);
1598        assert_eq!(rect.height, 8.0);
1599    }
1600
1601    #[test]
1602    fn test_geometric_shape_rect_medium_square() {
1603        // ◼ U+25FC — 0.75× cell_w square, centered
1604        let rect = get_geometric_shape_rect('\u{25FC}', 10.0, 20.0, 8.0, 16.0).unwrap();
1605        let size = 8.0 * 0.75; // 6.0
1606        assert_eq!(rect.x, 10.0 + (8.0 - size) / 2.0);
1607        assert_eq!(rect.y, 20.0 + (16.0 - size) / 2.0);
1608        assert_eq!(rect.width, size);
1609        assert_eq!(rect.height, size);
1610    }
1611
1612    #[test]
1613    fn test_geometric_shape_rect_small_square() {
1614        // ▪ U+25AA — 0.5× cell_w square, centered
1615        let rect = get_geometric_shape_rect('\u{25AA}', 10.0, 20.0, 8.0, 16.0).unwrap();
1616        let size = 8.0 * 0.5; // 4.0
1617        assert_eq!(rect.x, 10.0 + (8.0 - size) / 2.0);
1618        assert_eq!(rect.y, 20.0 + (16.0 - size) / 2.0);
1619        assert_eq!(rect.width, size);
1620        assert_eq!(rect.height, size);
1621    }
1622
1623    #[test]
1624    fn test_geometric_shape_rect_rectangle() {
1625        // ▬ U+25AC — horizontal rectangle, full width, 0.33 height
1626        let rect = get_geometric_shape_rect('\u{25AC}', 10.0, 20.0, 8.0, 16.0).unwrap();
1627        let h = 16.0 * 0.33;
1628        assert_eq!(rect.x, 10.0);
1629        assert!((rect.y - (20.0 + (16.0 - h) / 2.0)).abs() < 0.01);
1630        assert_eq!(rect.width, 8.0);
1631        assert!((rect.height - h).abs() < 0.01);
1632    }
1633
1634    #[test]
1635    fn test_geometric_shape_rect_vertical_rectangle() {
1636        // ▮ U+25AE — vertical rectangle, 0.5 width, full height
1637        let rect = get_geometric_shape_rect('\u{25AE}', 10.0, 20.0, 8.0, 16.0).unwrap();
1638        let w = 8.0 * 0.5;
1639        assert_eq!(rect.x, 10.0 + (8.0 - w) / 2.0);
1640        assert_eq!(rect.y, 20.0);
1641        assert_eq!(rect.width, w);
1642        assert_eq!(rect.height, 16.0);
1643    }
1644
1645    #[test]
1646    fn test_geometric_shape_rect_outline_returns_none() {
1647        // Outline/hollow shapes should return None (use font rendering)
1648        assert!(get_geometric_shape_rect('\u{25A1}', 0.0, 0.0, 8.0, 16.0).is_none()); // □
1649        assert!(get_geometric_shape_rect('\u{25AB}', 0.0, 0.0, 8.0, 16.0).is_none()); // ▫
1650        assert!(get_geometric_shape_rect('\u{25FB}', 0.0, 0.0, 8.0, 16.0).is_none()); // ◻
1651        assert!(get_geometric_shape_rect('\u{25FD}', 0.0, 0.0, 8.0, 16.0).is_none()); // ◽
1652    }
1653}