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
// (C) 2025 - Enzo Lombardi
//! Drawing primitives - Cell and DrawBuffer types for efficient line-based rendering.
use super::palette::Attr;
/// A single character cell with attributes
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub ch: char,
pub attr: Attr,
}
impl Cell {
pub const fn new(ch: char, attr: Attr) -> Self {
Self { ch, attr }
}
}
/// Buffer for efficient line-based drawing
pub struct DrawBuffer {
pub data: Vec<Cell>,
}
impl DrawBuffer {
/// Create a new draw buffer with the given width
pub fn new(width: usize) -> Self {
Self {
data: vec![Cell::new(' ', Attr::from_u8(0x07)); width],
}
}
/// Fill a range with a single character and attribute
pub fn move_char(&mut self, pos: usize, ch: char, attr: Attr, count: usize) {
let end = (pos + count).min(self.data.len());
for i in pos..end {
self.data[i] = Cell::new(ch, attr);
}
}
/// Write a string with the given attribute
pub fn move_str(&mut self, pos: usize, s: &str, attr: Attr) {
use unicode_width::UnicodeWidthChar;
let mut i = pos;
for ch in s.chars() {
if i >= self.data.len() {
break;
}
let w = ch.width().unwrap_or(0);
// Skip zero-width characters (combining marks, variation
// selectors like U+FE0F). They must not occupy their own
// cell — otherwise the buffer width diverges from what the
// terminal actually renders, misaligning subsequent content.
if w == 0 {
continue;
}
self.data[i] = Cell::new(ch, attr);
i += 1;
// Wide characters (emojis, CJK) occupy 2 cells — fill the
// second cell with a zero-width padding space so the cursor
// advances correctly.
if w > 1 {
for _ in 1..w {
if i >= self.data.len() {
break;
}
self.data[i] = Cell::new('\0', attr);
i += 1;
}
}
}
}
/// Copy cells from another buffer
pub fn move_buf(&mut self, pos: usize, src: &[Cell], count: usize) {
let end = (pos + count).min(self.data.len()).min(pos + src.len());
self.data[pos..end].copy_from_slice(&src[..(end - pos)]);
}
/// Put a single character at a position
pub fn put_char(&mut self, pos: usize, ch: char, attr: Attr) {
if pos < self.data.len() {
self.data[pos] = Cell::new(ch, attr);
}
}
/// Change the attribute of a cell without changing its character
/// Matches Borland: TDrawBuffer::putAttribute (help.cc:109)
pub fn put_attribute(&mut self, pos: usize, attr: Attr) {
if pos < self.data.len() {
self.data[pos].attr = attr;
}
}
/// Get the length of the buffer
pub fn len(&self) -> usize {
self.data.len()
}
/// Check if the buffer is empty
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
/// Write a string with shortcut highlighting
/// Format: "~X~" highlights X with shortcut_attr, rest uses normal_attr
/// Example: "~F~ile" displays "File" with "F" highlighted
pub fn move_str_with_shortcut(&mut self, mut pos: usize, s: &str, normal_attr: Attr, shortcut_attr: Attr) -> usize {
use unicode_width::UnicodeWidthChar;
let mut chars = s.chars();
let start_pos = pos;
// Helper: write a char at `pos`, advancing by its display width
let put_wide = |data: &mut [Cell], pos: &mut usize, ch: char, attr: Attr| {
if *pos >= data.len() { return; }
let w = ch.width().unwrap_or(0);
if w == 0 { return; }
data[*pos] = Cell::new(ch, attr);
*pos += 1;
for _ in 1..w {
if *pos >= data.len() { break; }
data[*pos] = Cell::new('\0', attr);
*pos += 1;
}
};
// Parse string character by character, handling ~X~ shortcut highlighting
while let Some(ch) = chars.next() {
if pos >= self.data.len() {
break;
}
if ch == '~' {
// Read characters until closing ~ and render with shortcut color
while let Some(shortcut_ch) = chars.next() {
if shortcut_ch == '~' {
break; // Found closing tilde
}
put_wide(&mut self.data, &mut pos, shortcut_ch, shortcut_attr);
}
} else if ch.is_control() {
// Skip control characters (e.g. \x03 centering directive)
// These are directives, not renderable glyphs
continue;
} else {
put_wide(&mut self.data, &mut pos, ch, normal_attr);
}
}
pos - start_pos // Return number of cells written
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::palette::TvColor;
#[test]
fn test_draw_buffer_basic() {
let buf = DrawBuffer::new(10);
assert_eq!(buf.len(), 10);
assert!(!buf.is_empty());
}
#[test]
fn test_move_char() {
let mut buf = DrawBuffer::new(10);
let attr = Attr::new(TvColor::White, TvColor::Black);
buf.move_char(2, 'X', attr, 3);
assert_eq!(buf.data[1].ch, ' ');
assert_eq!(buf.data[2].ch, 'X');
assert_eq!(buf.data[3].ch, 'X');
assert_eq!(buf.data[4].ch, 'X');
assert_eq!(buf.data[5].ch, ' ');
}
#[test]
fn test_move_str() {
let mut buf = DrawBuffer::new(20);
let attr = Attr::new(TvColor::White, TvColor::Black);
buf.move_str(0, "Hello, World!", attr);
assert_eq!(buf.data[0].ch, 'H');
assert_eq!(buf.data[6].ch, ' ');
assert_eq!(buf.data[12].ch, '!');
}
}