quadraui 0.6.1

Cross-platform Rust UI toolkit: write one AppLogic and run it unmodified on TUI, GTK4 (Linux), macOS, and Windows backends
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
//! TUI rasteriser for [`crate::Minimap`]: braille density view.
//!
//! Each terminal cell packs a `2`-dot-wide x `4`-dot-tall braille glyph
//! (`4` buffer lines per row — [`LINES_PER_ROW`] — and a horizontal
//! buffer-columns-per-cell scale that [`draw_minimap`] resolves adaptively
//! from the buffer's own widest line, not a fixed constant — see
//! [`resolve_cols_per_cell`], issue #1032), lifting the bit-packing itself
//! from [`super::braille`] rather than a second copy (see that module's
//! docs for why one copy matters). The dot rule: a dot's *coverage
//! fraction* —
//! how much of its source-column bucket is non-whitespace — is
//! thresholded through [`super::braille::dither_threshold_met`]'s ordered
//! dither, not a plain boolean OR. A boolean OR over a
//! bucket wider than one column saturates: as soon as *any* column in the
//! bucket is non-whitespace the dot sets, so practically every dot from
//! the end of a line's indent onward lights up and every row runs to the
//! strip's right edge, destroying the one signal — line length — that
//! makes a minimap read as a thumbnail of the code rather than a solid
//! bar. Dithering spreads that decision across many dots' worth of
//! threshold instead, so a sparsely-covered bucket only lights a few dot
//! positions and a densely-covered one lights (almost) all of them —
//! producing the ragged, length-tracking right edge VS Code's minimap has.
//!
//! Colour is one foreground per *cell*, read from
//! [`crate::Minimap::syntax_spans`] — already aggregated to this exact
//! cell granularity by [`crate::aggregate_spans`], so this rasteriser
//! never re-reduces colour data itself. The viewport highlight is a
//! **background** band across the highlighted rows — the previous
//! design's `█`/`▄`/`▌` overlay would have destroyed the dot content it
//! sat on, since braille has already spent its one foreground slot on
//! syntax colour.

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;

use super::braille::{dither_threshold_met, pack_braille_cell};
use super::{ratatui_color, set_cell};
use crate::primitives::minimap::{Minimap, MinimapLayout, MinimapSizing};
use crate::theme::Theme;

/// Buffer lines packed into one terminal row's braille dots.
pub const LINES_PER_ROW: usize = 4;
/// The narrowest (crispest) buffer-columns-per-cell scale — one braille
/// cell (2 dot columns) folds this many source columns into its colour +
/// dot content at the tightest resolution the rasteriser ever uses, so an
/// `N`-cell strip represents at least `N * COLS_PER_CELL` source columns.
///
/// [`draw_minimap_with_scale`] accepts any `cols_per_cell` explicitly, for
/// a host that wants to override the adaptive default. Whichever scale is
/// used, a host aggregating its own [`crate::MinimapSpan`]s must build its
/// [`crate::MinimapGrid::cols_per_cell`] from the **same** value — read back
/// via [`MinimapLayout::cols_per_cell`] rather than hardcoding this constant,
/// since [`crate::aggregate_spans`] and this rasteriser are only ever kept in
/// sync by that read-back convention, not by a shared type.
///
/// An **odd** `cols_per_cell` is rounded up to the nearest even effective
/// width internally (see [`cols_per_dot`]) so that the dot rasteriser and
/// the colour lookup always agree on exactly which source columns a cell
/// covers — passing an odd value still works, it just covers one extra
/// source column per cell rather than silently desyncing dots from
/// colour. [`resolve_cols_per_cell`] only ever returns an even
/// value, so this only matters for a caller of
/// [`draw_minimap_with_scale`] passing their own odd scale.
pub const COLS_PER_CELL: usize = 2;

/// Compute the TUI cell-unit layout for a [`Minimap`] without painting.
///
/// TUI uses [`MinimapSizing::FixedPitch`] at exactly `1.0` cell row per
/// minimap row, so `draw_minimap`'s paint loop repeats each row across
/// exactly one cell row — the layout's row pitch and the rasteriser's
/// paint granularity agree. Under the previous [`MinimapSizing::Fill`], a
/// short file's pitch could stretch up to
/// [`crate::primitives::minimap::MAX_ROW_PITCH`] cell rows, but the paint
/// loop at the time only painted the first cell row of each band, leaving
/// `pitch - 1` blank cell rows between every painted row — a gap that grew
/// as the file got shorter. `FixedPitch(1.0)` pins the pitch to the
/// rasteriser's one-row paint cost, so rows tile contiguously regardless of
/// strip height, and long files fall back to the same sliding-window
/// behaviour GTK already relies on (see the module docs on
/// [`crate::primitives::minimap::Minimap::layout_with_sizing`]). A caller
/// that wants a short file to instead *stretch* across a tall strip reaches
/// for [`tui_minimap_layout_with_sizing`] with
/// [`MinimapSizing::RepeatToFill`] — paired with
/// [`draw_minimap_with_sizing`], whose paint loop now repeats each row
/// across its full resolved pitch instead of hardcoding one cell row.
pub fn tui_minimap_layout(minimap: &Minimap, area: Rect) -> MinimapLayout {
    tui_minimap_layout_with_sizing(minimap, area, MinimapSizing::FixedPitch(1.0))
}

/// [`tui_minimap_layout`], but with the row-pitch strategy as an explicit
/// parameter instead of the hardcoded [`MinimapSizing::FixedPitch`]`(1.0)`
/// default.
///
/// [`MinimapSizing::RepeatToFill`] is the sizing a host reaches for when it
/// wants a short file to visually fill a tall strip: each row repeat-paints
/// across a whole number of cell rows rather than top-aligning with blank
/// strip below it, with no risk of the fractional-pitch gaps
/// [`MinimapSizing::Fill`] produces on a cell-native rasteriser (see
/// [`MinimapSizing::Fill`]'s own doc). [`draw_minimap_with_sizing`]
/// is the paint-side counterpart that actually repeats the content.
pub fn tui_minimap_layout_with_sizing(
    minimap: &Minimap,
    area: Rect,
    sizing: MinimapSizing,
) -> MinimapLayout {
    let mut layout = minimap.layout_with_sizing(
        crate::event::Rect::new(
            area.x as f32,
            area.y as f32,
            area.width as f32,
            area.height as f32,
        ),
        LINES_PER_ROW,
        sizing,
    );
    // `layout_with_sizing` sets the default scale; TUI is the one
    // rasteriser whose strip is narrow enough that a fixed scale clips most
    // of a real file's width, so this is where the buffer-width-adaptive
    // resolution happens, on every call (paint or no-paint alike) so a host
    // asking for layout-only geometry (`Backend::minimap_layout`) sees the
    // exact same scale `draw_minimap` will paint with.
    layout.cols_per_cell = resolve_cols_per_cell(minimap, area.width as usize);
    layout
}

/// Draw a [`Minimap`] into `area` on `buf`, at [`resolve_cols_per_cell`]'s
/// buffer-width-adaptive horizontal scale. Returns the layout for host click
/// dispatch (`layout.hit_test(x, y)` -> [`crate::MinimapHit`]) —
/// `layout.cols_per_cell` carries the scale actually used, for a host that
/// colour-aggregates its own [`crate::MinimapSpan`]s to build a matching
/// [`crate::MinimapGrid`].
///
/// A thin wrapper over [`draw_minimap_with_scale`] — kept as the
/// zero-argument-change entry point so existing callers are unaffected.
/// This delegates to `draw_minimap_with_scale`, resolving its scale via
/// [`resolve_cols_per_cell`] adaptively from the buffer's own widest line.
pub fn draw_minimap(
    buf: &mut Buffer,
    area: Rect,
    minimap: &Minimap,
    theme: &Theme,
) -> MinimapLayout {
    let cols_per_cell = resolve_cols_per_cell(minimap, area.width as usize);
    draw_minimap_with_scale(buf, area, minimap, theme, cols_per_cell)
}

