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}