Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/demo/scripts/native-parity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ const cases = [];
for (const [screen,draw] of Object.entries(screens)) {
for (const theme of ["dark", "dracula", "nord"]) {
for (const [width,height] of [[80,30],[120,40],[168,46],[200,60]]) {
// Both border modes. Collapsing changes the layout rather than only the
// glyphs, and covering just the open one is how every port shipped a `c`
// key that toggled a flag and moved nothing.
for (const collapsed of [false, true]) {
const state=createState(structuredClone(sample),"simulated",[]);
state.themeIndex=["dark","dracula","nord"].indexOf(theme);
const frame=renderToScreen(({ui,theme})=>draw(ui,state,theme),{width,height,theme});
state.collapsed=collapsed;
const frame=renderToScreen(({ui,theme})=>draw(ui,state,theme),{width,height,theme,collapseBorders:collapsed});
const hashes=[];
for(let y=0;y<height;y++) {
let h=2166136261;
Expand All @@ -25,7 +30,8 @@ for (const theme of ["dark", "dracula", "nord"]) {
}
hashes.push(h);
}
cases.push({screen,width,height,theme,text:frame.text(),hashes});
cases.push({screen,width,height,theme,collapsed,text:frame.text(),hashes});
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/content/widgets.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,7 @@
"width": 40,
"height": 12,
"blurb": "A semicircular dial. Wants at least nine columns by five rows.",
"preview": "<pre class=\"hqtui-preview\" style=\"background:#05070a;color:#c6d0db;padding:14px;font-size:13px;line-height:15.34px;font-family:ui-monospace,SFMono-Regular,Menlo,&#39;DejaVu Sans Mono&#39;,&#39;Liberation Mono&#39;,Consolas,&#39;Segoe UI Symbol&#39;,monospace;margin:0;overflow-x:auto;border-radius:8px;white-space:pre;font-variant-ligatures:none;-webkit-font-smoothing:antialiased\"><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⣠⣤⡶⠶⠞⠛⠛⠛⠛⠛⠛⠛⠶⠶⣦⣤⣀</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⣠⡶⠟⠋⠉</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠈⠉⠛⠷⣦⣀</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⡴⠟⠁</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠙⠷⣄</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⡴⠋</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠈⠳⣄</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⣠⠟</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠘⢧⡀</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢠⠏</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠈⢧</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⡟</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠘⡇</span>\n<span style=\"color:#ff6b6b;background:#05070a\">⢸⠃</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢻</span>\n<span style=\"color:#ff6b6b;background:#05070a;font-weight:700\"> 62% </span></pre>",
"preview": "<pre class=\"hqtui-preview\" style=\"background:#05070a;color:#c6d0db;padding:14px;font-size:13px;line-height:15.34px;font-family:ui-monospace,SFMono-Regular,Menlo,&#39;DejaVu Sans Mono&#39;,&#39;Liberation Mono&#39;,Consolas,&#39;Segoe UI Symbol&#39;,monospace;margin:0;overflow-x:auto;border-radius:8px;white-space:pre;font-variant-ligatures:none;-webkit-font-smoothing:antialiased\"><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⣠⣤⡶⠶⠞⠛⠛⠛⠛⠛⠛⠛⠶⠶⣦⣤⣀</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⣠⡶⠞⠋⠉</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠈⠉⠛⠷⣦⣀</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⡴⠟⠁</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠙⠷⣄</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢀⡴⠋</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠈⠳⣄</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⣠⠟</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠘⢧⡀</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢠⠏</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠈⢧</span><span style=\"color:#c6d0db;background:#05070a\"> </span>\n<span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⡟</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⠘⡇</span>\n<span style=\"color:#ff6b6b;background:#05070a\">⢸⠃</span><span style=\"color:#c6d0db;background:#05070a\"> </span><span style=\"color:#ff6b6b;background:#05070a\">⢻</span>\n<span style=\"color:#ff6b6b;background:#05070a;font-weight:700\"> 62% </span></pre>",
"examples": {
"typescript": {
"code": "export function gauge(ui: Container, _theme: Theme): void {\n // A semicircular dial. Wants at least 9x5.\n ui.gauge({ value: 62, label: \"62%\" });\n}",
Expand Down
18 changes: 16 additions & 2 deletions packages/hqtui/src/graphics/braille.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ const DOT_BITS = [
[0x40, 0x80],
];

/**
* Round, treating a value within a hair of .5 as the tie it mathematically is.
*
* Plot geometry runs through cos and sin, and libm implementations differ by an
* ULP at the exact angles where a coordinate lands on .5 -- cos(120 degrees) is
* -0.5, and V8 returns a hair under it where Go returns a hair over. Rounding
* the raw value makes that ULP decide a pixel, so the same widget at the same
* size differs between ports. The tolerance is far wider than an ULP and far
* narrower than anything geometric.
*/
function snap(v: number): number {
return Math.round(v + (v >= 0 ? 1e-9 : -1e-9));
}

export class BrailleCanvas {
/** Pixel dimensions (cells * 2 wide, cells * 4 tall). */
readonly width: number;
Expand All @@ -38,8 +52,8 @@ export class BrailleCanvas {
* everything, is ignored too rather than landing in cell 0.
*/
pixel(x: number, y: number): void {
const px = Math.round(x);
const py = Math.round(y);
const px = snap(x);
const py = snap(y);
if (!(px >= 0 && py >= 0 && px < this.width && py < this.height)) return;
const cell = (py >> 2) * this.cols + (px >> 1);
this.dots[cell] |= DOT_BITS[py & 3][px & 1];
Expand Down
4 changes: 4 additions & 0 deletions ports/bindings/include/hqtui_bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ void hqb_destroy(hqb_scene *scene);
* A rejected update leaves the last valid scene intact. See PROTOCOL.md. */
int hqb_set(hqb_scene *scene, const char *json, size_t length);
int hqb_resize(hqb_scene *scene, int width, int height);
/* Merge the borders of adjacent panels into shared lines, the way CSS collapses
* table borders. Applies to the next render or demo frame, and to every one
* after it until changed. */
int hqb_collapse(hqb_scene *scene, int enabled);
/* text, ansi (full frame), diff (against last rendered frame), hashes (tests).
*/
const char *hqb_render(hqb_scene *scene, const char *format);
Expand Down
16 changes: 13 additions & 3 deletions ports/bindings/src/bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,13 @@ struct hqb_scene {
const hq_theme *theme;
Json tree;
std::string output, input;
bool first = true;
bool first = true, collapse = false;
std::unique_ptr<demo::Terminal> terminal;
hqb_scene(int w, int h, const hq_theme *t)
: previous(w, h), frame(w, h), theme(t) {}
void paint(const Json &scene) {
frame.clear(theme->background, theme->foreground);
UI ui(frame.surface(theme));
UI ui(frame.surface(theme), false, 0, nullptr, collapse);
std::vector<PendingOverlay> overlays;
node(ui, scene, overlays);
ui.flush();
Expand Down Expand Up @@ -487,6 +487,13 @@ int hqb_resize(hqb_scene *s, int w, int h) {
s->first = true;
});
}
int hqb_collapse(hqb_scene *s, int enabled) {
return checked([&] {
if (!s)
throw std::runtime_error("closed scene");
s->collapse = enabled != 0;
});
}
const char *hqb_render(hqb_scene *s, const char *format) {
if (!checked([&] {
if (!s || !format)
Expand All @@ -513,8 +520,11 @@ const char *hqb_demo_frame(hqb_scene *s, const char *screen,
for (int t = 0; t < 9; t++)
if (std::string(s->theme->name) == demo::themes[t])
state.theme_index = t;
// Collapsing changes the layout, not only the glyphs: the screens close
// the seams between panels so their borders have something to merge.
state.collapse = s->collapse;
s->frame.clear(s->theme->background, s->theme->foreground);
UI ui(s->frame.surface(s->theme));
UI ui(s->frame.surface(s->theme), false, 0, nullptr, s->collapse);
demo::render(ui, state, true);
ui.flush();
s->finish(format);
Expand Down
3 changes: 2 additions & 1 deletion ports/bindings/tests/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def main():
frames=[json.loads(line) for line in result.stdout.splitlines()]
assert len(frames)==len(all_cases),(language,len(frames))
failures=[(case['screen'],case['width'],case['height'],case['theme'],
'collapsed' if case.get('collapsed') else 'open',
[i for i,(a,b) in enumerate(zip(actual,case['hashes'])) if a!=b])
for case,actual in zip(cases,frames) if actual!=case['hashes']]
assert not failures,(language,failures)
Expand All @@ -106,7 +107,7 @@ def main():
output=invoke(language,f'examples/dashboard.{suffix}',
['--real','--snapshot','--screen',screen,'--width','200','--height','60']).stdout
assert 'simulated' not in output and len(output.splitlines())==60,(language,screen)
print(f'{language}: 120 exact reference frames, 6 custom frames, ten tabs, custom app, resize and q/signals passed',flush=True)
print(f'{language}: {len(cases)} exact reference frames (both border modes), 6 custom frames, ten tabs, custom app, resize and q/signals passed',flush=True)
if sys.platform.startswith('linux'):
demos={language:[*command,str(ROOT/'ports'/language/'examples'/f'dashboard.{EXT[language]}')]
for language,command in COMMANDS.items()}
Expand Down
Loading