reflex-terminal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.doc/ext/reflex-terminal/native.cpp +19 -0
  3. data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
  4. data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
  5. data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
  6. data/.github/workflows/release-gem.yml +60 -0
  7. data/.github/workflows/tag.yml +35 -0
  8. data/.github/workflows/test.yml +46 -0
  9. data/.github/workflows/utils.rb +127 -0
  10. data/CONTRIBUTING.md +7 -0
  11. data/ChangeLog.md +6 -0
  12. data/Gemfile +5 -0
  13. data/LICENSE +21 -0
  14. data/README.md +100 -0
  15. data/Rakefile +44 -0
  16. data/VERSION +1 -0
  17. data/examples/terminal.rb +101 -0
  18. data/ext/reflex-terminal/defs.h +17 -0
  19. data/ext/reflex-terminal/extconf.rb +29 -0
  20. data/ext/reflex-terminal/native.cpp +19 -0
  21. data/ext/reflex-terminal/renderer.cpp +118 -0
  22. data/ext/reflex-terminal/terminal.cpp +572 -0
  23. data/include/reflex/terminal.h +233 -0
  24. data/include/reflex-terminal/defs.h +36 -0
  25. data/include/reflex-terminal/renderer.h +57 -0
  26. data/include/reflex-terminal/ruby/renderer.h +41 -0
  27. data/include/reflex-terminal/ruby/terminal.h +43 -0
  28. data/include/reflex-terminal/ruby.h +11 -0
  29. data/include/reflex-terminal.h +13 -0
  30. data/lib/reflex/bell_event.rb +15 -0
  31. data/lib/reflex/terminal.rb +168 -0
  32. data/lib/reflex/terminal_view.rb +268 -0
  33. data/lib/reflex-terminal/ext.rb +2 -0
  34. data/lib/reflex-terminal/extension.rb +41 -0
  35. data/lib/reflex-terminal.rb +7 -0
  36. data/reflex-terminal.gemspec +39 -0
  37. data/src/pty.cpp +236 -0
  38. data/src/renderer.cpp +365 -0
  39. data/src/terminal.cpp +1547 -0
  40. data/src/terminal.h +66 -0
  41. data/src/win32/pty.cpp +387 -0
  42. data/test/helper.rb +11 -0
  43. data/test/test_renderer.rb +113 -0
  44. data/test/test_terminal.rb +683 -0
  45. data/test/test_terminal_view.rb +182 -0
  46. metadata +152 -0