/// Resolve the default horizontal scale [`draw_minimap`] paints at:
/// buffer columns folded into one terminal cell, adapted to the buffer's
/// own widest sampled line rather than fixed at [`COLS_PER_CELL`].
///
/// `width_cells * 2` is the strip's total dot-column budget (each cell is
/// 2 braille dots wide). The scale is chosen so that budget spans the
/// buffer's widest line, capped at [`crate::primitives::minimap::COLUMN_CAPACITY`]
/// — VS Code's own `minimap.maxColumn` — the same cap the test
/// fixture already tunes against: a file wider than the cap still clips past
/// column 120, matching VS Code's own minimap.
///
/// A file whose widest line already fits inside `width_cells * 2`
/// resolves to exactly `2` — the same scale [`COLS_PER_CELL`] names —
/// so a narrow file's dots stay at the crisp 1-buffer-column-per-dot
/// scale rather than being widened for no reason; only a file with wider
/// content pays for a coarser scale.
///
/// Always returns an even number (`2 * cols_per_dot`, see [`cols_per_dot`]'s
/// own doc on why that matters) — safe to pass straight into
/// [`draw_minimap_with_scale`] or a [`crate::MinimapGrid::cols_per_cell`]
/// with no further rounding.
pub fn resolve_cols_per_cell(minimap: &Minimap, width_cells: usize) -> usize {
    let width_cells = width_cells.max(1);
    let max_line_len = minimap
        .lines
        .iter()
        .map(|l| l.text.chars().count())
        .max()
        .unwrap_or(0);
    let capped = max_line_len.min(crate::primitives::minimap::COLUMN_CAPACITY);
    let dot_budget = width_cells * 2;
    let resolved_cols_per_dot = capped.div_ceil(dot_budget).max(1);
    resolved_cols_per_dot * 2
}

/// [`draw_minimap`], but with the horizontal scale — how many source
/// columns fold into one terminal cell — as an explicit parameter instead
/// of the adaptive default.
///
/// `cols_per_cell` is clamped to at least `1` (a `0` value would collapse
/// [`cell_color`]'s lookup range to empty, matching nothing). The same
/// value must be used to build the [`crate::MinimapGrid`] passed to
/// [`crate::aggregate_spans`] — this rasteriser and that aggregation step
/// are only related by the caller's own consistent choice of scale, not by
/// a shared type, exactly as [`LINES_PER_ROW`] already works for the
/// vertical axis. The returned layout's `cols_per_cell` field always
/// echoes back this same (clamped) value.
pub fn draw_minimap_with_scale(
    buf: &mut Buffer,
    area: Rect,
    minimap: &Minimap,
    theme: &Theme,
    cols_per_cell: usize,
) -> MinimapLayout {
    let layout = tui_minimap_layout(minimap, area);
    paint_minimap_rows(buf, area, minimap, theme, cols_per_cell, layout)
}

/// [`draw_minimap_with_scale`], but with the row-pitch strategy as an
/// explicit [`MinimapSizing`] parameter instead of the hardcoded
/// [`MinimapSizing::FixedPitch`]`(1.0)` default — the paint-side counterpart
/// of [`tui_minimap_layout_with_sizing`].
///
/// Under [`MinimapSizing::RepeatToFill`], each visible row's
/// `vline.bounds.height` resolves to a whole-number multiple of the base
/// cell-row pitch; this function repeat-paints that row's identical
/// content across every one of those cell rows, so a short file visually
/// stretches to fill a tall strip with no blank rows between repeats.
pub fn draw_minimap_with_sizing(
    buf: &mut Buffer,
    area: Rect,
    minimap: &Minimap,
    theme: &Theme,
    cols_per_cell: usize,
    sizing: MinimapSizing,
) -> MinimapLayout {
    let layout = tui_minimap_layout_with_sizing(minimap, area, sizing);
    paint_minimap_rows(buf, area, minimap, theme, cols_per_cell, layout)
}

/// Shared paint loop behind [`draw_minimap_with_scale`] and
/// [`draw_minimap_with_sizing`]: rasterises `layout`'s visible rows,
/// repeat-painting each row's identical content across every whole cell row
/// its resolved `bounds.height` spans (exactly `1` for every existing
/// caller's [`MinimapSizing::FixedPitch`]`(1.0)` default, so this is a
/// strict generalisation with no behaviour change for them).
fn paint_minimap_rows(
    buf: &mut Buffer,
    area: Rect,
    minimap: &Minimap,
    theme: &Theme,
    cols_per_cell: usize,
    mut layout: MinimapLayout,
) -> MinimapLayout {
    let cols_per_cell = cols_per_cell.max(1);
    layout.cols_per_cell = cols_per_cell;

    if area.width == 0 || area.height == 0 {
        return layout;
    }

    let bg = ratatui_color(theme.background);
    let default_fg = ratatui_color(theme.foreground);
    // Pre-blended tint, not a solid `accent_bg` fill: a
    // terminal cell's background is one opaque colour, with no alpha
    // channel to composite through, so TUI can't lean on source-over the
    // way `gtk::minimap`/`macos::minimap` do (a translucent
    // `accent_bg`-at-0.25 fill over the already-painted background) --
    // it must pre-mix on the CPU instead, exactly like `win::minimap`
    // does via the same shared `Color::blend` helper. All four backends
    // now converge on the identical resulting colour:
    // `blend(background, accent_bg, 0.25)`.
    let highlight_bg = ratatui_color(theme.background.blend(theme.accent_bg, 0.25));
    let hl = &layout.viewport_highlight;
    let width_cells = area.width as usize;

    for vline in &layout.visible_lines {
        let row_y0 = vline.bounds.y.round();
        // How many whole cell rows this row's resolved pitch spans — `1`
        // for every fixed-pitch caller, `k` for `RepeatToFill`'s
        // whole-number stretch (see that variant's doc on why it's always
        // a whole number, never a fractional remainder to leave blank).
        let repeat_rows = vline.bounds.height.round().max(1.0) as u16;

        let row_mid = vline.bounds.y + vline.bounds.height * 0.5;
        let in_highlight = hl.height > 0.0 && row_mid >= hl.y && row_mid < hl.y + hl.height;
        let row_bg = if in_highlight { highlight_bg } else { bg };

        // The (up to) 4 buffer lines this row's braille dots come from,
        // pre-split into chars once per row rather than once per dot —
        // shared across every repeated cell row, since they all paint the
        // same content.
        let row_lines: Vec<Option<Vec<char>>> = (0..LINES_PER_ROW)
            .map(|dr| {
                minimap
                    .lines
                    .get(vline.start_line_idx + dr)
                    .map(|l| l.text.chars().collect())
            })
            .collect();

        for repeat in 0..repeat_rows {
            let row_y = row_y0 + repeat as f32;
            if row_y < 0.0 {
                continue;
            }
            let row_y = row_y as u16;
            if row_y < area.y || row_y >= area.y + area.height {
                continue;
            }

            for col in 0..width_cells {
                let ch = braille_char_for_cell(&row_lines, col, cols_per_cell);
                let fg = cell_color(
                    minimap,
                    vline.start_line_idx,
                    col,
                    default_fg,
                    theme,
                    cols_per_cell,
                );
                set_cell(buf, area.x + col as u16, row_y, ch, fg, row_bg);
            }
        }
    }

    layout
}

