Skip to main content

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}