uncurses/cell/mod.rs
1//! Terminal cell values and grapheme segmentation.
2//!
3//! This module defines [`Cell`], the value stored by buffers and surfaces.
4//! A cell combines grapheme content, visual style, and a structural role
5//! that describes whether it is a narrow cell, a wide-cell primary, or the
6//! continuation column for a wide primary.
7//!
8//! ## Cell value type
9//!
10//! A [`Cell`] stores three pieces of state:
11//!
12//! - `content`: the grapheme cluster to display. Continuation cells have no
13//! content of their own.
14//! - `style`: colors, attributes, underline data, and link metadata applied
15//! to the cell.
16//! - [`Kind`]: the structural role that determines the cell's column
17//! footprint.
18//!
19//! The cell's display width is derived from its [`Kind`]: narrow cells
20//! occupy one column, wide primaries occupy two columns, and continuation
21//! placeholders report width `0` because their column is owned by the wide
22//! primary to the left.
23//!
24//! ## Construction
25//!
26//! Construct cells with [`Cell::narrow`] for one-column graphemes and
27//! [`Cell::wide`] for two-column graphemes. Both constructors use the
28//! default [`Style`]. Use [`Cell::style()`] to attach a
29//! style after construction.
30//!
31//! [`Cell::continuation`] creates the internal placeholder used for the
32//! second column of a wide grapheme. Most callers should not write
33//! continuations directly; writing a wide cell through
34//! [`Buffer::set`](crate::buffer::Buffer::set) or
35//! [`SurfaceMut::set_cell`](crate::buffer::SurfaceMut::set_cell) creates the
36//! placeholder automatically.
37//!
38//! ```rust,ignore
39//! use uncurses::cell::Cell;
40//!
41//! let a = Cell::narrow("a");
42//! assert_eq!(a.width(), 1);
43//!
44//! let wide = Cell::wide("中");
45//! assert_eq!(wide.width(), 2);
46//! ```
47//!
48//! ## Wide cells
49//!
50//! A two-column grapheme occupies two adjacent grid columns. The left column
51//! stores the wide primary and the right column stores a continuation
52//! placeholder:
53//!
54//! ```text
55//! col: 0 1 2
56//! ┌───────┬───────┬───┐
57//! row 0 │ 中 │ cont. │ A │
58//! └───────┴───────┴───┘
59//! width=2 width=0 width=1
60//! ```
61//!
62//! Continuations are considered blank by [`Cell::is_blank`] because they do
63//! not render independent content. They exist so row storage can preserve
64//! the one-`Cell`-per-column layout while still representing wide graphemes
65//! accurately.
66
67use compact_str::CompactString;
68
69use crate::style::Style;
70
71/// Structural role of a cell within a terminal grid.
72///
73/// `Kind` encodes the width relationship between adjacent cells. A wide
74/// grapheme is stored as a [`Kind::Wide`] primary followed immediately by a
75/// [`Kind::Continuation`] placeholder in the column to the right. Width is
76/// derived from the kind by [`Cell::width`].
77#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
78pub enum Kind {
79 /// A cell that occupies exactly one terminal column.
80 Narrow,
81 /// The primary cell of a two-column grapheme.
82 ///
83 /// The following column should contain [`Kind::Continuation`] when the
84 /// cell is stored in a surface.
85 Wide,
86 /// The right-column placeholder for a [`Kind::Wide`] primary.
87 ///
88 /// A continuation carries no content of its own and reports width `0`.
89 Continuation,
90}
91
92/// A single terminal-grid cell.
93///
94/// `Cell` is the value stored in buffers and surfaces. It contains the
95/// grapheme content for a column, the style applied to that content, and a
96/// [`Kind`] that determines whether the cell is a one-column value, a
97/// two-column wide primary, or a continuation placeholder.
98///
99/// Use [`Cell::narrow`] and [`Cell::wide`] for normal construction. Use
100/// [`Cell::BLANK`] for an empty styled-as-default space. Continuations are
101/// normally produced by the surface write path rather than by application
102/// code.
103#[derive(Debug, Clone)]
104pub struct Cell {
105 /// The grapheme cluster content. Empty string for a wide-cell
106 /// continuation placeholder.
107 content: CompactString,
108 /// Visual style: colors, attributes, underline, and any attached
109 /// hyperlink. The link inside `style` is reference-counted so a
110 /// run of identically-linked cells shares a single allocation
111 /// without per-cell deep clones.
112 pub style: Style,
113 /// Structural kind: narrow, wide primary, or wide continuation.
114 kind: Kind,
115}
116
117impl PartialEq for Cell {
118 #[inline]
119 fn eq(&self, other: &Self) -> bool {
120 self.kind == other.kind && self.style == other.style && self.content == other.content
121 }
122}
123
124impl Eq for Cell {}
125
126impl Default for Cell {
127 fn default() -> Self {
128 Self::BLANK
129 }
130}
131
132impl Cell {
133 /// A blank narrow cell with default style.
134 ///
135 /// The blank cell stores a single space (`" "`), uses
136 /// [`Style::EMPTY`], and has [`Kind::Narrow`].
137 ///
138 /// # Returns
139 ///
140 /// This is a constant value, so use it directly wherever a default blank
141 /// cell is needed.
142 ///
143 /// # Panics
144 ///
145 /// Never panics.
146 ///
147 /// # Usage notes
148 ///
149 /// Clearing and newly allocated buffers use `BLANK`. Clone it when an
150 /// owned value is required.
151 pub const BLANK: Cell = Cell {
152 content: CompactString::const_new(" "),
153 style: Style::EMPTY,
154 kind: Kind::Narrow,
155 };
156
157 /// Create a single-column cell with the given grapheme-cluster
158 /// `content` and default style.
159 ///
160 /// # Parameters
161 ///
162 /// - `content`: grapheme content to store in the cell.
163 ///
164 /// # Returns
165 ///
166 /// A [`Kind::Narrow`] cell with width `1` and default style.
167 ///
168 /// # Panics
169 ///
170 /// Never panics.
171 ///
172 /// # Usage notes
173 ///
174 /// This constructor does not validate display width. Call it only for
175 /// content that should occupy one terminal column; use [`Cell::wide`] for
176 /// two-column graphemes.
177 pub fn narrow(content: impl Into<CompactString>) -> Self {
178 Cell {
179 content: content.into(),
180 style: Style::default(),
181 kind: Kind::Narrow,
182 }
183 }
184
185 /// Create the primary cell of a two-column grapheme.
186 ///
187 /// # Parameters
188 ///
189 /// - `content`: grapheme content to store in the wide primary.
190 ///
191 /// # Returns
192 ///
193 /// A [`Kind::Wide`] cell with width `2` and default style.
194 ///
195 /// # Panics
196 ///
197 /// Never panics.
198 ///
199 /// # Usage notes
200 ///
201 /// When a wide cell is written through the buffer/surface write path, the
202 /// slot at `column + 1` is filled with a [`Kind::Continuation`]
203 /// placeholder automatically. If there is no room for that placeholder
204 /// at the row's right edge, [`Buffer::set`](crate::buffer::Buffer::set)
205 /// stores a blank instead of half a wide grapheme.
206 pub fn wide(content: impl Into<CompactString>) -> Self {
207 Cell {
208 content: content.into(),
209 style: Style::default(),
210 kind: Kind::Wide,
211 }
212 }
213
214 /// Create a wide-character continuation placeholder.
215 ///
216 /// Continuation cells carry no content; they occupy the right
217 /// half of a [`Cell::wide`] primary at the column to their left.
218 ///
219 /// # Returns
220 ///
221 /// A [`Kind::Continuation`] cell with empty content, default style, and
222 /// width `0`.
223 ///
224 /// # Panics
225 ///
226 /// Never panics.
227 ///
228 /// # Usage notes
229 ///
230 /// Most callers should not construct continuations directly. Prefer
231 /// writing a [`Cell::wide`] through a surface so the primary and
232 /// continuation remain adjacent.
233 pub fn continuation() -> Self {
234 Cell {
235 content: CompactString::default(),
236 style: Style::default(),
237 kind: Kind::Continuation,
238 }
239 }
240
241 /// Return the cell's structural role.
242 ///
243 /// # Returns
244 ///
245 /// [`Kind::Narrow`], [`Kind::Wide`], or [`Kind::Continuation`].
246 ///
247 /// # Panics
248 ///
249 /// Never panics.
250 ///
251 /// # Usage notes
252 ///
253 /// Use this when matching all roles explicitly. Predicate helpers such
254 /// as [`Cell::is_wide`] are clearer for single-role checks.
255 #[inline]
256 pub fn kind(&self) -> Kind {
257 self.kind
258 }
259
260 /// Test whether this is a single-column cell.
261 ///
262 /// # Returns
263 ///
264 /// `true` when [`Cell::kind`] is [`Kind::Narrow`].
265 ///
266 /// # Panics
267 ///
268 /// Never panics.
269 ///
270 /// # Usage notes
271 ///
272 /// Blank cells are narrow; continuation cells are not.
273 #[inline]
274 pub fn is_narrow(&self) -> bool {
275 matches!(self.kind, Kind::Narrow)
276 }
277
278 /// Test whether this is the primary of a two-column grapheme.
279 ///
280 /// # Returns
281 ///
282 /// `true` when [`Cell::kind`] is [`Kind::Wide`].
283 ///
284 /// # Panics
285 ///
286 /// Never panics.
287 ///
288 /// # Usage notes
289 ///
290 /// In a well-formed surface, a wide cell is followed immediately by a
291 /// continuation placeholder.
292 #[inline]
293 pub fn is_wide(&self) -> bool {
294 matches!(self.kind, Kind::Wide)
295 }
296
297 /// Test whether this is a wide-character continuation placeholder.
298 ///
299 /// # Returns
300 ///
301 /// `true` when [`Cell::kind`] is [`Kind::Continuation`].
302 ///
303 /// # Panics
304 ///
305 /// Never panics.
306 ///
307 /// # Usage notes
308 ///
309 /// Continuations have width `0`, no content, and are considered blank.
310 #[inline]
311 pub fn is_continuation(&self) -> bool {
312 matches!(self.kind, Kind::Continuation)
313 }
314
315 /// Test whether this cell renders as blank space.
316 ///
317 /// # Returns
318 ///
319 /// `true` when the content is empty, the content is a single space, or
320 /// the cell is a continuation placeholder.
321 ///
322 /// # Panics
323 ///
324 /// Never panics.
325 ///
326 /// # Usage notes
327 ///
328 /// Style is not considered. A styled space still counts as blank because
329 /// this method answers whether the cell has independent textual content.
330 pub fn is_blank(&self) -> bool {
331 self.content.is_empty() || self.content == " " || self.is_continuation()
332 }
333
334 /// Return the cell's grapheme-cluster content.
335 ///
336 /// # Returns
337 ///
338 /// The stored content as `&str`. Continuation cells return an empty
339 /// string.
340 ///
341 /// # Panics
342 ///
343 /// Never panics.
344 ///
345 /// # Usage notes
346 ///
347 /// This returns the stored content exactly; it does not derive or append
348 /// the neighboring continuation for wide cells.
349 #[inline]
350 pub fn content(&self) -> &str {
351 self.content.as_str()
352 }
353
354 /// Column footprint of this cell on the grid.
355 ///
356 /// - `Narrow` → 1
357 /// - `Wide` → 2
358 /// - `Continuation` → 0 (the second slot of a wide primary)
359 ///
360 /// # Returns
361 ///
362 /// The number of terminal columns owned by this cell's role.
363 ///
364 /// # Panics
365 ///
366 /// Never panics.
367 ///
368 /// # Usage notes
369 ///
370 /// Row walkers generally advance by `cell.width().max(1)` after handling
371 /// continuations, so they make progress even when encountering a
372 /// continuation placeholder.
373 #[inline]
374 pub fn width(&self) -> u8 {
375 match self.kind {
376 Kind::Narrow => 1,
377 Kind::Wide => 2,
378 Kind::Continuation => 0,
379 }
380 }
381
382 /// Return this cell with a replacement style.
383 ///
384 /// # Parameters
385 ///
386 /// - `style`: style to store on the returned cell.
387 ///
388 /// # Returns
389 ///
390 /// `self` with its `style` field replaced by `style`.
391 ///
392 /// # Panics
393 ///
394 /// Never panics.
395 ///
396 /// # Usage notes
397 ///
398 /// This builder-style method preserves content and [`Kind`]. Styling a
399 /// continuation is possible, but continuations do not render independent
400 /// content.
401 pub fn style(mut self, style: impl Into<Style>) -> Self {
402 self.style = style.into();
403 self
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn test_blank_cell() {
413 let c = Cell::BLANK;
414 assert!(c.is_narrow());
415 assert_eq!(c.width(), 1);
416 assert!(c.is_blank());
417 assert!(c.style.is_empty());
418 }
419
420 #[test]
421 fn test_narrow_cell() {
422 let c = Cell::narrow("A");
423 assert_eq!(c.content(), "A");
424 assert!(c.is_narrow());
425 assert_eq!(c.width(), 1);
426 }
427
428 #[test]
429 fn test_wide_cell() {
430 let c = Cell::wide("中");
431 assert_eq!(c.content(), "中");
432 assert!(c.is_wide());
433 assert_eq!(c.width(), 2);
434 }
435
436 #[test]
437 fn test_continuation_cell() {
438 let c = Cell::continuation();
439 assert!(c.is_continuation());
440 assert_eq!(c.width(), 0);
441 }
442
443 #[test]
444 fn test_cell_with_style() {
445 let c = Cell::narrow("x").style(Style::default().bold());
446 assert!(c.style.attrs.contains(crate::style::AttrFlags::BOLD));
447 }
448}