/// Source columns folded into a single **dot** column, derived from a
/// requested `cols_per_cell` so that [`braille_char_for_cell`]'s dot
/// ranges and [`cell_color`]'s colour range can never disagree.
///
/// Both functions used to derive their column width from `cols_per_cell`
/// independently — `braille_char_for_cell` via truncating division and
/// `cell_color` via `cols_per_cell` directly — which coincide only when
/// `cols_per_cell` is even. For an odd value the two ranges drift apart.
///
/// Routing both call sites through this one function — rounding *up*
/// (`div_ceil`) rather than down, so the degenerate `cols_per_cell <= 1`
/// case still gets a non-empty width — makes the effective per-cell
/// width exactly `2 * cols_per_dot` everywhere, for any input.
fn cols_per_dot(cols_per_cell: usize) -> usize {
    cols_per_cell.max(1).div_ceil(2).max(1)
}

/// Pack one terminal cell's braille glyph from up to [`LINES_PER_ROW`]
/// pre-split lines. `col` is the terminal-cell column (0-based within
/// the minimap); each cell is 2 dots wide, so dot columns
/// `col*2..col*2+2` map to buffer columns at a **fixed** scale of
/// [`cols_per_dot`] buffer columns per dot — the same scale for every
/// line and every dot column of every cell, so widening it widens the
/// represented range without disturbing the fixed-per-line property.
///
/// Column position is otherwise the only signal indentation has: if the
/// scale were normalised per-line (each line stretched to fill the full
/// strip width), a short line's 4-space indent would land at a different
/// dot column than the same 4-space indent on a long line, destroying
/// indentation entirely. Fixed scale means a line shorter than the strip
/// simply leaves the remaining dots clear, and a line longer than
/// `width_cells * 2 * cols_per_dot(cols_per_cell)` is clipped rather than
/// compressed — exactly what VS Code's minimap does.
///
/// The dot rule itself is a **coverage fraction**, not a boolean OR:
/// `covered` counts the non-whitespace characters inside the dot's own
/// `[c0, c1)` bucket — a plain per-dot scan, the same order of work an
/// earlier scan already paid, no new allocation — and
/// [`dither_threshold_met`] thresholds that count against an ordered
/// dither matrix keyed on the dot's own absolute position. At the
/// (default) `cols_per_dot == 1` scale this is a no-op — a bucket of width
/// one has no fractional coverage to dither, so the result is identical to
/// a plain `covered > 0` check — the behaviour change only appears once a
/// host widens `cols_per_cell` far enough that a bucket can
/// be partially covered.
fn braille_char_for_cell(
    row_lines: &[Option<Vec<char>>],
    col: usize,
    cols_per_cell: usize,
) -> char {
    let cols_per_dot = cols_per_dot(cols_per_cell);
    pack_braille_cell(|dr, dc| {
        let chars = match row_lines.get(dr).and_then(|o| o.as_ref()) {
            Some(c) if !c.is_empty() => c,
            _ => return false,
        };
        let dot_col = col * 2 + dc;
        let c0 = dot_col * cols_per_dot;
        if c0 >= chars.len() {
            return false;
        }
        let c1 = (c0 + cols_per_dot).min(chars.len());
        let covered = chars[c0..c1].iter().filter(|c| !c.is_whitespace()).count();
        dither_threshold_met(covered, cols_per_dot, dr, dot_col)
    })
}

