uncurses/buffer/text_buffer.rs
1//! A width-aware cell buffer that paints text and serializes to escapes.
2//!
3//! [`TextBuffer`] is a [`Buffer`] plus a text-width policy. The policy is what
4//! [`TextSurface`] needs to lay out grapheme clusters into cells, so a
5//! `TextBuffer` implements [`TextSurface`] and can be painted with
6//! [`set_str`](TextSurface::set_str) directly, unlike a bare [`Buffer`].
7//!
8//! Because it is a [`Surface`], a `TextBuffer` also gets the
9//! [`Encode`](crate::text::Encode) trait for free, so you can paint a frame and
10//! serialize it to escape sequences with
11//! [`encode`](crate::text::Encode::encode) /
12//! [`encode_with`](crate::text::Encode::encode_with) or render it into a
13//! string with [`display`](crate::text::Encode::display). This makes it the
14//! tool for one-shot and append-style output: paint a full frame, encode it,
15//! and write the bytes wherever you like, with no diffing renderer and no
16//! terminal session. For in-place repainting of a live terminal across frames,
17//! reach for [`Screen`](crate::screen::Screen) instead, whose diffing renderer
18//! tracks the terminal and emits only the changed bytes.
19//!
20//! ```
21//! use uncurses::buffer::TextBuffer;
22//! use uncurses::style::Style;
23//! use uncurses::text::{Encode, TextSurface};
24//!
25//! let mut frame = TextBuffer::new(12, 1);
26//! frame.set_str((0, 0), "hello", Style::new().bold());
27//! let bytes = frame.display().to_string();
28//! assert!(bytes.contains("hello"));
29//! ```
30
31use crate::buffer::{Bounded, Buffer, Surface, SurfaceMut};
32use crate::cell::Cell;
33use crate::layout::{Position, Rect};
34use crate::text::{TextSurface, WidthMode};
35
36/// A [`Buffer`] paired with a text-width policy.
37///
38/// Construct one with [`new`](Self::new), choose the width policy with
39/// [`with_width_mode`](Self::with_width_mode) /
40/// [`with_eaw_wide`](Self::with_eaw_wide), paint with the
41/// [`TextSurface`]/[`SurfaceMut`] methods, then serialize with the
42/// [`Encode`](crate::text::Encode) trait.
43#[derive(Debug, Clone)]
44pub struct TextBuffer {
45 buffer: Buffer,
46 width_mode: WidthMode,
47 eaw_wide: bool,
48}
49
50impl TextBuffer {
51 /// Create a `width` by `height` text buffer of blank cells.
52 ///
53 /// The width policy defaults to [`WidthMode::Wc`] with East-Asian
54 /// Ambiguous characters measured as one cell.
55 pub fn new(width: u16, height: u16) -> Self {
56 Self {
57 buffer: Buffer::new(width, height),
58 width_mode: WidthMode::default(),
59 eaw_wide: false,
60 }
61 }
62
63 /// Set the grapheme-cluster width policy and return the updated buffer.
64 pub fn with_width_mode(mut self, mode: WidthMode) -> Self {
65 self.width_mode = mode;
66 self
67 }
68
69 /// Set the East-Asian Ambiguous policy and return the updated buffer.
70 ///
71 /// When `true`, code points whose East-Asian-Width property is
72 /// `Ambiguous` are measured as two cells instead of one.
73 pub fn with_eaw_wide(mut self, eaw_wide: bool) -> Self {
74 self.eaw_wide = eaw_wide;
75 self
76 }
77
78 /// Set the grapheme-cluster width policy in place.
79 pub fn set_width_mode(&mut self, mode: WidthMode) {
80 self.width_mode = mode;
81 }
82
83 /// Set the East-Asian Ambiguous policy in place.
84 pub fn set_eaw_wide(&mut self, eaw_wide: bool) {
85 self.eaw_wide = eaw_wide;
86 }
87
88 /// The width in cells.
89 pub fn width(&self) -> u16 {
90 self.buffer.width()
91 }
92
93 /// The height in cells.
94 pub fn height(&self) -> u16 {
95 self.buffer.height()
96 }
97
98 /// Resize the buffer, preserving overlapping cells.
99 pub fn resize(&mut self, width: u16, height: u16) {
100 self.buffer.resize(width, height);
101 }
102
103 /// Borrow the underlying [`Buffer`].
104 pub fn buffer(&self) -> &Buffer {
105 &self.buffer
106 }
107
108 /// Mutably borrow the underlying [`Buffer`].
109 pub fn buffer_mut(&mut self) -> &mut Buffer {
110 &mut self.buffer
111 }
112
113 /// Consume the text buffer and return the underlying [`Buffer`].
114 pub fn into_buffer(self) -> Buffer {
115 self.buffer
116 }
117}
118
119impl Bounded for TextBuffer {
120 fn bounds(&self) -> Rect {
121 self.buffer.bounds()
122 }
123}
124
125impl Surface for TextBuffer {
126 fn cell(&self, pos: Position) -> Option<&Cell> {
127 self.buffer.cell(pos)
128 }
129}
130
131impl SurfaceMut for TextBuffer {
132 fn set_cell(&mut self, pos: Position, cell: &Cell) {
133 self.buffer.set_cell(pos, cell);
134 }
135
136 fn cell_mut(&mut self, pos: Position) -> Option<&mut Cell> {
137 self.buffer.cell_mut(pos)
138 }
139}
140
141impl TextSurface for TextBuffer {
142 fn width_mode(&self) -> WidthMode {
143 self.width_mode
144 }
145
146 fn eaw_wide(&self) -> bool {
147 self.eaw_wide
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::style::Style;
155 use crate::text::Encode;
156
157 #[test]
158 fn paints_and_encodes() {
159 let mut tb = TextBuffer::new(8, 1);
160 let end = tb.set_str((0, 0), "hi", Style::new());
161 assert_eq!(end, Position::new(2, 0));
162 assert_eq!(tb.display().to_string(), "hi");
163 }
164
165 #[test]
166 fn width_policy_affects_measurement() {
167 // A flag emoji presentation sequence: Wc measures the first scalar,
168 // Grapheme measures the whole cluster. Just assert the policy plumbs
169 // through to str_width without asserting exact terminal widths.
170 let narrow = TextBuffer::new(4, 1);
171 let wide = TextBuffer::new(4, 1).with_eaw_wide(true);
172 assert!(wide.str_width("\u{2764}") >= narrow.str_width("\u{2764}"));
173 }
174
175 #[test]
176 fn into_buffer_roundtrips() {
177 let mut tb = TextBuffer::new(3, 1);
178 tb.set_str((0, 0), "ab", Style::new());
179 let buf = tb.into_buffer();
180 assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "a");
181 }
182}