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.
- checksums.yaml +7 -0
- data/.doc/ext/reflex-terminal/native.cpp +19 -0
- data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
- data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
- data/.github/workflows/release-gem.yml +60 -0
- data/.github/workflows/tag.yml +35 -0
- data/.github/workflows/test.yml +46 -0
- data/.github/workflows/utils.rb +127 -0
- data/CONTRIBUTING.md +7 -0
- data/ChangeLog.md +6 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/examples/terminal.rb +101 -0
- data/ext/reflex-terminal/defs.h +17 -0
- data/ext/reflex-terminal/extconf.rb +29 -0
- data/ext/reflex-terminal/native.cpp +19 -0
- data/ext/reflex-terminal/renderer.cpp +118 -0
- data/ext/reflex-terminal/terminal.cpp +572 -0
- data/include/reflex/terminal.h +233 -0
- data/include/reflex-terminal/defs.h +36 -0
- data/include/reflex-terminal/renderer.h +57 -0
- data/include/reflex-terminal/ruby/renderer.h +41 -0
- data/include/reflex-terminal/ruby/terminal.h +43 -0
- data/include/reflex-terminal/ruby.h +11 -0
- data/include/reflex-terminal.h +13 -0
- data/lib/reflex/bell_event.rb +15 -0
- data/lib/reflex/terminal.rb +168 -0
- data/lib/reflex/terminal_view.rb +268 -0
- data/lib/reflex-terminal/ext.rb +2 -0
- data/lib/reflex-terminal/extension.rb +41 -0
- data/lib/reflex-terminal.rb +7 -0
- data/reflex-terminal.gemspec +39 -0
- data/src/pty.cpp +236 -0
- data/src/renderer.cpp +365 -0
- data/src/terminal.cpp +1547 -0
- data/src/terminal.h +66 -0
- data/src/win32/pty.cpp +387 -0
- data/test/helper.rb +11 -0
- data/test/test_renderer.rb +113 -0
- data/test/test_terminal.rb +683 -0
- data/test/test_terminal_view.rb +182 -0
- 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
|