Skip to main content

termgrid_core/
ops.rs

1use crate::{
2    style::is_plain_style,
3    text::{is_default_wrap_opts, Span, WrapOpts},
4    Style,
5};
6use serde::{Deserialize, Serialize};
7
8fn is_default_charset(c: &BoxCharset) -> bool {
9    *c == BoxCharset::default()
10}
11
12/// A set of draw ops emitted by a producer for a single render tick.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
14pub struct Frame {
15    pub ops: Vec<RenderOp>,
16}
17
18/// A single cell in a `blit` payload.
19///
20/// When present, this cell overwrites the destination.
21/// When absent (`null` in JSON), the destination cell is left unchanged.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct BlitCell {
24    pub glyph: String,
25    #[serde(default, skip_serializing_if = "is_plain_style")]
26    pub style: Style,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum BoxCharset {
32    /// ASCII `+`, `-`, `|`.
33    Ascii,
34    /// Unicode single-line box drawing characters.
35    UnicodeSingle,
36    /// Unicode double-line box drawing characters.
37    UnicodeDouble,
38}
39
40impl Default for BoxCharset {
41    fn default() -> Self {
42        Self::UnicodeSingle
43    }
44}
45
46fn is_default_truncate_mode(m: &TruncateMode) -> bool {
47    *m == TruncateMode::default()
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum TruncateMode {
53    /// Drop any glyphs that do not fit.
54    Clip,
55    /// Replace the tail with an ellipsis ("…") when truncation occurs.
56    Ellipsis,
57}
58
59impl Default for TruncateMode {
60    fn default() -> Self {
61        Self::Clip
62    }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(tag = "op", rename_all = "snake_case")]
67pub enum RenderOp {
68    /// Clear the entire grid to empty cells.
69    Clear,
70
71    /// Clear a full line (row) to plain spaces.
72    ///
73    /// This is rendered as plain spaces rather than `Cell::Empty` to avoid
74    /// style bleed from earlier styled cells on the same line.
75    ClearLine { y: u16 },
76
77    /// Clear from (`x`,`y`) to end-of-line (inclusive) to plain spaces.
78    ///
79    /// This mirrors ANSI EL (erase in line) mode 0.
80    ClearEol { x: u16, y: u16 },
81
82    /// Clear from start-of-line to (`x`,`y`) (inclusive) to plain spaces.
83    ///
84    /// This mirrors ANSI EL (erase in line) mode 1.
85    ClearBol { x: u16, y: u16 },
86
87    /// Clear from (`x`,`y`) to end-of-screen (inclusive) to plain spaces.
88    ///
89    /// This mirrors ANSI ED (erase in display) mode 0.
90    ClearEos { x: u16, y: u16 },
91
92    /// Clear a rectangle to plain spaces.
93    ///
94    /// This is semantically equivalent to `FillRect` with a plain style.
95    ClearRect { x: u16, y: u16, w: u16, h: u16 },
96
97    /// Put text at a coordinate.
98    Put {
99        x: u16,
100        y: u16,
101        text: String,
102        #[serde(default, skip_serializing_if = "is_plain_style")]
103        style: Style,
104    },
105
106    /// Put a single glyph (grapheme cluster) at a coordinate.
107    PutGlyph {
108        x: u16,
109        y: u16,
110        glyph: String,
111        #[serde(default, skip_serializing_if = "is_plain_style")]
112        style: Style,
113    },
114
115    /// Put a single-line label, clipped to `w` cells.
116    ///
117    /// This is a convenience op for common UI labels.
118    Label {
119        x: u16,
120        y: u16,
121        /// Maximum width in cells.
122        w: u16,
123        text: String,
124        #[serde(default, skip_serializing_if = "is_plain_style")]
125        style: Style,
126        #[serde(default, skip_serializing_if = "is_default_truncate_mode")]
127        truncate: TruncateMode,
128    },
129
130    /// Put a single-line styled label (spans), clipped to `w` cells.
131    ///
132    /// This op is analogous to `Label` but supports inline styling.
133    LabelStyled {
134        x: u16,
135        y: u16,
136        /// Maximum width in cells.
137        w: u16,
138        spans: Vec<Span>,
139        #[serde(default, skip_serializing_if = "is_default_truncate_mode")]
140        truncate: TruncateMode,
141    },
142
143    /// Put a single-line styled label (spans), clipped to `w` cells.
144    ///
145    /// This op is useful for UI where inline styling is needed (for example,
146    /// highlighted search matches or mixed emphasis).
147    PutStyled {
148        x: u16,
149        y: u16,
150        /// Maximum width in cells.
151        w: u16,
152        spans: Vec<Span>,
153        #[serde(default, skip_serializing_if = "is_default_truncate_mode")]
154        truncate: TruncateMode,
155    },
156
157    /// Put wrapped text within `w` cells, flowing downward from (`x`,`y`).
158    ///
159    /// Wrapping is whitespace-aware with hard-break fallback for long words.
160    PutWrapped {
161        x: u16,
162        y: u16,
163        /// Wrap width in cells.
164        w: u16,
165        text: String,
166        #[serde(default, skip_serializing_if = "is_plain_style")]
167        style: Style,
168    },
169
170    /// Put wrapped styled text (spans) within `w` cells, flowing downward from (`x`,`y`).
171    ///
172    /// Wrapping is whitespace-aware with hard-break fallback for long tokens.
173    /// Use `wrap_opts` to control whitespace preservation and trimming.
174    PutWrappedStyled {
175        x: u16,
176        y: u16,
177        /// Wrap width in cells.
178        w: u16,
179        spans: Vec<Span>,
180        #[serde(default, skip_serializing_if = "is_default_wrap_opts")]
181        wrap_opts: WrapOpts,
182        /// Optional maximum number of visual lines to render.
183        #[serde(default, skip_serializing_if = "Option::is_none")]
184        max_lines: Option<u16>,
185    },
186
187    /// Blit (copy) a small source cell-map onto the grid.
188    ///
189    /// - `cells` is a row-major array of length `w*h`.
190    /// - `null` cells are transparent (leave destination unchanged).
191    /// - Wide glyphs (width=2) occupy two destination cells; the next source cell
192    ///   in that row is ignored.
193    Blit {
194        x: u16,
195        y: u16,
196        w: u16,
197        h: u16,
198        cells: Vec<Option<BlitCell>>,
199    },
200
201    /// Fill a rectangle with styled spaces.
202    ///
203    /// Use this for clearing regions and for background fills.
204    FillRect {
205        x: u16,
206        y: u16,
207        w: u16,
208        h: u16,
209        #[serde(default, skip_serializing_if = "is_plain_style")]
210        style: Style,
211    },
212
213    /// Draw a horizontal line using a single glyph.
214    ///
215    /// `len` is measured in **cells**. For width=2 glyphs, placement will stop
216    /// when fewer than 2 cells remain.
217    #[serde(rename = "hline")]
218    HLine {
219        x: u16,
220        y: u16,
221        len: u16,
222        glyph: String,
223        #[serde(default, skip_serializing_if = "is_plain_style")]
224        style: Style,
225    },
226
227    /// Draw a vertical line using a single glyph.
228    ///
229    /// `len` is measured in **rows**.
230    #[serde(rename = "vline")]
231    VLine {
232        x: u16,
233        y: u16,
234        len: u16,
235        glyph: String,
236        #[serde(default, skip_serializing_if = "is_plain_style")]
237        style: Style,
238    },
239
240    /// Draw a bordered box.
241    ///
242    /// The box is clipped to the grid bounds.
243    Box {
244        x: u16,
245        y: u16,
246        w: u16,
247        h: u16,
248        #[serde(default, skip_serializing_if = "is_plain_style")]
249        style: Style,
250        #[serde(default, skip_serializing_if = "is_default_charset")]
251        charset: BoxCharset,
252    },
253}