data/src/renderer.cpp ADDED
@@ -0,0 +1,365 @@
1
+ #include <reflex-terminal/renderer.h>
2
+
3
+
4
+ #include <assert.h>
5
+ #include <math.h>
6
+ #include <vector>
7
+ #include <map>
8
+ #include <rays/image.h>
9
+ #include <reflex/exception.h>
10
+ #include "terminal.h"
11
+
12
+
13
+ namespace ReflexTerminal
14
+ {
15
+
16
+
17
+ using namespace Reflex;
18
+
19
+
20
+ enum
21
+ {
22
+
23
+ ATLAS_WIDTH = 1024,
24
+
25
+ ATLAS_INITIAL_ROWS = 8,
26
+
27
+ ATLAS_MAX_HEIGHT = 4096
28
+
29
+ };
30
+
31
+
32
+ struct Glyph
33
+ {
34
+ // a glyph's place in the atlas image. width is its own, so that a
35
+ // wide character does not run over the slot after it
36
+
37
+ coord x, y, width;
38
+
39
+ bool is_valid () const {return width > 0;}
40
+
41
+ };// Glyph
42
+
43
+
44
+ struct Renderer::Data
45
+ {
46
+
47
+ Font font;
48
+
49
+ coord cell_width = 0, cell_height = 0;
50
+
51
+ // where the next glyph goes
52
+ coord x = 0, y = 0;
53
+
54
+ // Painter::text() flushes the batch and uploads a texture on every
55
+ // call, so every glyph is rasterized here once and copied from
56
+ Image atlas;
57
+
58
+ // the empty Glyph of a character too wide for the atlas is kept
59
+ // as well, so that it is not measured again on every frame
60
+ std::map<String, Glyph> glyphs;
61
+
62
+ // reused between frames so that a screenful of new characters
63
+ // does not allocate
64
+ std::vector<String> missing;
65
+
66
+ };// Renderer::Data
67
+
68
+
69
+ Renderer::Renderer ()
70
+ {
71
+ }
72
+
73
+ Renderer::Renderer (const Font& font)
74
+ {
75
+ set_font(font);
76
+ }
77
+
78
+ Renderer::~Renderer ()
79
+ {
80
+ }
81
+
82
+ static void
83
+ reset_atlas (Renderer::Data* self)
84
+ {
85
+ self->glyphs.clear();
86
+ self->x = self->y = 0;
87
+ self->atlas = Image(ATLAS_WIDTH, self->cell_height * ATLAS_INITIAL_ROWS);
88
+ }
89
+
90
+ void
91
+ Renderer::set_font (const Font& font)
92
+ {
93
+ if (!font)
94
+ argument_error(__FILE__, __LINE__);
95
+
96
+ self->font = font;
97
+ self->cell_width = font.get_width("M");
98
+ self->cell_height = ceil(font.get_height());
99
+ reset_atlas(self.get());
100
+ }
101
+
102
+ const Font&
103
+ Renderer::font () const
104
+ {
105
+ return self->font;
106
+ }
107
+
108
+ coord
109
+ Renderer::cell_width () const
110
+ {
111
+ return self->cell_width;
112
+ }
113
+
114
+ coord
115
+ Renderer::cell_height () const
116
+ {
117
+ return self->cell_height;
118
+ }
119
+
120
+ static bool
121
+ grow_atlas (Renderer::Data* self)
122
+ {
123
+ // the image is replaced rather than resized, so nothing may be
124
+ // drawing from it while this runs
125
+ if (self->y + self->cell_height <= self->atlas.height())
126
+ return true;
127
+
128
+ coord height = self->atlas.height() * 2;
129
+ if (height > ATLAS_MAX_HEIGHT) return false;
130
+
131
+ Image grown(ATLAS_WIDTH, height);
132
+ Painter painter = grown.painter();
133
+ painter.begin();
134
+ painter.image(self->atlas);
135
+ painter.end();
136
+
137
+ self->atlas = grown;
138
+ return true;
139
+ }
140
+
141
+ static Glyph
142
+ allocate_glyph (Renderer::Data* self, const String& text)
143
+ {
144
+ coord width = ceil(self->font.get_width(text.c_str()));
145
+ if (width <= 0 || width > ATLAS_WIDTH) return {0, 0, 0};
146
+
147
+ if (self->x + width > ATLAS_WIDTH)
148
+ {
149
+ self->x = 0;
150
+ self->y += self->cell_height;
151
+ }
152
+ if (!grow_atlas(self)) return {0, 0, 0};
153
+
154
+ Glyph glyph = {self->x, self->y, width};
155
+ self->x += width;
156
+ return glyph;
157
+ }
158
+
159
+ static void
160
+ add_glyphs (Renderer::Data* self, const std::vector<String>& texts)
161
+ {
162
+ // each paint switches the offscreen rendering context, which costs
163
+ // far more than the drawing itself, so the unknown glyphs are
164
+ // collected first and drawn together
165
+
166
+ std::map<String, Glyph> added;
167
+ for (const String& text : texts)
168
+ {
169
+ if (self->glyphs.find(text) != self->glyphs.end()) continue;
170
+ if (added.find(text) != added.end()) continue;
171
+
172
+ Glyph glyph = allocate_glyph(self, text);
173
+ if (glyph.is_valid())
174
+ added[text] = glyph;
175
+ else
176
+ self->glyphs[text] = glyph;// no room: do not try again
177
+ }
178
+ if (added.empty()) return;
179
+
180
+ Painter painter = self->atlas.painter();
181
+ painter.begin();
182
+ painter.set_font(self->font);
183
+ painter.set_fill(1, 1, 1);
184
+ for (const auto& it : added)
185
+ painter.text(it.first.c_str(), it.second.x, it.second.y);
186
+ painter.end();
187
+
188
+ self->glyphs.insert(added.begin(), added.end());
189
+ }
190
+
191
+ template <typename FUN>
192
+ static void
193
+ add_missing_glyphs (Renderer::Data* self, FUN fun)
194
+ {
195
+ assert(self->missing.empty());
196
+
197
+ fun();
198
+ if (self->missing.empty())
199
+ return;
200
+
201
+ add_glyphs(self, self->missing);
202
+ self->missing.clear();
203
+ }
204
+
205
+ void
206
+ Renderer::bake_glyphs (const Terminal& terminal)
207
+ {
208
+ // baking paints into the offscreen atlas, which switches the
209
+ // rendering context and replaces the image whenever it grows, so a
210
+ // caller must not run bake_glyphs inside a frame being drawn
211
+
212
+ if (!self->font)
213
+ invalid_state_error(__FILE__, __LINE__);
214
+
215
+ // an empty atlas is seeded with printable ascii, so that a screen
216
+ // of it is never handed back to be rasterized a character at a
217
+ // time. it also leaves the atlas holding something no matter what
218
+ // the screen showed, which is how a caller tells it has been baked
219
+ if (self->glyphs.empty())
220
+ {
221
+ // baked on its own, so that the walk that follows collects
222
+ // only what the seed does not already cover
223
+ add_missing_glyphs(self.get(), [&]()
224
+ {
225
+ for (char c = 0x20; c <= 0x7e; ++c)
226
+ self->missing.emplace_back(&c, 1);
227
+ });
228
+ }
229
+
230
+ add_missing_glyphs(self.get(), [&]()
231
+ {
232
+ const uint* cell_offsets = Terminal_get_cell_offsets(terminal).data();
233
+ for (const Terminal::SpanList& row : terminal.spans())
234
+ {
235
+ for (const Terminal::Span& span : row)
236
+ {
237
+ const uint* offsets = cell_offsets + span.cell_offset;
238
+ for (uint i = 0; i < span.cell_size; ++i)
239
+ {
240
+ uint begin = offsets[i];
241
+ uint end = (i + 1) < span.cell_size ? offsets[i + 1] : (uint) span.text.size();
242
+
243
+ String text(span.text.data() + begin, end - begin);
244
+ if (self->glyphs.find(text) == self->glyphs.end())
245
+ self->missing.emplace_back(text);
246
+ }
247
+ }
248
+ }
249
+ });
250
+ }
251
+
252
+ size_t
253
+ Renderer::glyph_count () const
254
+ {
255
+ // how many glyphs the atlas has been asked to hold, whether or not it
256
+ // had the room for them: one too wide to fit is kept as an empty Glyph
257
+ // and still counted
258
+
259
+ return self->glyphs.size();
260
+ }
261
+
262
+ static Color
263
+ to_color (int rgb, const Color& fallback)
264
+ {
265
+ if (rgb == Terminal::Span::COLOR_NONE)
266
+ return fallback;
267
+
268
+ return Color(
269
+ ((rgb >> 16) & 0xff) / 255.f,
270
+ ((rgb >> 8) & 0xff) / 255.f,
271
+ ( rgb & 0xff) / 255.f);
272
+ }
273
+
274
+ static bool
275
+ colors_inverted (uint attribs)
276
+ {
277
+ return
278
+ ((attribs & Terminal::Span::INVERSE) != 0) !=
279
+ ((attribs & Terminal::Span::SELECTED) != 0);
280
+ }
281
+
282
+ void
283
+ Renderer::draw (Painter* painter, const Terminal& terminal, const Bounds& bounds)
284
+ {
285
+ if (!painter)
286
+ argument_error(__FILE__, __LINE__);
287
+ if (!self->font)
288
+ invalid_state_error(__FILE__, __LINE__);
289
+
290
+ // what the terminal has not been told is left to the renderer, so the
291
+ // theme starts here and get_color only writes over it when the child
292
+ // or the application has named a color of its own
293
+ Color theme_fg(1, 1, 1), theme_bg(0, 0, 0);
294
+ terminal.get_color(Terminal::COLOR_FOREGROUND, &theme_fg);
295
+ terminal.get_color(Terminal::COLOR_BACKGROUND, &theme_bg);
296
+
297
+ coord cw = self->cell_width;
298
+ coord ch = self->cell_height;
299
+
300
+ painter->push_state();
301
+ painter->set_font(self->font);
302
+
303
+ painter->set_fill(theme_bg);
304
+ painter->rect(bounds);
305
+
306
+ // every background first, then every glyph: alternating shapes
307
+ // and images breaks the painter's batch and costs several times
308
+ // more than the two passes do
309
+ const Terminal::RowList& rows = terminal.spans();
310
+ for (size_t y = 0; y < rows.size(); ++y)
311
+ {
312
+ for (const Terminal::Span& span : rows[y])
313
+ {
314
+ bool inverted = colors_inverted(span.attribs);
315
+ Color fill = inverted ? to_color(span.fg, theme_fg) : to_color(span.bg, theme_bg);
316
+ if (fill == theme_bg) continue;
317
+
318
+ painter->set_fill(fill);
319
+ painter->rect(span.x * cw, y * ch, span.width * cw, ch);
320
+ }
321
+ }
322
+
323
+ const Image& atlas = self->atlas;
324
+ const uint* cell_offsets = Terminal_get_cell_offsets(terminal).data();
325
+ for (size_t y = 0; y < rows.size(); ++y)
326
+ {
327
+ for (const Terminal::Span& span : rows[y])
328
+ {
329
+ if (span.cell_size == 0) continue;
330
+
331
+ bool inverted = colors_inverted(span.attribs);
332
+ painter->set_fill(inverted ? to_color(span.bg, theme_bg) : to_color(span.fg, theme_fg));
333
+
334
+ // a span breaks where narrow meets wide, so every cell in
335
+ // one steps the same distance
336
+ coord step = (coord) span.width / span.cell_size * cw;
337
+ coord left = span.x * cw;
338
+ coord top = y * ch;
339
+ const uint* offsets = cell_offsets + span.cell_offset;
340
+
341
+ for (uint i = 0; i < span.cell_size; ++i, left += step)
342
+ {
343
+ uint begin = offsets[i];
344
+ uint end = i + 1 < span.cell_size ? offsets[i + 1] : (uint) span.text.size();
345
+ String text(span.text.data() + begin, end - begin);
346
+
347
+ auto it = self->glyphs.find(text);
348
+ if (it == self->glyphs.end() || !it->second.is_valid())
349
+ {
350
+ // the atlas is full: draw it the slow way
351
+ painter->text(text.c_str(), left, top);
352
+ continue;
353
+ }
354
+
355
+ const Glyph& glyph = it->second;
356
+ painter->image(atlas, glyph.x, glyph.y, glyph.width, ch, left, top, glyph.width, ch);
357
+ }
358
+ }
359
+ }
360
+
361
+ painter->pop_state();
362
+ }
363
+
364
+
365
+ }// ReflexTerminal