1use crate::buffer::Buffer;
2use crate::geom::{Pos, Rect};
3use crate::node::NodeId;
4use crate::style::{Border, Style, TextAlign, TextTruncate, TextWrap};
5
6pub struct RenderCx<'a> {
8 pub rect: Rect,
9 pub buffer: &'a mut Buffer,
10 pub cursor: Pos,
11 pub style: Style,
12 pub focused_id: Option<NodeId>,
14 pub clip_rect: Option<Rect>,
16 pub wrap: TextWrap,
18 pub truncate: TextTruncate,
20 pub align: TextAlign,
22}
23
24impl<'a> RenderCx<'a> {
25 pub fn new(rect: Rect, buffer: &'a mut Buffer, style: Style) -> Self {
26 let cursor = Pos {
27 x: rect.x,
28 y: rect.y,
29 };
30 Self {
31 rect,
32 buffer,
33 cursor,
34 style,
35 focused_id: None,
36 clip_rect: None,
37 wrap: TextWrap::None,
38 truncate: TextTruncate::None,
39 align: TextAlign::Left,
40 }
41 }
42
43 pub fn is_focused(&self, id: NodeId) -> bool {
45 self.focused_id == Some(id)
46 }
47
48 pub fn align_offset(&self, text_width: u16) -> u16 {
50 let clip = self.effective_clip();
51 let available = clip.x.saturating_add(clip.width).saturating_sub(self.rect.x);
52 match self.align {
53 TextAlign::Left => 0,
54 TextAlign::Center => available.saturating_sub(text_width) / 2,
55 TextAlign::Right => available.saturating_sub(text_width),
56 }
57 }
58
59 fn effective_clip(&self) -> Rect {
61 self.clip_rect.unwrap_or(self.rect)
62 }
63
64 fn wrap_clip(&self) -> Rect {
66 let mut clip = self.effective_clip();
67 if self.wrap != TextWrap::None {
68 clip.height = u16::MAX.saturating_sub(clip.y); }
70 clip
71 }
72
73 pub fn text(&mut self, text: impl AsRef<str>) {
75 let text = text.as_ref();
76 let clip = self.effective_clip();
77 let available = clip.x.saturating_add(clip.width).saturating_sub(self.cursor.x);
78
79 if self.wrap == TextWrap::None && self.truncate == TextTruncate::Ellipsis {
80 let total: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
81 if total > available && available >= 1 {
82 let mut used: u16 = 0;
83 let mut bytes = 0;
84 for ch in text.chars() {
85 let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
86 if used + w > available.saturating_sub(1) { break; }
87 used += w;
88 bytes += ch.len_utf8();
89 }
90 if bytes > 0 {
91 self.buffer.write_text(self.cursor, clip, &text[..bytes], &self.style);
92 self.cursor.x = self.cursor.x.saturating_add(used);
93 }
94 self.buffer.write_text(self.cursor, clip, "…", &self.style);
95 self.cursor.x = self.cursor.x.saturating_add(1);
96 return;
97 }
98 }
99
100 self.buffer.write_text(self.cursor, clip, text, &self.style);
101 let width: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
102 self.cursor.x = self.cursor.x.saturating_add(width);
103 }
104
105 pub fn line(&mut self, text: impl AsRef<str>) {
107 let text = text.as_ref();
108 let clip = self.wrap_clip();
109 let available = if clip.width >= self.cursor.x.saturating_sub(clip.x) {
110 clip.width.saturating_sub(self.cursor.x.saturating_sub(clip.x))
111 } else {
112 0
113 };
114
115 let saved_clip = self.clip_rect;
116 if self.wrap != TextWrap::None {
117 self.clip_rect = Some(clip); }
119
120 match self.wrap {
121 TextWrap::None => {
122 let tw: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
123 self.cursor.x = self.rect.x.saturating_add(self.align_offset(tw));
124 match self.truncate {
125 TextTruncate::None => {
126 self.text(text);
127 }
128 TextTruncate::Ellipsis => {
129 let total = tw;
130 if total > available && available >= 1 {
131 let mut used: u16 = 0;
132 let mut bytes = 0;
133 for ch in text.chars() {
134 let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
135 if used + w > available.saturating_sub(1) { break; }
136 used += w;
137 bytes += ch.len_utf8();
138 }
139 if bytes > 0 {
140 self.text(&text[..bytes]);
141 }
142 self.text("…");
143 } else {
144 self.text(text);
145 }
146 }
147 }
148 self.cursor.y = self.cursor.y.saturating_add(1);
149 self.cursor.x = self.rect.x;
150 }
151 TextWrap::Char => {
152 let mut remaining = text;
153 loop {
154 let line_widths: Vec<(usize, u16)> = remaining
155 .char_indices()
156 .map(|(i, c)| (i, unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16))
157 .filter(|&(_, w)| w > 0)
158 .collect();
159
160 let mut used: u16 = 0;
161 let mut bytes = 0;
162 for &(byte_idx, w) in &line_widths {
163 if used + w > available { break; }
164 used += w;
165 bytes = byte_idx + remaining[byte_idx..].chars().next().map(|c| c.len_utf8()).unwrap_or(0);
166 }
167 if bytes == 0 { break; }
168 let line_text = &remaining[..bytes];
169 let lw: u16 = line_text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
170 self.cursor.x = self.rect.x.saturating_add(self.align_offset(lw));
171 self.text(line_text);
172 remaining = &remaining[bytes..];
173 if remaining.is_empty() { break; }
174 self.cursor.y = self.cursor.y.saturating_add(1);
175 self.cursor.x = self.rect.x;
176 }
177 if text.is_empty() {
178 self.cursor.y = self.cursor.y.saturating_add(1);
179 self.cursor.x = self.rect.x;
180 }
181 }
182 TextWrap::Word => {
183 let mut remaining = text;
184 loop {
185 let (word, rest) = next_word(remaining);
188 if word.is_empty() { break; }
189
190 let ww: u16 = word.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
191
192 let cur_w: u16 = self.cursor.x.saturating_sub(self.rect.x);
194 if cur_w + ww > available && cur_w > 0 {
195 self.cursor.y = self.cursor.y.saturating_add(1);
196 self.cursor.x = self.rect.x;
197 }
198 self.text(word);
199 remaining = rest;
200 }
201 if text.is_empty() {
202 self.cursor.y = self.cursor.y.saturating_add(1);
203 self.cursor.x = self.rect.x;
204 }
205 self.cursor.y = self.cursor.y.saturating_add(1);
207 self.cursor.x = self.rect.x;
208 }
209 }
210
211 self.clip_rect = saved_clip;
212 }
213
214 pub fn set_style(&mut self, style: Style) {
216 self.style = style;
217 }
218
219 pub fn draw_border(&mut self, border: Border) {
221 let clip = self.effective_clip();
222 self.buffer.draw_border(clip, border, &self.style);
223 }
224}
225
226fn next_word(text: &str) -> (&str, &str) {
229 let mut chars = text.char_indices().peekable();
230
231 while let Some(&(_i, c)) = chars.peek() {
233 if c == ' ' { chars.next(); } else { break; }
234 }
235
236 let start = chars.peek().map(|&(i, _)| i).unwrap_or(text.len());
237 if start >= text.len() { return ("", ""); }
238
239 if let Some(&(_, c)) = chars.peek() {
241 let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
242 if w > 1 {
243 let end = start + c.len_utf8();
244 return (&text[start..end], &text[end..]);
245 }
246 }
247
248 let mut end = text.len();
250 for (i, c) in text[start..].char_indices() {
251 if c == ' ' {
252 end = start + i + c.len_utf8(); break;
254 }
255 let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
256 if w > 1 {
257 if i == 0 {
258 end = start + c.len_utf8();
259 } else {
260 end = start + i;
261 }
262 break;
263 }
264 }
265 (&text[start..end], &text[end..])
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::buffer::Buffer;
272 use crate::geom::{Rect, Size};
273
274 #[test]
275 fn test_word_wrap_simple() {
276 let mut buf = Buffer::new(Size { width: 20, height: 3 });
277 let mut cx = RenderCx::new(Rect { x: 0, y: 0, width: 8, height: 3 }, &mut buf, Style::default());
278 cx.wrap = TextWrap::Word;
279 cx.line("hello world");
280 assert_eq!(&buf.cells[0].symbol, "h");
282 assert_eq!(&buf.cells[20].symbol, "w");
284 }
285
286 #[test]
287 fn test_next_word_english() {
288 let (w, r) = next_word("hello world");
289 assert_eq!(w, "hello ");
290 assert_eq!(r, "world");
291 }
292
293 #[test]
294 fn test_next_word_cjk() {
295 let (w, r) = next_word("你好世界");
296 assert_eq!(w, "你");
297 assert_eq!(r, "好世界");
298 }
299
300 #[test]
301 fn test_word_wrap_long() {
302 let mut buf = Buffer::new(Size { width: 20, height: 2 });
303 let mut cx = RenderCx::new(Rect { x: 0, y: 0, width: 5, height: 2 }, &mut buf, Style::default());
304 cx.wrap = TextWrap::Word;
305 cx.line("superlongword");
306 assert_eq!(&buf.cells[0].symbol, "s");
308 }
309}