Skip to content

Commit 990ea83

Browse files
committed
Polyfill graphical characters on EGA/VGA terminals.
1 parent 18f01f3 commit 990ea83

24 files changed

+3139
-33
lines changed

examples/nwindows_test/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ add_executable(nwindows_test
99
edit_text_test_window.cpp
1010
error_test_window.cpp
1111
rendering_test_window.cpp
12+
ascii_fallback_test_window.cpp
13+
console_font_test_window.cpp
1214
)
1315

1416
target_link_libraries(
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/*
2+
* Copyright (c) 2025 Robin E. R. Davies
3+
* All rights reserved.
4+
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
12+
* The above copyright notice and this permission notice shall be included in all
13+
* copies or substantial portions of the Software.
14+
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
* SOFTWARE.
22+
*/
23+
24+
#include "tests.hpp"
25+
#include <term.h>
26+
#include <memory.h>
27+
28+
class NAltTextElement: public NElement {
29+
private:
30+
NAltTextElement(int acs)
31+
:NElement("AltText"), acs(acs)
32+
{
33+
}
34+
public:
35+
using self = NAltTextElement;
36+
using super = NElement;
37+
using ptr = std::shared_ptr<self>;
38+
39+
static ptr create(int acs) {
40+
return std::shared_ptr<self>(new NAltTextElement(acs));
41+
}
42+
43+
protected:
44+
virtual NSize measure(const NSize& constraints) override {
45+
return { 1,1 };
46+
}
47+
virtual void render() override {
48+
move(0,0);
49+
print_acs(0,0,this->acs);
50+
}
51+
private:
52+
int acs;
53+
54+
};
55+
56+
class NCharacterTestWindow: public NElement {
57+
private:
58+
NCharacterTestWindow(wchar_t c)
59+
:NElement("CharacterTest"), c(c)
60+
{
61+
}
62+
public:
63+
using self = NCharacterTestWindow;
64+
using super = NElement;
65+
using ptr = std::shared_ptr<self>;
66+
67+
static ptr create(wchar_t c) {
68+
return std::shared_ptr<self>(new NCharacterTestWindow(c));
69+
}
70+
71+
protected:
72+
virtual NSize measure(const NSize& constraints) override {
73+
return { 2,1 };
74+
}
75+
virtual void render() override {
76+
cchar_t ch;
77+
memset(&ch,0,sizeof(ch));
78+
ch.chars[0] = this->c;
79+
ch.chars[1] = 0;
80+
81+
bool valid = wadd_wch(stdscr,&ch) != ERR;
82+
move(0,0);
83+
wchar_t str[2] = {this->c,0};
84+
if (!valid) {
85+
str[0] = L'X';
86+
}
87+
if (ch.chars[0] != this->c) {
88+
str[0] = L'?';
89+
}
90+
print(str);
91+
}
92+
private:
93+
wchar_t c;
94+
95+
};
96+
97+
98+
static NElement::ptr character_test(NWindow::ptr parentWindow, const std::string& label, char32_t c)
99+
{
100+
bool supported = parentWindow->can_display_character(c);
101+
return NHorizontalStackElement::create()
102+
| column_gap(1)
103+
| add_child(NTextElement::create(label))
104+
| add_child(NTextElement::create(u32string_to_utf8(std::u32string(1, c))))
105+
| add_child(NCharacterTestWindow::create(c))
106+
| add_child(NTextElement::create(supported ? "Supported" : "Not supported"));
107+
}
108+
109+
static NElement::ptr acs_grid(NWindow::ptr parentWindow)
110+
{
111+
NVerticalStackElement::ptr grid = NVerticalStackElement::create();
112+
for (char32_t c = 0x20; c < 0x80; c += 16)
113+
{
114+
NHorizontalStackElement::ptr row = NHorizontalStackElement::create();
115+
for (int i = 0; i < 16; i++)
116+
{
117+
char32_t cc = c+i;
118+
row->add_child(NAltTextElement::create((int)cc));
119+
}
120+
grid->add_child(row);
121+
}
122+
return grid;
123+
}
124+
125+
static NElement::ptr character_grid(NWindow::ptr parentWindow)
126+
{
127+
NVerticalStackElement::ptr grid = NVerticalStackElement::create();
128+
for (char32_t c = 0x20; c < 0x100; c += 16)
129+
{
130+
NHorizontalStackElement::ptr row = NHorizontalStackElement::create();
131+
for (int i = 0; i < 16; i++)
132+
{
133+
char32_t cc = c+i;
134+
if (cc == 0x7F) cc = 0xFFFD;
135+
if (wcwidth(cc) == -1)
136+
{
137+
cc = 0xFFFD;
138+
}
139+
row->add_child(NTextElement::create(u32string_to_utf8(std::u32string(1, cc))));
140+
}
141+
grid->add_child(row);
142+
}
143+
return grid;
144+
}
145+
146+
static NElement::ptr device_grid(NWindow::ptr parentWindow)
147+
{
148+
NVerticalStackElement::ptr grid = NVerticalStackElement::create();
149+
for (char32_t c = 0xF000; c < 0xF100; c += 16) // special range for device characters on EGA/VGA devices.
150+
{
151+
NHorizontalStackElement::ptr row = NHorizontalStackElement::create();
152+
for (int i = 0; i < 16; i++)
153+
{
154+
char32_t cc = c+i;
155+
if (cc == 0x7F) cc = 0xFFFD;
156+
if (wcwidth(cc) == -1)
157+
{
158+
cc = 0xFFFD;
159+
}
160+
row->add_child(NTextElement::create(u32string_to_utf8(std::u32string(1, cc))));
161+
}
162+
grid->add_child(row);
163+
}
164+
return grid;
165+
}
166+
167+
void ascii_fallback_test_window(NWindow::ptr parentWindow /* = nullptr */)
168+
{
169+
NWindow::ptr window = NWindow::create(parentWindow, 66, AUTO_SIZE);
170+
NTextElement::ptr keyIndicator;
171+
172+
std::string oGrave = u32string_to_utf8(U"o\u0300");
173+
NColorPair boundsColor;
174+
if (parentWindow && parentWindow->max_color_pairs() > 8) {
175+
boundsColor = window->make_color_pair(0xC0C0C0, 0x4040C0);
176+
}
177+
else {
178+
boundsColor = window->make_color_pair(0xE00000, 0x000000);
179+
}
180+
window
181+
| title("ASCII Fallback Test")
182+
| add_child(
183+
NVerticalStackElement::create()
184+
| margin({ 2,1,2,1 })
185+
| row_gap(1)
186+
| add_child(
187+
NTextElement::create("Fallbacks only occur on non-Unicode terminals. ◉●○🗹□☑☐")
188+
| wrap_text()
189+
)
190+
| add_child(
191+
NHorizontalStackElement::create()
192+
| column_gap(2)
193+
| add_child(
194+
NVerticalStackElement::create()
195+
| add_child(
196+
character_test(parentWindow,
197+
"O with Grave: ",
198+
199+
200+
201+
0xF2)
202+
)
203+
| add_child(
204+
character_test(parentWindow,
205+
"Composing accent: ",
206+
0x301)
207+
)
208+
| add_child(
209+
character_test(parentWindow,
210+
"Smile Emjoi: ",
211+
0x1F60A
212+
)
213+
)
214+
| add_child(
215+
character_test(parentWindow,
216+
"Checkbox: ",
217+
U'')
218+
)
219+
| add_child(
220+
character_test(parentWindow,
221+
"Checkbox: ",
222+
U'')
223+
)
224+
| add_child(
225+
character_test(parentWindow,
226+
"RadioButton: ",
227+
U'')
228+
)
229+
)
230+
| add_child(
231+
NVerticalStackElement::create()
232+
| alignment(NAlignment::Start)
233+
| add_child(NCheckboxElement::create("Checkbox", true))
234+
| add_child(NCheckboxElement::create("Checkbox", false))
235+
| add_child(NRadioGroupElement::create(
236+
NOrientation::Vertical,
237+
{ "Radio 1", "Radio 2", "Radio 3" }, 1))
238+
)
239+
)
240+
| add_child(
241+
NHorizontalStackElement::create()
242+
| column_gap(3)
243+
| add_child(character_grid(parentWindow))
244+
| add_child(acs_grid(parentWindow))
245+
| add_child(device_grid(parentWindow))
246+
)
247+
| add_child(
248+
NHorizontalStackElement::create()
249+
| alignment(NAlignment::End)
250+
| add_child(
251+
NButtonElement::create("OK")
252+
//| is_default()
253+
| width(15)
254+
| on_clicked([windowRef = window->weak_ptr()](NMouseButton button, NClickedEventArgs& args) mutable {
255+
args.handled = true;
256+
windowRef.lock()->close();
257+
})
258+
)
259+
)
260+
)
261+
;
262+
263+
}
264+

0 commit comments

Comments
 (0)