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 the number of
5//! terminal columns the content occupies.
6//!
7//! ## Cell value type
8//!
9//! A [`Cell`] stores three pieces of state:
10//!
11//! - `content`: the grapheme cluster to display. Continuation cells have no
12//! content of their own.
13//! - `style`: colors, attributes, underline data, and link metadata applied
14//! to the cell.
15//! - `width`: the cell's column footprint, returned by [`Cell::width`].
16//!
17//! Width also encodes the cell's structural role. A width of `1` or more
18//! marks a primary cell that owns that many columns. A width of `0` marks a
19//! continuation placeholder, whose column is owned by the primary to its
20//! left.
21//!
22//! ## Construction
23//!
24//! Construct every cell with [`Cell::new`], passing the content and the
25//! number of columns it occupies. Measure that width with
26//! [`WidthMode::grapheme_width`](crate::text::WidthMode::grapheme_width)
27//! rather than assuming it. The constructor uses the default [`Style`]; use
28//! [`Cell::style()`] to attach a style afterwards.
29//!
30//! [`Cell::CONTINUATION`] is the placeholder that stands in the columns
31//! after a multi-column grapheme. Most callers never write one; writing a
32//! multi-column cell through
33//! [`Buffer::set`](crate::buffer::Buffer::set) or
34//! [`SurfaceMut::set_cell`](crate::buffer::SurfaceMut::set_cell) creates the
35//! placeholders automatically.
36//!
37//! ```rust,ignore
38//! use uncurses::cell::Cell;
39//!
40//! let a = Cell::new("a", 1);
41//! assert_eq!(a.width(), 1);
42//!
43//! let wide = Cell::new("中", 2);
44//! assert_eq!(wide.width(), 2);
45//! ```
46//!
47//! ## Multi-column cells
48//!
49//! A grapheme wider than one column occupies that many adjacent grid
50//! columns. The leftmost column stores the primary and every column after it
51//! stores a continuation placeholder:
52//!
53//! ```text
54//! col: 0 1 2
55//! ┌───────┬───────┬───┐
56//! row 0 │ 中 │ cont. │ A │
57//! └───────┴───────┴───┘
58//! width=2 width=0 width=1
59//! ```
60//!
61//! Most graphemes are one or two columns wide. A cell may be wider: under
62//! [`WidthMode::Wc`](crate::text::WidthMode::Wc) a joined emoji sequence
63//! measures the sum of its code points, so a four-person family emoji is
64//! eight columns and owns seven continuations.
65//!
66//! Continuations preserve the one-`Cell`-per-column layout for wide
67//! graphemes. [`Cell::is_blank`] identifies a cell that claims one column
68//! and draws a space, regardless of style. A continuation claims zero
69//! columns, so it returns `false`.
70
71use compact_str::CompactString;
72
73use crate::style::Style;
74
75/// A single terminal-grid cell.
76///
77/// `Cell` is the value stored in buffers and surfaces. It contains the
78/// grapheme content for a column, the style applied to that content, and the
79/// number of columns the content occupies.
80///
81/// Use [`Cell::new`] for construction and [`Cell::BLANK`] for an empty
82/// styled-as-default space. Continuations are normally produced by the
83/// surface write path rather than by application code.
84#[derive(Debug, Clone)]
85pub struct Cell {
86 /// The grapheme cluster content. Empty string for a continuation
87 /// placeholder.
88 content: CompactString,
89 /// Visual style: colors, attributes, underline, and any attached
90 /// hyperlink. The link inside `style` is reference-counted so a
91 /// run of identically-linked cells shares a single allocation
92 /// without per-cell deep clones.
93 pub style: Style,
94 /// Column footprint. `0` marks a continuation placeholder; `1` or more
95 /// marks a primary owning that many columns.
96 width: u8,
97}
98
99impl PartialEq for Cell {
100 #[inline]
101 fn eq(&self, other: &Self) -> bool {
102 self.width == other.width && self.style == other.style && self.content == other.content
103 }
104}
105
106impl Eq for Cell {}
107
108impl Default for Cell {
109 fn default() -> Self {
110 Self::BLANK
111 }
112}
113
114impl Cell {
115 /// A blank one-column cell with default style.
116 ///
117 /// The blank cell stores a single space (`" "`), uses [`Style::EMPTY`],
118 /// and has width `1`.
119 ///
120 /// # Returns
121 ///
122 /// This is a constant value, so use it directly wherever a default blank
123 /// cell is needed.
124 ///
125 /// # Panics
126 ///
127 /// Never panics.
128 ///
129 /// # Usage notes
130 ///
131 /// Clearing and newly allocated buffers use `BLANK`. Clone it when an
132 /// owned value is required.
133 pub const BLANK: Cell = Cell {
134 content: CompactString::const_new(" "),
135 style: Style::EMPTY,
136 width: 1,
137 };
138
139 /// A continuation placeholder with default style.
140 ///
141 /// The placeholder stores no content, uses [`Style::EMPTY`], and has
142 /// width `0`. [`Cell::is_continuation`] reports `true` for it.
143 ///
144 /// # Returns
145 ///
146 /// This is a constant value, so use it directly wherever a continuation
147 /// is needed.
148 ///
149 /// # Panics
150 ///
151 /// Never panics.
152 ///
153 /// # Usage notes
154 ///
155 /// A wide cell is followed by [`width`](Cell::width) `- 1` of these, one
156 /// for every column the cell owns beyond its first. The surface write
157 /// path lays them down; application code needs them only when it builds
158 /// a row by hand.
159 ///
160 /// Under background color erase a continuation carries the style of the
161 /// cell that owns it, so clone the constant and give it that style
162 /// rather than leaving it default.
163 pub const CONTINUATION: Cell = Cell {
164 content: CompactString::const_new(""),
165 style: Style::EMPTY,
166 width: 0,
167 };
168
169 /// Create a cell holding `content` across `width` terminal columns.
170 ///
171 /// # Parameters
172 ///
173 /// - `content`: grapheme content to store in the cell.
174 /// - `width`: number of terminal columns the content occupies. Width `0`
175 /// makes a continuation placeholder whatever the content, so use
176 /// [`Cell::CONTINUATION`] when that is what you mean.
177 ///
178 /// # Returns
179 ///
180 /// A cell with the given content and width, and the default style.
181 ///
182 /// # Panics
183 ///
184 /// Never panics.
185 ///
186 /// # Usage notes
187 ///
188 /// This constructor does not validate that `width` matches the display
189 /// width of `content`; the caller chooses the measurement policy. Use
190 /// [`WidthMode::grapheme_width`](crate::text::WidthMode::grapheme_width)
191 /// to measure, so the stored width matches what the terminal advances.
192 ///
193 /// When a cell wider than one column is written through the
194 /// buffer/surface write path, the `width - 1` slots to its right are
195 /// filled with continuation placeholders automatically. If there is no
196 /// room for them at the row's right edge,
197 /// [`Buffer::set`](crate::buffer::Buffer::set) stores a blank instead of
198 /// part of a grapheme.
199 pub fn new(content: impl Into<CompactString>, width: u8) -> Self {
200 Cell {
201 content: content.into(),
202 style: Style::default(),
203 width,
204 }
205 }
206
207 /// Test whether this cell occupies more than one column.
208 ///
209 /// # Returns
210 ///
211 /// `true` when [`Cell::width`] is `2` or more.
212 ///
213 /// # Panics
214 ///
215 /// Never panics.
216 ///
217 /// # Usage notes
218 ///
219 /// Width alone decides the answer, because width alone decides how many
220 /// columns the cell takes. A cell that stores no content still owns the
221 /// columns its width names, and the blank drawn in its place covers all
222 /// of them.
223 ///
224 /// In a well-formed surface, such a cell is followed immediately by
225 /// `width - 1` continuation placeholders.
226 #[inline]
227 pub fn is_wide(&self) -> bool {
228 self.width > 1
229 }
230
231 /// Test whether this is a continuation placeholder.
232 ///
233 /// # Returns
234 ///
235 /// `true` when [`Cell::width`] is `0`.
236 ///
237 /// # Panics
238 ///
239 /// Never panics.
240 ///
241 /// # Usage notes
242 ///
243 /// Width alone decides, as it does for [`Cell::is_wide`]: a cell that
244 /// claims no column has none to draw in, so whatever it stores is never
245 /// reached. Width `0`, `1`, and `2` or more partition every cell into a
246 /// continuation, a narrow cell, and a wide one.
247 #[inline]
248 pub fn is_continuation(&self) -> bool {
249 self.width == 0
250 }
251
252 /// Test whether this cell is a single blank column.
253 ///
254 /// # Returns
255 ///
256 /// `true` when the cell claims one column and draws a space in it, which
257 /// is the shape of [`Cell::BLANK`].
258 ///
259 /// # Panics
260 ///
261 /// Never panics.
262 ///
263 /// # Usage notes
264 ///
265 /// Style is not considered. A styled space is still blank, because this
266 /// method answers what the cell occupies rather than how it is painted.
267 ///
268 /// Width is considered, so the answer stays about one column. A
269 /// continuation claims none, and a cell claiming several stands for a
270 /// span rather than a single blank; each is something other than a blank
271 /// column, and each reports `false`.
272 ///
273 /// A cell that claims its one column while storing nothing draws a space
274 /// there, the same as [`Cell::BLANK`], and answers the same way. The
275 /// question is what reaches the column, so two cells that put the same
276 /// thing in it give one answer.
277 pub fn is_blank(&self) -> bool {
278 self.width == 1 && (self.content == " " || self.content.is_empty())
279 }
280
281 /// The bytes that draw this cell.
282 ///
283 /// A cell storing no content still owns the columns its width names, so
284 /// it draws as a blank in every one of them. Writing the content alone
285 /// would close a gap the grid is holding open, and put everything after
286 /// it on the row a column short for each one skipped.
287 ///
288 /// Call this only on a cell that is not a continuation; a continuation
289 /// draws nothing at all, and its caller returns before reaching here.
290 pub(crate) fn draw_bytes(&self) -> &[u8] {
291 /// Enough spaces to stand in for any width a cell can claim.
292 const BLANKS: [u8; u8::MAX as usize] = [b' '; u8::MAX as usize];
293
294 if self.content.is_empty() {
295 &BLANKS[..self.width as usize]
296 } else {
297 self.content.as_bytes()
298 }
299 }
300
301 /// Return the cell's grapheme-cluster content.
302 ///
303 /// # Returns
304 ///
305 /// The stored content as `&str`. Continuation cells return an empty
306 /// string.
307 ///
308 /// # Panics
309 ///
310 /// Never panics.
311 ///
312 /// # Usage notes
313 ///
314 /// This returns the stored content exactly; it does not derive or append
315 /// the neighboring continuations for multi-column cells.
316 #[inline]
317 pub fn content(&self) -> &str {
318 self.content.as_str()
319 }
320
321 /// Column footprint of this cell on the grid.
322 ///
323 /// # Returns
324 ///
325 /// The number of terminal columns owned by this cell: `0` for a
326 /// continuation placeholder, otherwise the width passed to
327 /// [`Cell::new`].
328 ///
329 /// # Panics
330 ///
331 /// Never panics.
332 ///
333 /// # Usage notes
334 ///
335 /// Row walkers generally advance by `cell.width().max(1)` after handling
336 /// continuations, so they make progress even when encountering a
337 /// continuation placeholder.
338 #[inline]
339 pub fn width(&self) -> u8 {
340 self.width
341 }
342
343 /// Return this cell with a replacement style.
344 ///
345 /// # Parameters
346 ///
347 /// - `style`: style to store on the returned cell.
348 ///
349 /// # Returns
350 ///
351 /// `self` with its `style` field replaced by `style`.
352 ///
353 /// # Panics
354 ///
355 /// Never panics.
356 ///
357 /// # Usage notes
358 ///
359 /// This builder-style method preserves content and width. Styling a
360 /// continuation is possible, but continuations do not render independent
361 /// content.
362 pub fn style(mut self, style: impl Into<Style>) -> Self {
363 self.style = style.into();
364 self
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_blank_cell() {
374 let c = Cell::BLANK;
375 assert_eq!(c.width(), 1);
376 assert!(c.is_blank());
377 assert!(c.style.is_empty());
378 }
379
380 #[test]
381 fn test_narrow_cell() {
382 let c = Cell::new("A", 1);
383 assert_eq!(c.content(), "A");
384 assert!(!c.is_wide());
385 assert_eq!(c.width(), 1);
386 }
387
388 #[test]
389 fn test_wide_cell() {
390 let c = Cell::new("中", 2);
391 assert_eq!(c.content(), "中");
392 assert!(c.is_wide());
393 assert_eq!(c.width(), 2);
394 }
395
396 #[test]
397 fn test_continuation_cell() {
398 let c = Cell::CONTINUATION;
399 assert!(c.is_continuation());
400 assert_eq!(c.width(), 0);
401 assert!(!c.is_blank());
402 }
403
404 /// `is_blank` answers for one blank column, so width decides as much as
405 /// content does.
406 ///
407 /// Reading it as "holds nothing to draw" put a continuation and a blank
408 /// in the same class, though one claims no column and the other claims
409 /// exactly one. A caller scanning a row for the columns it can drop then
410 /// had to re-check the width every time to tell them apart.
411 #[test]
412 fn only_a_cell_drawing_one_blank_column_is_blank() {
413 assert!(Cell::BLANK.is_blank());
414 assert!(Cell::new(" ", 1).is_blank());
415 // Storing nothing in one column still draws a space there, which is
416 // what `draw_bytes` puts on the screen.
417 let empty = Cell::new("", 1);
418 assert_eq!(empty.draw_bytes(), b" ");
419 assert!(empty.is_blank());
420 // Style rides along; it says how the column is painted, not what it
421 // holds.
422 assert!(Cell::BLANK.style(Style::default().bold()).is_blank());
423
424 // A span of blank columns is not one blank column.
425 assert!(!Cell::new(" ", 2).is_blank());
426 assert!(!Cell::new("", 2).is_blank());
427 // No column at all is not one blank column either.
428 assert!(!Cell::CONTINUATION.is_blank());
429 assert!(!Cell::new("x", 1).is_blank());
430 }
431
432 #[test]
433 fn the_continuation_constant_matches_one_built_by_hand() {
434 // Rows built before the constant existed pass `Cell::new("", 0)`,
435 // and `PartialEq` weighs content, width, and style. A constant that
436 // differed in any of the three would compare unequal and make the
437 // renderer redraw a column that did not change.
438 assert_eq!(Cell::CONTINUATION, Cell::new("", 0));
439 }
440
441 #[test]
442 fn a_cell_may_own_more_than_two_columns() {
443 // Under `WidthMode::Wc` a joined emoji sequence measures the sum of
444 // its code points, so the grid must be able to credit one cell with
445 // more columns than a CJK ideograph takes.
446 let family = "\u{1f468}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}";
447 let c = Cell::new(family, 8);
448 assert_eq!(c.width(), 8);
449 assert!(c.is_wide());
450 assert!(!c.is_continuation());
451 }
452
453 #[test]
454 fn width_alone_decides_the_structural_role() {
455 // Width `0`, `1`, and `2` or more partition every cell into a
456 // continuation, a narrow cell, and a wide one. Content never enters
457 // into it: a cell that claims no column has none to draw in, and one
458 // that stores nothing still owns every column it claims.
459 assert!(Cell::CONTINUATION.is_continuation());
460 assert!(Cell::new("a", 0).is_continuation());
461 assert!(Cell::new("", 2).is_wide());
462 assert!(Cell::new("\u{1f469}", 2).is_wide());
463 }
464
465 #[test]
466 fn a_cell_claiming_no_column_is_a_continuation_whatever_it_stores() {
467 // `grapheme_width` measures a lone combining mark as zero, and
468 // `Cell::new`'s own advice is to measure with it, so a cell holding
469 // content at width zero is one step from the documented path. It
470 // claims no column, so the grid treats it as the placeholder it
471 // structurally is and the surface declines to plant it loose.
472 use crate::buffer::{Buffer, Surface, SurfaceMut};
473 use crate::text::Encode;
474
475 let mut buf = Buffer::new(3, 1);
476 buf.set_cell((0, 0).into(), &Cell::new("A", 1));
477 buf.set_cell((1, 0).into(), &Cell::new("\u{301}", 0));
478 buf.set_cell((2, 0).into(), &Cell::new("B", 1));
479
480 assert!(Cell::new("\u{301}", 0).is_continuation());
481 assert_eq!(
482 buf.cell((1, 0).into()).unwrap().content(),
483 " ",
484 "a loose continuation is declined, leaving the blank"
485 );
486 assert_eq!(buf.display().to_string(), "A B");
487 }
488
489 #[test]
490 fn test_cell_with_style() {
491 let c = Cell::new("x", 1).style(Style::default().bold());
492 assert!(c.style.attrs.contains(crate::style::AttrFlags::BOLD));
493 }
494}