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
//! Compact mode rendering for TOC using Canvas with Braille markers for thin lines.
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Color,
symbols::Marker,
widgets::{
canvas::{Canvas, Line},
Widget,
},
};
use crate::widgets::markdown_widget::extensions::toc::Toc;
use crate::widgets::markdown_widget::state::toc_state::TocEntry;
use super::calculate_line_width::calculate_line_width;
impl<'a> Toc<'a> {
/// Render the TOC in compact mode using Canvas with Braille markers.
///
/// Braille gives 2x4 dots per cell for thin lines with sub-pixel positioning.
/// `line_spacing` controls the spacing between lines in dot units.
/// Uses two-pass rendering to ensure active line color is correct.
pub(crate) fn render_compact(&self, area: Rect, buf: &mut Buffer) {
if area.height == 0 || area.width == 0 {
return;
}
// Fill entire area with background first (including under border)
fill_background(buf, area, self.config.background_style);
// Draw border on top of background
let content_area = if self.config.show_border {
self.render_border(area, buf)
} else {
area
};
if content_area.width == 0 || content_area.height == 0 {
return;
}
let entries = &self.toc_state.entries;
if entries.is_empty() {
return;
}
// Get active index from TocState (hovered_entry when hovered, otherwise None for now)
// Note: Active index tracking may need to be added to TocState
let active_index = self.toc_state.hovered_entry;
render_compact_lines(
entries,
content_area,
buf,
self.config.line_spacing,
self.config.line_style.fg.unwrap_or(Color::Gray),
self.config.active_line_style.fg.unwrap_or(Color::Yellow),
active_index,
);
}
}
/// Fill an area with background style.
fn fill_background(buf: &mut Buffer, area: Rect, style: ratatui::style::Style) {
for y in area.y..area.y + area.height {
for x in area.x..area.x + area.width {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_char(' ').set_style(style);
}
}
}
}
/// Render compact lines using Canvas with two-pass rendering.
fn render_compact_lines(
entries: &[TocEntry],
content_area: Rect,
buf: &mut Buffer,
line_spacing: u8,
normal_color: Color,
active_color: Color,
active_index: Option<usize>,
) {
let spacing = line_spacing.max(1) as f64;
// Canvas coordinates: x = 0..width*2, y = 0..height*4 (Braille: 2x4 dots per cell)
let canvas_width = (content_area.width as f64) * 2.0;
let canvas_height = (content_area.height as f64) * 4.0;
// Two-pass rendering: non-active lines first, then active line
let canvas = Canvas::default()
.marker(Marker::Braille)
.x_bounds([0.0, canvas_width])
.y_bounds([0.0, canvas_height])
.paint(move |ctx| {
// Pass 1: Draw all non-active lines
for (idx, entry) in entries.iter().enumerate() {
if Some(idx) == active_index {
continue;
}
let pixel_y = canvas_height - (idx as f64 * spacing);
if pixel_y <= 0.0 {
break;
}
let line_width = calculate_line_width(canvas_width, entry.level);
let x_start = canvas_width - line_width;
ctx.draw(&Line {
x1: x_start,
y1: pixel_y,
x2: canvas_width,
y2: pixel_y,
color: normal_color,
});
}
// Pass 2: Draw active line last so it wins shared cells
if let Some(active_idx) = active_index {
if let Some(entry) = entries.get(active_idx) {
let pixel_y = canvas_height - (active_idx as f64 * spacing);
if pixel_y > 0.0 {
let line_width = calculate_line_width(canvas_width, entry.level);
let x_start = canvas_width - line_width;
ctx.draw(&Line {
x1: x_start,
y1: pixel_y,
x2: canvas_width,
y2: pixel_y,
color: active_color,
});
}
}
}
});
canvas.render(content_area, buf);
}