Skip to main content

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}