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