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 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}