/// Resolve this cell's foreground: the aggregated [`crate::MinimapSpan`]
/// covering `(start_line_idx, col)` if one exists, else the theme
/// default. `minimap.syntax_spans` is expected to already be aggregated
/// at TUI's `4`-line x (`2 * `[`cols_per_dot`]`(cols_per_cell)`)-column
/// cell granularity (via [`crate::aggregate_spans`], with a matching
/// [`crate::MinimapGrid::cols_per_cell`]) — this does a plain containment
/// scan, no re-aggregation.
///
/// The column range is derived from [`cols_per_dot`] — the same helper
/// [`braille_char_for_cell`] uses — rather than from `cols_per_cell`
/// directly, so the two can never disagree.
fn cell_color(
    minimap: &Minimap,
    start_line_idx: usize,
    col: usize,
    default_fg: ratatui::style::Color,
    theme: &Theme,
    cols_per_cell: usize,
) -> ratatui::style::Color {
    let cell_width = 2 * cols_per_dot(cols_per_cell);
    let col_lo = col * cell_width;
    let col_hi = col_lo + cell_width;
    minimap
        .syntax_spans
        .iter()
        .find(|s| s.line_idx == start_line_idx && s.start_col < col_hi && s.end_col > col_lo)
        .map(|s| ratatui_color(s.color))
        .unwrap_or({
            let _ = theme; // default_fg already derives from theme
            default_fg
        })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::primitives::minimap::{MinimapHit, MinimapLine, MinimapSpan};
    use crate::types::{Color, WidgetId};

    fn cell_char(buf: &Buffer, x: u16, y: u16) -> char {
        buf[(x, y)].symbol().chars().next().unwrap_or(' ')
    }

    fn minimap_from(lines: Vec<&str>, total_buffer_lines: usize) -> Minimap {
        Minimap {
            id: WidgetId::new("mm"),
            lines: lines
                .into_iter()
                .enumerate()
                .map(|(i, t)| MinimapLine {
                    text: t.into(),
                    line_idx: i,
                })
                .collect(),
            syntax_spans: Vec::new(),
            visible_row_start: 0,
            visible_row_count: 0,
            total_buffer_lines,
        }
    }

    /// 8 lines x 4 columns, one "on" dot per line at a different column,
    /// laid out over a 2-cell-wide x 2-row area (dot grid 4 wide x 8
    /// tall matches the buffer exactly — no proportional scaling to
    /// reason about). This is the transposition guard: each of the 4 dot
    /// *rows* in the first cell-row carries exactly one set dot, in a
    /// different dot *column* each time, so swapping row/col in the bit
    /// table would produce a different (wrong) codepoint.
    fn eight_by_four() -> Minimap {
        minimap_from(
            vec![
                "X   ", "  X ", " X  ", "   X", "    ", "    ", "    ", "    ",
            ],
            8,
        )
    }

    #[test]
    fn braille_packing_exercises_all_dot_rows_and_both_dot_columns() {
        let mm = eight_by_four();
        let area = Rect::new(0, 0, 2, 2);
        let mut buf = Buffer::empty(area);
        let _layout = draw_minimap(&mut buf, area, &mm, &Theme::default());
        // Hand-derived from the BRAILLE_OFFSETS table (see `super::braille`):
        // left cell: bit0 (line0 col0) + bit5 (line2 col1) -> 0x21.
        // right cell: bit1 (line1 col0) + bit7 (line3 col1) -> 0x82.
        assert_eq!(cell_char(&buf, 0, 0), '\u{2821}', "left cell mispacked");
        assert_eq!(cell_char(&buf, 1, 0), '\u{2882}', "right cell mispacked");
    }

    #[test]
    fn all_whitespace_group_packs_to_blank_braille_not_a_space() {
        let mm = eight_by_four();
        let area = Rect::new(0, 0, 2, 2);
        let mut buf = Buffer::empty(area);
        let _layout = draw_minimap(&mut buf, area, &mm, &Theme::default());
        // Second row group (lines 4-7) is all-whitespace: must paint the
        // actual U+2800 blank-braille glyph, not a plain space, so a
        // minimap row visually reads as "no code here" rather than an
        // untouched cell.
        assert_eq!(cell_char(&buf, 0, 1), '\u{2800}');
        assert_eq!(cell_char(&buf, 1, 1), '\u{2800}');
    }

    #[test]
    fn cell_color_uses_the_aggregated_span_for_that_cell() {
        let mut mm = eight_by_four();
        let red = Color::rgb(255, 0, 0);
        mm.syntax_spans.push(MinimapSpan {
            line_idx: 0,
            start_col: 0,
            end_col: 2,
            color: red,
        });
        let area = Rect::new(0, 0, 2, 2);
        let mut buf = Buffer::empty(area);
        let _layout = draw_minimap(&mut buf, area, &mm, &Theme::default());
        assert_eq!(buf[(0u16, 0u16)].fg, ratatui_color(red));
        // The other cell has no matching span: falls back to theme fg.
        assert_eq!(
            buf[(1u16, 0u16)].fg,
            ratatui_color(Theme::default().foreground)
        );
    }

    #[test]
    fn viewport_highlight_paints_a_background_band_not_a_foreground_overlay() {
        let mut mm = eight_by_four();
        mm.visible_row_start = 0;
        mm.visible_row_count = 4; // first row group only
        let theme = Theme {
            accent_bg: Color::rgb(9, 9, 9),
            ..Theme::default()
        };
        let area = Rect::new(0, 0, 2, 2);
        let mut buf = Buffer::empty(area);
        let _layout = draw_minimap(&mut buf, area, &mm, &theme);
        let expected_tint = ratatui_color(theme.background.blend(theme.accent_bg, 0.25));
        assert_eq!(buf[(0u16, 0u16)].bg, expected_tint);
        assert_ne!(buf[(0u16, 1u16)].bg, expected_tint);
    }

    /// Regression test: the viewport highlight must be the same pre-blended
    /// tint converged on across backends (`blend(background, accent_bg, 0.25)`),
    /// not opaque `theme.accent_bg` at full strength.
    #[test]
    fn viewport_highlight_is_a_blended_tint_not_opaque_accent_bg() {
        let mut mm = eight_by_four();
        mm.visible_row_start = 0;
        mm.visible_row_count = 4; // first row group only
        let theme = Theme {
            background: Color::rgb(20, 20, 20),
            accent_bg: Color::rgb(0, 96, 191),
            ..Theme::default()
        };
        let area = Rect::new(0, 0, 2, 2);
        let mut buf = Buffer::empty(area);
        let _layout = draw_minimap(&mut buf, area, &mm, &theme);

        let expected = theme.background.blend(theme.accent_bg, 0.25);
        assert_eq!(buf[(0u16, 0u16)].bg, ratatui_color(expected));
        assert_ne!(
            buf[(0u16, 0u16)].bg,
            ratatui_color(theme.accent_bg),
            "the viewport slider must not paint opaque accent_bg at full strength"
        );
    }

    #[test]
    fn fully_visible_file_paints_no_viewport_band_at_all() {
        // An empty or short buffer whose viewport already spans every row
        // has nothing to scroll to, so no row may carry the highlight
        // tint -- otherwise the whole strip reads as one solid band
        // instead of a position indicator.
        let theme = Theme {
            background: Color::rgb(20, 20, 20),
            accent_bg: Color::rgb(0, 96, 191),
            ..Theme::default()
        };
        let tint = ratatui_color(theme.background.blend(theme.accent_bg, 0.25));

        for (lines, label) in [
            (vec![""], "single empty line"),
            (vec!["fn main() {}", "", "// short"], "three-line file"),
        ] {
            let total = lines.len();
            let mut mm = minimap_from(lines, total);
            mm.visible_row_start = 0;
            // A host reporting its whole window's row budget, well past
            // the end of the buffer.
            mm.visible_row_count = 40;
            let area = Rect::new(0, 0, 3, 6);
            let mut buf = Buffer::empty(area);
            let layout = draw_minimap(&mut buf, area, &mm, &theme);
            assert_eq!(
                layout.viewport_highlight.height, 0.0,
                "{label}: layout must not report a viewport band"
            );
            assert!(
                !layout.visible_lines.is_empty(),
                "{label}: nothing was painted, so the tint check below is vacuous"
            );
            for y in area.y..area.y + area.height {
                for x in area.x..area.x + area.width {
                    assert_ne!(buf[(x, y)].bg, tint, "{label}: cell ({x},{y}) is tinted");
                }
            }
        }
    }

    /// Shared body for `paint_and_click_round_trip_returns_seek_for_the_clicked_fraction`
    /// — see `docs/PRIMITIVE_RULES.md`'s "Coordinate frames for
    /// `*_layout` methods": `minimap_layout` is documented
    /// **ABSOLUTE**, so `hit_test` must be called with coordinates
    /// shifted by the same `origin_x`/`origin_y` the minimap was
    /// painted at, not with area-local coordinates.
    fn paint_and_click_round_trip_at(origin_x: u16, origin_y: u16) {
        let mm = minimap_from(vec!["x"; 8], 8);
        let area = Rect::new(origin_x, origin_y, 4, 8); // 2 rows of 4 lines each -> track height 8
        let mut buf = Buffer::empty(area);
        let layout = draw_minimap(&mut buf, area, &mm, &Theme::default());
        assert_eq!(
            layout.hit_test(origin_x as f32 + 2.0, origin_y as f32 + 4.0),
            MinimapHit::Seek { fraction: 0.5 }
        );
        assert_eq!(
            layout.hit_test(origin_x as f32 + 2.0, origin_y as f32),
            MinimapHit::Seek { fraction: 0.0 }
        );
    }

    #[test]
    fn paint_and_click_round_trip_returns_seek_for_the_clicked_fraction() {
        paint_and_click_round_trip_at(0, 0);
    }

    /// Non-zero-origin regression guard: `area = (0, 0)` is exactly the case
    /// where a LOCAL/ABSOLUTE mixup in `tui_minimap_layout` would be invisible.
    #[test]
    fn paint_and_click_round_trip_returns_seek_for_the_clicked_fraction_at_nonzero_origin() {
        paint_and_click_round_trip_at(7, 13);
    }

    /// Regression test: under the previous [`MinimapSizing::Fill`] sizing, a
    /// short file in a tall strip resolved to a row pitch of up to
    /// `MAX_ROW_PITCH` cell rows, but `draw_minimap` only ever painted the
    /// first cell row of each band — leaving `pitch - 1` blank cell rows
    /// between every painted row, growing as the file got shorter. TUI now
    /// uses `FixedPitch(1.0)`: exactly one cell row per minimap row, tiled
    /// top-down with no gaps, regardless of strip height.
    #[test]
    fn short_file_in_a_tall_strip_paints_contiguous_rows_no_gaps() {
        let mm = eight_by_four(); // 8 lines, 2 row groups of 4 lines each
        let area = Rect::new(0, 0, 2, 20); // strip far taller than 2 rows
        let mut buf = Buffer::empty(area);
        let layout = draw_minimap(&mut buf, area, &mm, &Theme::default());

        assert_eq!(layout.visible_lines.len(), 2);
        assert_eq!(
            layout.visible_lines[0].bounds.height, 1.0,
            "row pitch must be exactly one cell row, not stretched toward MAX_ROW_PITCH"
        );
        assert_eq!(layout.visible_lines[1].bounds.height, 1.0);
        assert_eq!(layout.visible_lines[0].bounds.y, 0.0);
        assert_eq!(
            layout.visible_lines[1].bounds.y, 1.0,
            "row 1 must sit directly below row 0 with no gap"
        );

        // Both painted rows must actually carry braille glyphs (row 1 is
        // all-whitespace, so it paints the blank-braille U+2800 glyph, not
        // an untouched space) -- contiguous, with no blank cell row
        // painted between them.
        assert_ne!(cell_char(&buf, 0, 0), ' ');
        assert_ne!(cell_char(&buf, 0, 1), ' ');
        // The rest of the tall strip stays untouched: a short file
        // top-aligns instead of stretching to fill it (unchanged from
        // `Fill`'s own top-align behaviour).
        assert_eq!(cell_char(&buf, 0, 2), ' ');
    }

    #[test]
    fn zero_size_is_a_no_op() {
        let mm = eight_by_four();
        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 4));
        let _layout = draw_minimap(&mut buf, Rect::new(0, 0, 0, 4), &mm, &Theme::default());
        assert_eq!(cell_char(&buf, 0, 0), ' ');
    }

    /// Returns the first terminal-cell column (out of `0..width_cells`)
    /// whose braille glyph differs from the blank glyph U+2800, i.e. the
    /// cell carrying `chars`' leading non-whitespace character, at the
    /// default [`COLS_PER_CELL`] scale.
    fn first_set_cell(chars: &[char], width_cells: usize) -> Option<usize> {
        first_set_cell_at_scale(chars, width_cells, COLS_PER_CELL)
    }

    /// [`first_set_cell`], but at an explicit `cols_per_cell` scale
    fn first_set_cell_at_scale(
        chars: &[char],
        width_cells: usize,
        cols_per_cell: usize,
    ) -> Option<usize> {
        let row_lines = [Some(chars.to_vec()), None, None, None];
        (0..width_cells)
            .find(|&col| braille_char_for_cell(&row_lines, col, cols_per_cell) != '\u{2800}')
    }

    /// Regression test: `braille_char_for_cell` used to normalise each line's
    /// dot mapping by **that line's own** character count, so a short line's
    /// content was stretched across the *entire* strip width regardless of how
    /// little of that width the line actually occupies. Column position is the
    /// only signal indentation has, so three lines with increasing indent
    /// ("x", "    x", "        x") must paint their set dots at three
    /// distinct columns *proportional to the indent* — not bunched together
    /// near the strip's far edge, which is what the old per-line normalisation
    /// produced.
    #[test]
    fn indentation_lands_at_distinct_columns_proportional_to_indent() {
        let width_cells = 8;
        let flush: Vec<char> = "x".chars().collect();
        let indent_4: Vec<char> = "    x".chars().collect();
        let indent_8: Vec<char> = "        x".chars().collect();

        let c_flush = first_set_cell(&flush, width_cells).expect("flush line must set a dot");
        let c_indent_4 =
            first_set_cell(&indent_4, width_cells).expect("indent-4 line must set a dot");
        let c_indent_8 =
            first_set_cell(&indent_8, width_cells).expect("indent-8 line must set a dot");

        assert!(
            c_flush < c_indent_4 && c_indent_4 < c_indent_8,
            "expected strictly increasing columns, got {c_flush} < {c_indent_4} < {c_indent_8}"
        );
        // Fixed scale: COLS_PER_CELL buffer columns per terminal cell,
        // so doubling the indent must double the column offset exactly
        // — this is what "proportional" pins down, not just "increasing".
        assert_eq!(c_indent_4, c_flush + 4 / COLS_PER_CELL);
        assert_eq!(c_indent_8, c_flush + 8 / COLS_PER_CELL);
    }

    /// A short, indented line's dot column must depend only on its own
    /// indent — never on the length of some other line sharing the same
    /// braille row group. This is the fixed-scale requirement applied
    /// defensively at the row-group level: `dr=0`'s short line and
    /// `dr=1`'s 200-column line are packed into the very same terminal
    /// cell row, so a regression toward row-level normalisation would move
    /// `dr=0`'s dot even though nothing about that line itself changed.
    #[test]
    fn a_long_neighbour_line_does_not_move_a_short_lines_dot() {
        let width_cells = 8;
        let indented: Vec<char> = "    x".chars().collect();
        let very_long: Vec<char> = format!("{}y", " ".repeat(200)).chars().collect();

        let alone = {
            let row_lines = [Some(indented.clone()), None, None, None];
            (0..width_cells)
                .find(|&col| braille_char_for_cell(&row_lines, col, COLS_PER_CELL) != '\u{2800}')
        };
        let with_long_neighbor = {
            let row_lines = [Some(indented.clone()), Some(very_long), None, None];
            (0..width_cells)
                .find(|&col| braille_char_for_cell(&row_lines, col, COLS_PER_CELL) != '\u{2800}')
        };

        assert_eq!(
            alone, with_long_neighbor,
            "a long neighbouring line must not shift where the short line's dot lands"
        );
    }

    /// A line longer than the strip's visible column range must be
    /// **clipped**, not compressed to fit — content past
    /// `width_cells * COLS_PER_CELL` never sets a dot, no matter how far
    /// past it extends. This is what distinguishes a fixed scale from a
    /// per-line stretch, which had no notion of "off the edge" since it
    /// always rescaled to fit exactly.
    #[test]
    fn a_line_past_the_visible_range_is_clipped_not_compressed() {
        let width_cells = 4;
        let visible_cols = width_cells * COLS_PER_CELL;

        // Non-whitespace only at the last visible column: must be seen.
        let mut at_edge = vec![' '; visible_cols];
        at_edge[visible_cols - 1] = 'x';
        assert!(
            first_set_cell(&at_edge, width_cells).is_some(),
            "a dot within the visible range must be painted"
        );

        // Non-whitespace only one column past the visible range: clipped.
        let mut past_edge = vec![' '; visible_cols + 1];
        past_edge[visible_cols] = 'x';
        assert_eq!(
            first_set_cell(&past_edge, width_cells),
            None,
            "content past width_cells * COLS_PER_CELL must be clipped, not compressed into view"
        );
    }

    // ── cols_per_cell as a parameter, not a constant ───────────

    #[test]
    fn draw_minimap_matches_draw_minimap_with_scale_at_the_default_cols_per_cell() {
        // `draw_minimap` delegates to `draw_minimap_with_scale`, resolving
        // its own scale via `resolve_cols_per_cell` rather than always passing
        // a hardcoded constant. `eight_by_four`'s widest line is 4 columns on a
        // 2-cell-wide strip (dot budget 4), which resolves to exactly
        // `COLS_PER_CELL` — this fixture is deliberately narrow so the two entry
        // points still agree here, proving `draw_minimap` is still a pure
        // delegation (just to a resolved, not hardcoded, scale) rather than
        // diverging paint logic of its own.
        let mm = eight_by_four();
        let area = Rect::new(0, 0, 2, 2);

        let mut via_plain = Buffer::empty(area);
        let plain_layout = draw_minimap(&mut via_plain, area, &mm, &Theme::default());

        let mut via_scale = Buffer::empty(area);
        let scale_layout =
            draw_minimap_with_scale(&mut via_scale, area, &mm, &Theme::default(), COLS_PER_CELL);

        assert_eq!(via_plain, via_scale);
        assert_eq!(plain_layout, scale_layout);
    }

    /// At the default [`COLS_PER_CELL`] scale, an 11-cell (VS-Code-proportioned)
    /// strip can only represent `11 * 2 = 22` source columns, so content at
    /// column 40 is unrepresentable — clipped. Widening `cols_per_cell` to
    /// `10` (5 source columns per dot) makes the same 11-cell strip cover
    /// `11 * 10 = 110` columns, so column 40 now lands inside cell `4` — this
    /// is the scale becoming a *parameter* a host can widen, not a constant
    /// baked into the rasteriser.
    #[test]
    fn draw_minimap_with_scale_widens_the_representable_source_range() {
        let width_cells = 11;
        let mut indent_40 = vec![' '; 41];
        indent_40[40] = 'x';

        assert_eq!(
            first_set_cell_at_scale(&indent_40, width_cells, COLS_PER_CELL),
            None,
            "column 40 must be unrepresentable at the default 2-cols-per-cell scale \
             on an 11-cell strip (visible range is only 22 columns)"
        );

        let widened_cols_per_cell = 10;
        assert_eq!(
            first_set_cell_at_scale(&indent_40, width_cells, widened_cols_per_cell),
            Some(4),
            "column 40 must land in cell 4 once cols_per_cell is widened to 10 \
             (5 source columns per dot, 11 cells -> 110 columns of coverage)"
        );
    }

    /// With `cols_per_cell` widened *explicitly* to `10`, two syntax spans at
    /// columns 40 and 80 each resolve to their own colour in the painted cell
    /// rather than falling back to the theme default — the same colour-survival
    /// property guaranteed near column 0, now reachable at depths the fixed
    /// [`COLS_PER_CELL`] scale could never paint at all.
    ///
    /// The "before widening" baseline here uses [`draw_minimap_with_scale`]
    /// pinned to the fixed [`COLS_PER_CELL`], not [`draw_minimap`]'s own
    /// default — since `draw_minimap` resolves its scale adaptively from the
    /// buffer's widest line, so for this fixture (a 90-column line) it would
    /// already reach both spans without any explicit widening. This test's own
    /// job is to prove the *explicit* `cols_per_cell` parameter works as a
    /// widening mechanism independent of the adaptive default.
    #[test]
    fn cell_color_survives_at_indent_40_and_80_with_a_widened_scale() {
        let mut line = vec![' '; 90];
        line[40] = 'a';
        line[80] = 'b';
        let text: String = line.into_iter().collect();

        let mut mm = minimap_from(vec![&text], 8);
        let red = Color::rgb(255, 0, 0);
        let blue = Color::rgb(0, 0, 255);
        mm.syntax_spans.push(MinimapSpan {
            line_idx: 0,
            start_col: 40,
            end_col: 50,
            color: red,
        });
        mm.syntax_spans.push(MinimapSpan {
            line_idx: 0,
            start_col: 80,
            end_col: 90,
            color: blue,
        });

        let area = Rect::new(0, 0, 11, 1);

        // Both spans are beyond the 22-column visible range at the fixed
        // COLS_PER_CELL scale, so both cells fall back to the theme default.
        let mut buf_narrow = Buffer::empty(area);
        draw_minimap_with_scale(&mut buf_narrow, area, &mm, &Theme::default(), COLS_PER_CELL);
        assert_eq!(
            buf_narrow[(4u16, 0u16)].fg,
            ratatui_color(Theme::default().foreground),
            "at the fixed COLS_PER_CELL scale, column 40's span must be unreachable"
        );
        assert_eq!(
            buf_narrow[(8u16, 0u16)].fg,
            ratatui_color(Theme::default().foreground),
            "at the fixed COLS_PER_CELL scale, column 80's span must be unreachable"
        );

        // Widened to 10 cols per cell (5 per dot), both spans land inside
        // the strip and paint their own distinct colour.
        let mut buf_wide = Buffer::empty(area);
        draw_minimap_with_scale(&mut buf_wide, area, &mm, &Theme::default(), 10);
        assert_eq!(
            buf_wide[(4u16, 0u16)].fg,
            ratatui_color(red),
            "colour must survive at indent 40"
        );
        assert_eq!(
            buf_wide[(8u16, 0u16)].fg,
            ratatui_color(blue),
            "colour must survive at indent 80"
        );
    }

    #[test]
    fn draw_minimap_with_scale_clamps_zero_to_one_instead_of_matching_everything() {
        // `cols_per_cell: 0` would otherwise collapse `cell_color`'s
        // `col_lo..col_hi` lookup range to empty (matching nothing) while
        // `braille_char_for_cell`'s `cols_per_dot` already guards its own
        // `.max(1)` — the clamp in `draw_minimap_with_scale` must cover
        // both call sites uniformly rather than relying on each helper's
        // own partial guard.
        let mm = eight_by_four();
        let area = Rect::new(0, 0, 2, 2);

        let mut via_zero = Buffer::empty(area);
        draw_minimap_with_scale(&mut via_zero, area, &mm, &Theme::default(), 0);

        let mut via_one = Buffer::empty(area);
        draw_minimap_with_scale(&mut via_one, area, &mm, &Theme::default(), 1);

        assert_eq!(
            via_zero, via_one,
            "cols_per_cell: 0 must behave like 1, not empty-match"
        );
    }

    /// Regression test: before this fix, `braille_char_for_cell` derived its
    /// dot range via truncating division while `cell_color` derived its colour
    /// range directly — these only coincide when `cols_per_cell` is **even**.
    /// At an odd value (`11`), the two used to diverge: dots for terminal cell
    /// 4 covered source columns `[40, 50)` while `cell_color` looked up
    /// `[44, 55)`. A span landing in only one of the two mismatched ranges
    /// either painted colour with no real dot behind it, or a dot with no colour.
    ///
    /// Both functions now derive their width from the same [`cols_per_dot`]
    /// helper, rounding `11` up to `6` and giving both a shared effective cell
    /// width of `2 * 6 = 12`: cell 4 covers exactly `[48, 60)`. This asserts
    /// that shared boundary directly — a source range fully inside it covers
    /// both a dot and its span's colour in cell 4, and a source column just
    /// outside it must set neither, proving the two lookups can't drift apart.
    ///
    /// The "inside" case fills the *entire* `[48, 60)` bucket rather than
    /// a single character: a lone non-whitespace character in a multi-column
    /// bucket is no longer guaranteed to set its dot — that coverage is now
    /// dithered — but full coverage of a bucket always does, since
    /// [`super::braille::dither_threshold_met`]'s threshold tops out below
    /// full scale. This keeps the test's target — dot and colour ranges must
    /// agree — deterministic regardless of which positions the dither lands on.
    #[test]
    fn odd_cols_per_cell_keeps_dot_and_colour_ranges_aligned() {
        let width_cells = 8;
        let area = Rect::new(0, 0, width_cells as u16, 1);
        let red = Color::rgb(255, 0, 0);

        // Inside the shared [48, 60) range for cell 4: dot and colour
        // must both be present.
        {
            let mut chars = vec![' '; 60];
            chars[48..60].fill('x');
            let text: String = chars.into_iter().collect();
            let mut mm = minimap_from(vec![&text], 8);
            mm.syntax_spans.push(MinimapSpan {
                line_idx: 0,
                start_col: 55,
                end_col: 56,
                color: red,
            });

            let mut buf = Buffer::empty(area);
            draw_minimap_with_scale(&mut buf, area, &mm, &Theme::default(), 11);
            assert_ne!(
                cell_char(&buf, 4, 0),
                '\u{2800}',
                "a fully-covered [48, 60) bucket must set a dot in cell 4"
            );
            assert_eq!(
                buf[(4u16, 0u16)].fg,
                ratatui_color(red),
                "column 55's span is inside cell 4's [48, 60) colour range and must be found"
            );
        }

        // Just outside the shared range (column 47, one before 48): dot
        // and colour must both be absent -- if the two ranges had drifted
        // apart (as they did pre-fix), one of these would fire while the
        // other didn't.
        {
            let mut chars = vec![' '; 60];
            chars[47] = 'x';
            let text: String = chars.into_iter().collect();
            let mut mm = minimap_from(vec![&text], 8);
            mm.syntax_spans.push(MinimapSpan {
                line_idx: 0,
                start_col: 47,
                end_col: 48,
                color: red,
            });

            let mut buf = Buffer::empty(area);
            draw_minimap_with_scale(&mut buf, area, &mm, &Theme::default(), 11);
            assert_eq!(
                cell_char(&buf, 4, 0),
                '\u{2800}',
                "column 47 is outside cell 4's [48, 60) range and must not set a dot"
            );
            assert_eq!(
                buf[(4u16, 0u16)].fg,
                ratatui_color(Theme::default().foreground),
                "column 47's span is outside cell 4's [48, 60) colour range and must not be found"
            );
        }
    }

    // ── density dithering fixes right-edge saturation ────────────

    /// Draws `lines` into a `width_cells`-wide, `rows`-tall strip at
    /// `cols_per_cell` scale (down-sampling first if there are more than
    /// `rows * LINES_PER_ROW` lines, exactly as a real host would via
    /// [`crate::primitives::minimap::sample_blocks`]) and returns, for each
    /// painted row, the index of the last terminal-cell column whose
    /// braille glyph is not blank (`U+2800`) — `None` for a row with no
    /// content at all.
    fn last_set_cell_per_row(
        lines: &[&str],
        width_cells: usize,
        rows: usize,
        cols_per_cell: usize,
    ) -> Vec<Option<usize>> {
        use crate::primitives::minimap::sample_blocks;

        let sampled = sample_blocks(lines.len(), rows * LINES_PER_ROW, |i| lines[i].to_string());
        let total = lines.len();
        let mm = minimap_from_sampled(sampled, total);

        let area = Rect::new(0, 0, width_cells as u16, rows as u16);
        let mut buf = Buffer::empty(area);
        draw_minimap_with_scale(&mut buf, area, &mm, &Theme::default(), cols_per_cell);

        (0..rows as u16)
            .map(|y| {
                (0..width_cells as u16)
                    .rev()
                    .find(|&x| cell_char(&buf, x, y) != '\u{2800}')
                    .map(|x| x as usize)
            })
            .collect()
    }

    fn minimap_from_sampled(
        sampled: Vec<crate::primitives::minimap::MinimapLine>,
        total_buffer_lines: usize,
    ) -> Minimap {
        Minimap {
            id: WidgetId::new("mm"),
            lines: sampled,
            syntax_spans: Vec::new(),
            visible_row_start: 0,
            visible_row_count: 0,
            total_buffer_lines,
        }
    }

    /// Core deliverable, acceptance criterion 1: on a real (>= 500 line)
    /// source file, at a VS-Code-proportioned 12-cell x 33-row TUI strip, at
    /// most 20% of painted rows may run all the way to the strip's right edge.
    /// This is the exact reproduction the original measurement produced (100%)
    /// under the old boolean-OR dot rule; this test drives the same widened-scale
    /// + dithered-dots combination: `cols_per_cell: 10` (5 source columns per dot)
    /// is exactly the scale a host widens to via `draw_minimap_with_scale` once
    /// it also wants dithered dots to stop saturating at that scale.
    #[test]
    fn real_source_file_minimap_right_edge_tracks_line_length_not_saturated() {
        let source = include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/src/compose/tab_group.rs"
        ));
        let lines: Vec<&str> = source.lines().collect();
        assert!(
            lines.len() >= 500,
            "fixture must be a real, checked-in source file of at least 500 lines, got {}",
            lines.len()
        );

        let width_cells = 12;
        let rows = 33;
        let cols_per_cell = 10; // 5 source columns per dot (issue #1007 "variant C")

        let last_cols = last_set_cell_per_row(&lines, width_cells, rows, cols_per_cell);

        let painted: Vec<usize> = last_cols.into_iter().flatten().collect();
        assert!(
            !painted.is_empty(),
            "expected at least some painted rows from a real source file"
        );
        let right_edge_rows = painted.iter().filter(|&&c| c == width_cells - 1).count();
        let fraction = right_edge_rows as f64 / painted.len() as f64;
        assert!(
            fraction <= 0.20,
            "{right_edge_rows}/{} painted rows ({:.1}%) run to the strip's right edge, \
             expected at most 20% -- the whole point of #1007 is that most rows should \
             NOT saturate to the full strip width",
            painted.len(),
            fraction * 100.0
        );
    }

    /// Acceptance criterion 2: a file of uniform-length lines and a file of
    /// mixed-length lines must produce *visibly different* right-edge profiles
    /// — the whole reason this fix exists is that line length should be a
    /// visible signal in the minimap's silhouette, not something every row
    /// erases by saturating to the same column.
    #[test]
    fn last_set_cell_varies_across_rows_for_mixed_lengths_but_not_for_uniform_ones() {
        let width_cells = 12;
        let rows = 16;
        let cols_per_cell = 10;

        // Every line identical in shape and length: the right edge must
        // land on the very same column for every painted row.
        let uniform_lines: Vec<String> = (0..rows * LINES_PER_ROW)
            .map(|_| "    let value = compute_something(a, b, c);".to_string())
            .collect();
        let uniform_refs: Vec<&str> = uniform_lines.iter().map(String::as_str).collect();
        let uniform_last_cols =
            last_set_cell_per_row(&uniform_refs, width_cells, rows, cols_per_cell);
        let uniform_painted: Vec<usize> = uniform_last_cols.into_iter().flatten().collect();
        assert!(
            uniform_painted.len() > 1,
            "expected multiple painted rows from the uniform fixture"
        );
        assert!(
            uniform_painted.iter().all(|&c| c == uniform_painted[0]),
            "uniform-length lines must produce a uniform right edge, got {uniform_painted:?}"
        );

        // Alternating short/long lines, one strip *row's* worth (4 lines)
        // at a time -- since a row's braille cell ORs across all
        // LINES_PER_ROW lines it packs, alternating line-by-line would
        // give every row group the exact same short+long mix and thus the
        // exact same (non-varying) right edge; alternating a whole row
        // group at a time is what actually exercises "does length vary
        // *between rows*".
        let mixed_lines: Vec<String> = (0..rows)
            .flat_map(|r| {
                let line = if r % 2 == 0 {
                    "x;".to_string()
                } else {
                    "                    let very_long_line_of_code_here = 12345;".to_string()
                };
                std::iter::repeat(line).take(LINES_PER_ROW)
            })
            .collect();
        let mixed_refs: Vec<&str> = mixed_lines.iter().map(String::as_str).collect();
        let mixed_last_cols = last_set_cell_per_row(&mixed_refs, width_cells, rows, cols_per_cell);
        let mixed_painted: Vec<usize> = mixed_last_cols.into_iter().flatten().collect();
        assert!(
            mixed_painted.len() > 1,
            "expected multiple painted rows from the mixed fixture"
        );
        assert!(
            mixed_painted.iter().any(|&c| c != mixed_painted[0]),
            "mixed-length lines must produce a varying right edge, got {mixed_painted:?}"
        );
    }

    // ── default scale adapts to the buffer's own width ──────────

    #[test]
    fn resolve_cols_per_cell_stays_at_the_narrow_default_when_the_file_already_fits() {
        // A 20-column line on a 12-cell strip (dot budget 24) already
        // fits inside the crispest possible scale: widening would only
        // throw away resolution for no reason.
        let text = "x".repeat(20);
        let mm = minimap_from(vec![text.as_str()], 1);
        assert_eq!(resolve_cols_per_cell(&mm, 12), COLS_PER_CELL);
    }

    #[test]
    fn resolve_cols_per_cell_widens_to_cover_a_wider_files_longest_line() {
        // Same 90-column fixture as
        // `cell_color_survives_at_indent_40_and_80_with_a_widened_scale`
        // above, but asserting the resolution function directly: an
        // 11-cell strip (dot budget 22) covering a 90-column file must
        // widen to 10 cols/cell (5 cols/dot, 110 columns of coverage) —
        // the exact value that test widens to *explicitly*, now produced
        // *automatically*.
        let text = format!("{}x", " ".repeat(89));
        let mm = minimap_from(vec![text.as_str()], 1);
        assert_eq!(resolve_cols_per_cell(&mm, 11), 10);
    }

    #[test]
    fn resolve_cols_per_cell_caps_at_column_capacity_for_an_extremely_long_line() {
        use crate::primitives::minimap::COLUMN_CAPACITY;
        let far_past_cap = "x".repeat(COLUMN_CAPACITY * 4);
        let exactly_at_cap = "x".repeat(COLUMN_CAPACITY);
        let mm_far = minimap_from(vec![far_past_cap.as_str()], 1);
        let mm_cap = minimap_from(vec![exactly_at_cap.as_str()], 1);
        assert_eq!(
            resolve_cols_per_cell(&mm_far, 12),
            resolve_cols_per_cell(&mm_cap, 12),
            "a line far past COLUMN_CAPACITY must resolve the same scale as a line \
             exactly at the cap -- VS Code parity, not unbounded widening"
        );
    }

    #[test]
    fn resolve_cols_per_cell_on_an_empty_buffer_does_not_divide_by_zero() {
        let mm = minimap_from(vec![], 0);
        assert_eq!(resolve_cols_per_cell(&mm, 12), COLS_PER_CELL);
    }

    /// Distinguishes the adaptive default scale from the fixed scale: a
    /// 24-column line and a 300-column line — both at or past the old fixed
    /// default's 24-column window — painted identically saturated runs under
    /// the fixed scale. The fixed scale only passed because the 1-char line
    /// sits *inside* the window; this fixture straddles the old window's right
    /// edge instead, so it only passes once the scale itself adapts. Both lines
    /// live in the same `Minimap` (different row groups so their dots never
    /// interact) — the resolved scale is a per-*file* property, so both rows
    /// share the one scale the file's widest line demands.
    #[test]
    fn default_scale_distinguishes_a_24_column_line_from_a_300_column_line_in_the_same_file() {
        let width_cells = 12;
        let short_line = "x".repeat(24); // exactly the old fixed-default window width
        let long_line = "x".repeat(300); // far past it, and past COLUMN_CAPACITY too

        let mm = minimap_from(
            vec![
                short_line.as_str(),
                "",
                "",
                "",
                long_line.as_str(),
                "",
                "",
                "",
            ],
            8,
        );

        let area = Rect::new(0, 0, width_cells as u16, 2);
        let mut buf = Buffer::empty(area);
        draw_minimap(&mut buf, area, &mm, &Theme::default());

        let last_set = |y: u16| -> Option<u16> {
            (0..width_cells as u16)
                .rev()
                .find(|&x| cell_char(&buf, x, y) != '\u{2800}')
        };
        let short_last = last_set(0);
        let long_last = last_set(1);

        assert_ne!(
            short_last, long_last,
            "a 24-column line and a 300-column line in the same file must not paint \
             identical-extent runs under the default (adaptive) scale -- got \
             short={short_last:?} long={long_last:?}"
        );
        assert_eq!(
            long_last,
            Some(width_cells as u16 - 1),
            "the 300-column line, clipped at COLUMN_CAPACITY like VS Code's own minimap, \
             should still saturate the strip's right edge"
        );
    }

    /// Direct regression for the exact scenario: `draw_minimap`'s *default*
    /// entry point (no explicit `cols_per_cell`) reaching indent 40 and 80 on
    /// a 90-column buffer, with no host-side widening required. Companion to
    /// `cell_color_survives_at_indent_40_and_80_with_a_widened_scale`, which
    /// pins the same buffer's colour behaviour under the *explicit* parameter
    /// mechanism instead.
    #[test]
    fn draw_minimap_default_scale_reaches_deep_indents_on_a_wide_buffer() {
        let mut line = vec![' '; 90];
        line[40] = 'a';
        line[80] = 'b';
        let text: String = line.into_iter().collect();

        let mut mm = minimap_from(vec![&text], 8);
        let red = Color::rgb(255, 0, 0);
        let blue = Color::rgb(0, 0, 255);
        mm.syntax_spans.push(MinimapSpan {
            line_idx: 0,
            start_col: 40,
            end_col: 50,
            color: red,
        });
        mm.syntax_spans.push(MinimapSpan {
            line_idx: 0,
            start_col: 80,
            end_col: 90,
            color: blue,
        });

        let area = Rect::new(0, 0, 11, 1);
        let mut buf = Buffer::empty(area);
        let layout = draw_minimap(&mut buf, area, &mm, &Theme::default());

        assert_eq!(
            layout.cols_per_cell, 10,
            "layout must report back the scale draw_minimap actually resolved and used"
        );
        assert_eq!(
            buf[(4u16, 0u16)].fg,
            ratatui_color(red),
            "the default scale must reach indent 40 without any explicit widening"
        );
        assert_eq!(
            buf[(8u16, 0u16)].fg,
            ratatui_color(blue),
            "the default scale must reach indent 80 without any explicit widening"
        );
    }

    /// [`tui_minimap_layout`] (the no-paint layout query `Backend::minimap_layout`
    /// uses) must resolve the exact same scale `draw_minimap` will paint
    /// with — a host reading `cols_per_cell` back before a paint has run
    /// (e.g. to hit-test a click, or to build a colour-aggregation grid
    /// ahead of painting, as `examples/common/minimap_app.rs` does) must
    /// not see a different answer than after painting.
    #[test]
    fn tui_minimap_layout_resolves_the_same_scale_draw_minimap_paints_with() {
        let mut line = vec![' '; 90];
        line[40] = 'a';
        let text: String = line.into_iter().collect();
        let mm = minimap_from(vec![&text], 8);
        let area = Rect::new(0, 0, 11, 1);

        let no_paint_layout = tui_minimap_layout(&mm, area);

        let mut buf = Buffer::empty(area);
        let painted_layout = draw_minimap(&mut buf, area, &mm, &Theme::default());

        assert_eq!(no_paint_layout.cols_per_cell, painted_layout.cols_per_cell);
        assert_eq!(painted_layout.cols_per_cell, 10);
    }

    /// [`draw_minimap_with_sizing`] under [`MinimapSizing::RepeatToFill`]
    /// must repeat-paint a short file's one sampled row across every cell
    /// row [`tui_minimap_layout_with_sizing`] assigns it — a 4-line file in
    /// a 20-row-tall strip resolves `k = 20` (one braille dot-group repeated
    /// 20 times), so every one of the 20 cell rows must carry the identical
    /// braille glyph, with no blank gap anywhere in the strip.
    #[test]
    fn draw_minimap_with_sizing_repeat_to_fill_paints_identical_content_across_every_repeated_row()
    {
        let mm = minimap_from(vec!["X   ", "  X ", " X  ", "   X"], 4);
        let area = Rect::new(0, 0, 2, 20);
        let mut buf = Buffer::empty(area);

        let layout = draw_minimap_with_sizing(
            &mut buf,
            area,
            &mm,
            &Theme::default(),
            2,
            MinimapSizing::RepeatToFill(1.0),
        );

        assert_eq!(
            layout.visible_lines.len(),
            1,
            "a 4-line file samples to exactly one braille row group"
        );
        assert_eq!(
            layout.visible_lines[0].bounds.height, 20.0,
            "the repeat factor must stretch the single row across the whole 20-row strip"
        );

        let expected_left = cell_char(&buf, 0, 0);
        let expected_right = cell_char(&buf, 1, 0);
        assert_ne!(
            expected_left, '\u{2800}',
            "the sampled row must actually paint content, not a blank braille glyph"
        );
        for y in 0..20u16 {
            assert_eq!(
                cell_char(&buf, 0, y),
                expected_left,
                "row {y} must repeat the same left-cell glyph as every other repeated row"
            );
            assert_eq!(
                cell_char(&buf, 1, y),
                expected_right,
                "row {y} must repeat the same right-cell glyph as every other repeated row"
            );
        }
    }
}