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
// (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) {
let mut i = pos;
for ch in s.chars() {
if i >= self.data.len() {
break;
}
self.data[i] = Cell::new(ch, 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 {
let mut chars = s.chars();
let start_pos = pos;
// 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
}
if pos < self.data.len() {
self.data[pos] = Cell::new(shortcut_ch, shortcut_attr);
pos += 1;
}
}
} else {
self.data[pos] = Cell::new(ch, normal_attr);
pos += 1;
}
}
pos - start_pos // Return number of characters 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, '!');
}
}