aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile16
-rw-r--r--docs/design/003-pixel-surface.md33
-rw-r--r--docs/design/004-text.md3
-rw-r--r--surfaces/draw.c102
-rw-r--r--surfaces/main.c78
-rw-r--r--surfaces/pixels.c322
-rw-r--r--surfaces/vega.h51
-rw-r--r--symbols/symbols-tool.c9
-rw-r--r--tests/check.py89
9 files changed, 612 insertions, 91 deletions
diff --git a/Makefile b/Makefile
index b57de0f..6ca6183 100644
--- a/Makefile
+++ b/Makefile
@@ -24,8 +24,13 @@ $(BUILD):
$(LIBPURE): FORCE
$(MAKE) -C $(PURE) build/libpure.a
-$(BUILD)/vega: $(SURFACES) surfaces/vega.h surfaces/dbus.h symbols/symbols.c symbols/symbols.h $(LIBPURE) | $(BUILD)
- $(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core -I$(PURE)/serve -Isymbols $(SURFACES) symbols/symbols.c $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@
+# The pixel surfaces set type: the text modules come in (TYPE, below).
+TYPE = text/layout.c text/shape.c text/syllabic-data.c text/bidi.c text/linebreak.c text/grapheme.c text/unicode-data.c
+TYPE_HEADERS = text/layout.h text/shape.h text/text.h text/syllabic.h
+$(BUILD)/vega: $(SURFACES) surfaces/vega.h surfaces/dbus.h symbols/symbols.c symbols/symbols.h $(TYPE) $(TYPE_HEADERS) \
+ $(LIBPURE) | $(BUILD)
+ $(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core -I$(PURE)/serve -Isymbols -Itext $(SURFACES) symbols/symbols.c $(TYPE) \
+ $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@
# The same host as a surface a Pure node maps (pure-serve --surface, Pure's
# serve/surface.h): the node's display. It calls the app model pure-serve
@@ -34,10 +39,11 @@ $(BUILD)/vega: $(SURFACES) surfaces/vega.h surfaces/dbus.h symbols/symbols.c sym
# when Pure's headers change: a node refuses a surface built for another
# layout of its app and module.
PURE_HEADERS = $(wildcard $(PURE)/core/*.h) $(PURE)/serve/surface.h
-$(BUILD)/vega-surface.so: $(SURFACES) surfaces/vega.h surfaces/dbus.h symbols/symbols.c symbols/symbols.h $(PURE_HEADERS) | $(BUILD)
+$(BUILD)/vega-surface.so: $(SURFACES) surfaces/vega.h surfaces/dbus.h symbols/symbols.c symbols/symbols.h $(TYPE) \
+ $(TYPE_HEADERS) $(PURE_HEADERS) | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-unused-function -fPIC -shared -fvisibility=hidden -ffunction-sections \
- -fdata-sections -DVEGA_SURFACE -I$(PURE)/core -I$(PURE)/serve -Isymbols $(SURFACES) symbols/symbols.c \
- $(LDFLAGS) -Wl,--gc-sections -lm $(LDLIBS) -o $@
+ -fdata-sections -DVEGA_SURFACE -I$(PURE)/core -I$(PURE)/serve -Isymbols -Itext $(SURFACES) symbols/symbols.c \
+ $(TYPE) $(LDFLAGS) -Wl,--gc-sections -lm $(LDLIBS) -o $@
# Building symbol files reads fonts through FreeType, and compiles and checks
# their shaping with HarfBuzz; reading them needs neither.
diff --git a/docs/design/003-pixel-surface.md b/docs/design/003-pixel-surface.md
index 725f3b1..7e60e58 100644
--- a/docs/design/003-pixel-surface.md
+++ b/docs/design/003-pixel-surface.md
@@ -28,6 +28,29 @@
Raspberry Pi 5. todo 예제에 한글 항목 넷, 48×12칸, 20px: 실행 전체(3.6 MB 심볼 파일 열기, 글리프 약 40개 굽기, 그리기,
PPM 쓰기)가 7 ms다. 192×45칸(1920×1350)은 프레임마다 12 ms다(그리기, 합성, 7.8 MB PPM을 `/dev/shm`에 쓰기).
+## 조판: 격자가 놓고, 글꼴이 짠다
+
+칸은 터미널의 단위다. 사람이 보는 화면에서 고정폭 칸에 한 글자씩 두면 아랍 문자는 이어지지 않고, 데바나가리는 결합자를 못
+만들고, 라틴은 커닝이 없다. 칸마다 그리는 길은 셰이핑([004](004-text.md))을 쓸 자리가 없다. 그렇다고 칸의 레이아웃을 버리고
+박스 레이아웃을 새로 두면 TUI와 GUI가 두 레이아웃이 된다. 그래서 레이아웃은 칸의 것을 그대로 쓰고, 글을 짜는 일만 픽셀이 한다.
+
+- `vega_draw`는 칸을 채우면서 쓴 글을 스팬으로도 남긴다(`VegaSpan`: 행, 받은 열 [column, end), 칸이 닿은 곳, UTF-8, 톤).
+ 같은 펜이 멈춘 자리에서 이어 쓴 스팬은 앞 스팬을 따른다(필드 이름과 값, 도움말 줄, 입력 줄). 펜을 옮겨 쓴 스팬(표의 열,
+ 목록의 라벨, 배지)은 제 열에서 새로 시작한다. 배지는 끝에 붙인다.
+- 첫 글꼴이 셰이핑을 가지면(`--shape`로 만든 심볼 파일) 픽셀은 글을 짠다. 스팬과 그것을 따르는 스팬들이 한 줄의 글이
+ 된다. 한 문단으로 짜므로(`vega_layout`, 방향은 첫 강한 문자에서) 양방향과 커닝이 스팬 경계를 넘는다. 글리프의 색은 그
+ 문자가 온 스팬의 톤이다. 줄은 제 열에서 시작해, 스팬들의 끝과 행의 다음 스팬의 열 중 가까운 곳까지 쓸 수 있다. 넘치면 자소
+ 클러스터 경계에서 자르고 말줄임표를 붙인다(이분 탐색, 들어가는 가장 긴 앞부분). 입력 줄에는 글 뒤에 커서를 그린다.
+- 글꼴은 `--symbols`를 되풀이해 선호 순서로 준다(최대 8개). 클러스터마다 그것을 가진 첫 글꼴로 간다. 칸의 크기는 첫 글꼴의
+ 숫자와 줄이다. 다른 글꼴의 글리프도 같은 기준선에 놓이고, 줄 밖으로 나가는 잉크(데바나가리의 아래 모음 부호 등)는 자르지
+ 않는다. 첫 글꼴이 셰이핑을 갖지 않으면 전처럼 칸마다 그리되, 칸의 글자도 그것을 가진 첫 글꼴에서 가져온다.
+- 셰이핑할 수 없는 줄(어느 글꼴도 그 문자 체계의 계획이 없다)은 그 스팬들의 칸으로 그린다.
+
+DejaVu Sans(라틴·아랍), Noto Sans CJK KR(한글), Noto Sans Devanagari를 20px로 쌓은 todo 화면에서 한글 항목, 결합한 아랍
+낱말, 데바나가리 결합자(स्त), 커닝(AV, Wa), 한 줄 안의 양방향("hello 123 مرحبا world"), 끝에 붙은 배지, 말줄임표, 커서를
+눈으로 확인했다. 100×50칸(1200×1200)에 여러 문자 체계의 줄 50개를 짜는 데 프레임마다 약 2.9 ms가 더 든다(칸으로 그리기
+대비, 40프레임 0.39 s 대 0.28 s, PPM 쓰기 포함).
+
## 디스플레이(KMS)
`vega display MODULE --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WxH --out PREFIX] < script`: 같은 프레임을
@@ -62,8 +85,16 @@ SIGTERM)에서 멈춘다.
`tests/check.py` 11번. 같은 스크립트를 `tui --dump`와 `pixels`로 돌려 글이 같고, 그림에서 빈 칸은 그 행의 배경(포커스 행은 제
색)뿐이며 글자가 있는 칸에는 잉크가 있다(프레임 둘, 칸 1,024개).
+조판은 11번의 둘째 부분이다. DejaVu Sans, Noto Sans CJK KR, FreeSans(데바나가리)를 쌓는다. 목록에 혼자 있는 한 문자 체계의
+라벨(라틴 커닝, 한글, 아랍, 데바나가리)은 그것을 가진 글꼴 하나로 `vega-symbols paragraph`가 짠 그림과 픽셀마다 같다(커버리지
+±4, 57,408픽셀). 그래서 클러스터마다 맞는 글꼴로 가서 레이아웃이 말하는 대로 셰이핑되고 놓였다. 클러스터를 늘 첫 글꼴로
+보내는 변이를 넣으면 한글 라벨에서 잡는다. 여러 라벨에서는 행에 안 들어가는 라벨이 행 안에서 잘리고, 글이 없는 행은 비어
+있고, 입력 줄에 커서가 있다.
+
## 다음
- 바뀐 칸만 다시 그리고 복사한다(TUI가 이미 칸마다 보인 것을 기억한다). 커밋을 기다리지 않고 페이지 넘김 이벤트로 맞춘다.
- 입력: 한글 입력기(지금은 US 배열의 글자만 친다), 제스처(끌기로 스크롤).
-- 비례 글꼴의 줄: 칸이 아닌 글자 폭으로 놓는 표와 문단. 합성은 GPU로(아틀라스, [002](002-symbol-format.md)).
+- 조판의 캐시: 바뀌지 않은 줄은 다시 짜지 않는다(지금은 프레임마다 모든 줄을 짠다). 합성은 GPU로(아틀라스,
+ [002](002-symbol-format.md)).
+- 칸이 아닌 글자 폭으로 놓는 표와 여러 줄 문단(지금은 한 줄을 받은 열 안에 짠다).
diff --git a/docs/design/004-text.md b/docs/design/004-text.md
index 8325109..4e8e7fb 100644
--- a/docs/design/004-text.md
+++ b/docs/design/004-text.md
@@ -261,7 +261,8 @@ U+180B–180E만 넣고 네 번째 자유 이체 선택자(U+180F)를 빠뜨렸�
- 글리프의 클러스터가 자기 줄 안에 있다.
- 글리프가 `vega_bidi_order`(줄 끝의 L1 뒤)가 문자를 놓는 순서대로 놓인다.
-3만 문단(565만 문자, 22.9만 줄)에서 어긋남이 없다. 문자당 776 ns다(재기 위한 셰이핑과 줄마다의 셰이핑 두 번, 양방향, 줄 바꿈
+3만 문단(565만 문자, 22.9만 줄)에서 어긋남이 없다. 글꼴 목록은 픽셀 표면의 조판이 끝에서 끝까지 확인한다
+([003](003-pixel-surface.md)). 문자당 776 ns다(재기 위한 셰이핑과 줄마다의 셰이핑 두 번, 양방향, 줄 바꿈
포함).
`vega-symbols paragraph`는 문단을 짜서 심볼로 그린다. 라틴(ffi·fi·fj 합자, AVAWA 커닝), 아랍(연결, lam-alef, 부호,
diff --git a/surfaces/draw.c b/surfaces/draw.c
index 31188c7..ec0b536 100644
--- a/surfaces/draw.c
+++ b/surfaces/draw.c
@@ -199,6 +199,7 @@ void vega_say(FILE *out, PureApp *app, const VegaScreen *screen, bool all) {
/* ---- TUI ------------------------------------------------------------ */
bool vega_grid(VegaGrid *grid, unsigned width, unsigned height) {
+ grid->span_count = grid->text_used = 0;
size_t count = (size_t)width * height;
VegaCell *cells = calloc(count ? count : 1, sizeof(*cells)), *shown = calloc(count ? count : 1, sizeof(*shown));
struct VegaRow *rows = calloc(height ? height : 1, sizeof(*rows));
@@ -220,14 +221,73 @@ void vega_grid_free(VegaGrid *grid) {
free(grid->cells);
free(grid->shown);
free(grid->rows);
+ free(grid->spans);
+ free(grid->text);
*grid = (VegaGrid){0};
}
-typedef struct { VegaGrid *grid; unsigned row, column, end; uint8_t tone; bool focus, strong; } Pen;
+/* Where text goes. reach is where the pen's last text stopped (written: it
+ * wrote some): text written there follows it. right sets its text flush
+ * with its end, when set as type. */
+typedef struct {
+ VegaGrid *grid;
+ unsigned row, column, end;
+ uint8_t tone;
+ bool focus, strong, written, right;
+ unsigned reach;
+} Pen;
+
+static Pen pen_at(VegaGrid *grid, unsigned row, unsigned column, unsigned end, uint8_t tone, bool focus, bool strong) {
+ return (Pen){grid, row, column, end, tone, focus, strong, false, false, 0};
+}
+
+/* The text as a span of the columns [pen's column, end), for a surface that
+ * sets type (the cells get it too; after them, stop() says where they
+ * reached). Out of memory, the span is lost: the cells still show it. */
+static void span(Pen *pen, const char *text, size_t length, unsigned end) {
+ VegaGrid *g = pen->grid;
+ if (!length || pen->column >= end) return;
+ if (g->span_count == g->spans_allocated) {
+ size_t n = g->spans_allocated ? 2 * g->spans_allocated : 64;
+ VegaSpan *grown = realloc(g->spans, n * sizeof(*grown));
+ if (!grown) return;
+ g->spans = grown;
+ g->spans_allocated = n;
+ }
+ if (g->text_used + length > g->text_allocated) {
+ size_t n = g->text_allocated ? g->text_allocated : 4096;
+ while (n < g->text_used + length) n *= 2;
+ char *grown = realloc(g->text, n);
+ if (!grown) return;
+ g->text = grown;
+ g->text_allocated = n;
+ }
+ memcpy(g->text + g->text_used, text, length);
+ g->spans[g->span_count++] = (VegaSpan){pen->row, pen->column, end, pen->column, (uint32_t)g->text_used,
+ (uint32_t)length, pen->tone, pen->focus, pen->strong,
+ pen->written && pen->reach == pen->column, pen->right};
+ g->text_used += length;
+}
+
+/* The pen's text was written: where its cells reached. */
+static void stop(Pen *pen, size_t spans) {
+ VegaGrid *g = pen->grid;
+ if (g->span_count > spans) g->spans[g->span_count - 1].reach = pen->column;
+ pen->written = true;
+ pen->reach = pen->column;
+}
/* Write text from the pen's column up to its end; a wide character that
* does not fit is left out, and a mark joins the character before it. */
+static void put_cells(Pen *pen, const char *text, size_t length);
static void write_text(Pen *pen, const char *text, size_t length) {
+ size_t spans = pen->grid->span_count;
+ span(pen, text, length, pen->end);
+ put_cells(pen, text, length);
+ stop(pen, spans);
+}
+
+static void put_cells(Pen *pen, const char *text, size_t length) {
VegaCell *line = pen->grid->cells + (size_t)pen->row * pen->grid->width;
for (size_t at = 0; at < length;) {
uint32_t codepoint;
@@ -261,16 +321,19 @@ static void fill(Pen *pen) {
/* Text cut to width with an ellipsis. */
static void write_fit(Pen *pen, const char *text, size_t length, unsigned width) {
unsigned end = pen->end;
+ size_t spans = pen->grid->span_count;
if (pen->column + width < end) pen->end = pen->column + width;
+ span(pen, text, length, pen->end);
if (vega_text_width(text, length) > pen->end - pen->column && pen->end > pen->column) {
--pen->end;
- write_text(pen, text, length);
+ put_cells(pen, text, length);
pen->end = end;
- write_cstr(pen, "\xe2\x80\xa6");
+ put_cells(pen, "\xe2\x80\xa6", 3);
} else {
- write_text(pen, text, length);
+ put_cells(pen, text, length);
pen->end = end;
}
+ stop(pen, spans);
}
static const char *key_symbol(const char *key) {
@@ -294,8 +357,9 @@ static void draw_list(VegaGrid *grid, PureApp *app, const VegaPart *part, int in
grid->rows[top + r] = (struct VegaRow){index, row};
bool focused = row == focus;
VegaTone tone = vega_tone(app, part, row);
- Pen pen = {grid, top + r, 0, grid->width, (uint8_t)tone, focused, false};
+ Pen pen = pen_at(grid, top + r, 0, grid->width, (uint8_t)tone, focused, false);
write_cstr(&pen, focused ? "> " : " ");
+ pen.written = false; /* the label starts at its column, however wide the marker is set */
char badge[VEGA_TEXT];
size_t badge_length = 0;
if (part->badge >= 0 && !vega_text(app, (uint32_t)part->badge, row, badge, &badge_length)) badge_length = 0;
@@ -314,8 +378,13 @@ static void draw_list(VegaGrid *grid, PureApp *app, const VegaPart *part, int in
if (badge_length && grid->width >= badge_width) {
/* The badge ends at the last column, two cells after the label at least. */
unsigned start = grid->width - (badge_width - 2);
- if (pen.column < start) fill(&(Pen){grid, pen.row, pen.column, start, pen.tone, focused, false});
+ if (pen.column < start) {
+ Pen gap = pen_at(grid, pen.row, pen.column, start, pen.tone, focused, false);
+ fill(&gap);
+ }
pen.column = start > pen.column ? start : pen.column;
+ pen.written = false;
+ pen.right = true;
write_fit(&pen, badge, badge_length, badge_width - 2);
}
fill(&pen);
@@ -344,7 +413,7 @@ static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, int i
for (unsigned c = 0; c < part->column_count; ++c) if (widths[c] > share) widths[c] = share;
}
if (!height) return;
- Pen head = {grid, top, 2, grid->width, VEGA_MUTED, false, true};
+ Pen head = pen_at(grid, top, 2, grid->width, VEGA_MUTED, false, true);
for (unsigned c = 0; c < part->column_count; ++c) {
const char *name = m->view_names[part->columns[c]];
unsigned start = head.column;
@@ -355,13 +424,17 @@ static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, int i
size_t row = first + r;
bool focused = row == focus;
grid->rows[top + 1 + r] = (struct VegaRow){index, row};
- Pen pen = {grid, top + 1 + r, 0, grid->width, (uint8_t)vega_tone(app, part, row), focused, false};
+ Pen pen = pen_at(grid, top + 1 + r, 0, grid->width, (uint8_t)vega_tone(app, part, row), focused, false);
write_cstr(&pen, focused ? "> " : " ");
+ pen.written = false;
for (unsigned c = 0; c < part->column_count; ++c) {
unsigned start = pen.column, end = start + widths[c] + 2 < grid->width ? start + widths[c] + 2 : grid->width;
if (vega_text(app, part->columns[c], row, text, &length)) write_fit(&pen, text, length, widths[c]);
else write_cstr(&pen, "-");
- if (pen.column < end) fill(&(Pen){grid, pen.row, pen.column, end, pen.tone, focused, false});
+ if (pen.column < end) {
+ Pen gap = pen_at(grid, pen.row, pen.column, end, pen.tone, focused, false);
+ fill(&gap);
+ }
pen.column = end;
}
fill(&pen);
@@ -378,6 +451,7 @@ bool vega_hit(const VegaGrid *grid, unsigned line, unsigned *part, size_t *item)
void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const char *prompt, size_t prompt_length,
bool typing) {
const PureModule *m = app->module;
+ grid->span_count = grid->text_used = 0;
for (size_t i = 0; i < (size_t)grid->width * grid->height; ++i) grid->cells[i] = (VegaCell){' ', 0, 0, false, false};
for (unsigned r = 0; r < grid->height; ++r) grid->rows[r].part = -1;
grid->cursor_row = grid->cursor_column = -1;
@@ -386,7 +460,7 @@ void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const cha
size_t length;
unsigned row = 0, bottom = grid->height - 1 - (screen->prompt >= 0 && grid->height > 1);
if (screen->title >= 0 && row < bottom && vega_text(app, (uint32_t)screen->title, 0, text, &length)) {
- Pen pen = {grid, row++, 0, grid->width, VEGA_NORMAL, false, true};
+ Pen pen = pen_at(grid, row++, 0, grid->width, VEGA_NORMAL, false, true);
write_fit(&pen, text, length, grid->width);
}
unsigned groups = 0;
@@ -394,7 +468,7 @@ void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const cha
const VegaPart *part = &screen->parts[i];
if (part->kind != VEGA_FIELD) { ++groups; continue; }
if (row >= bottom) continue;
- Pen pen = {grid, row++, 0, grid->width, VEGA_MUTED, false, false};
+ Pen pen = pen_at(grid, row++, 0, grid->width, VEGA_MUTED, false, false);
write_cstr(&pen, m->view_names[part->view]);
write_cstr(&pen, " ");
pen.tone = VEGA_NORMAL;
@@ -411,7 +485,7 @@ void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const cha
++group;
if (!share) continue;
size_t rows = vega_rows(app, part), focus = vega_focus(app, part);
- Pen head = {grid, row, 0, grid->width, VEGA_NORMAL, false, true};
+ Pen head = pen_at(grid, row, 0, grid->width, VEGA_NORMAL, false, true);
write_cstr(&head, m->view_names[part->view]);
char count[32];
snprintf(count, sizeof(count), " (%zu)", rows);
@@ -426,7 +500,7 @@ void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const cha
row += share;
}
/* Keys the app answers to, then the text being typed. */
- Pen help = {grid, bottom, 0, grid->width, VEGA_MUTED, false, false};
+ Pen help = pen_at(grid, bottom, 0, grid->width, VEGA_MUTED, false, false);
if (screen->prompt >= 0) write_cstr(&help, typing ? "Tab keys " : "Tab type ");
for (uint32_t i = 0; i < m->inputs; ++i) {
if (m->input[i].kind != PURE_KIND_KEY) continue;
@@ -439,7 +513,7 @@ void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const cha
write_cstr(&help, " ");
}
if (screen->prompt >= 0 && grid->height > 1) {
- Pen pen = {grid, grid->height - 1, 0, grid->width, typing ? VEGA_ACCENT : VEGA_MUTED, false, false};
+ Pen pen = pen_at(grid, grid->height - 1, 0, grid->width, typing ? VEGA_ACCENT : VEGA_MUTED, false, false);
write_cstr(&pen, m->input_names[screen->prompt]);
write_cstr(&pen, "> ");
pen.tone = typing ? VEGA_NORMAL : VEGA_MUTED;
diff --git a/surfaces/main.c b/surfaces/main.c
index d58625a..6c7e368 100644
--- a/surfaces/main.c
+++ b/surfaces/main.c
@@ -21,10 +21,11 @@
* say sentences (the first natural-language surface), driven by a script
* web [--listen ADDRESS] [--port N]
* a web page (a GUI), on 127.0.0.1 unless another address is given
- * pixels --symbols FONT.vsym [--size PX] --grid WxH --out PREFIX
- * the TUI's frames drawn with a font's glyphs, driven by a
- * script: each show writes PREFIX-N.ppm after the text
- * display --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WxH --out PREFIX]
+ * pixels --symbols FONT.vsym... [--size PX] --grid WxH --out PREFIX
+ * the TUI's frames drawn in fonts (in order of preference;
+ * set as type when the first shapes), driven by a script:
+ * each show writes PREFIX-N.ppm after the text
+ * display --symbols FONT.vsym... [--size PX] [--card PATH] [--writeback WxH --out PREFIX]
* [--live | --input EVENTS]
* the same on a display through KMS, as many cells as fit,
* driven by a script, or live: keyboards, mice and touch
@@ -865,22 +866,37 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
/* Surfaces that draw glyphs. */
static bool glyphed(const char *surface) { return !strcmp(surface, "pixels") || !strcmp(surface, "display"); }
-/* The symbols a pixel surface draws with, read whole and kept. */
-static bool open_face(const char *path, float size) {
- static VegaSymbols symbols;
- FILE *f = fopen(path, "rb");
- long length = f && !fseek(f, 0, SEEK_END) ? ftell(f) : -1;
- unsigned char *bytes = length > 0 && !fseek(f, 0, SEEK_SET) ? malloc((size_t)length) : NULL;
- bool ok = bytes && fread(bytes, 1, (size_t)length, f) == (size_t)length;
- if (f) fclose(f);
- char error[128] = "cannot read it";
- ok = ok && vega_symbols_open(&symbols, bytes, (size_t)length, error, sizeof(error));
- if (ok && !vega_face_open(&face, &symbols, size)) {
- ok = false;
- snprintf(error, sizeof(error), "no line and digit to lay a grid out with");
+/* The fonts a pixel surface draws with (--symbols, in order of
+ * preference), read whole and kept. */
+static const char *symbols_paths[VEGA_FACES];
+static unsigned symbols_count;
+static bool add_symbols(const char *path) {
+ if (symbols_count == VEGA_FACES) return false;
+ symbols_paths[symbols_count++] = path;
+ return true;
+}
+static bool open_face(float size) {
+ static VegaSymbols symbols[VEGA_FACES];
+ const VegaSymbols *opened[VEGA_FACES];
+ char error[160] = "cannot read it";
+ for (unsigned k = 0; k < symbols_count; ++k) {
+ const char *path = symbols_paths[k];
+ FILE *f = fopen(path, "rb");
+ long length = f && !fseek(f, 0, SEEK_END) ? ftell(f) : -1;
+ unsigned char *bytes = length > 0 && !fseek(f, 0, SEEK_SET) ? malloc((size_t)length) : NULL;
+ bool ok = bytes && fread(bytes, 1, (size_t)length, f) == (size_t)length;
+ if (f) fclose(f);
+ if (!ok || !vega_symbols_open(&symbols[k], bytes, (size_t)length, error, sizeof(error))) {
+ fprintf(stderr, "vega: %s: %s\n", path, error);
+ return false;
+ }
+ opened[k] = &symbols[k]; /* the bytes stay: the face reads them */
}
- if (!ok) fprintf(stderr, "vega: %s: %s\n", path, error);
- return ok; /* the bytes stay: the face reads them */
+ if (!vega_face_open(&face, opened, symbols_count, size, error, sizeof(error))) {
+ fprintf(stderr, "vega: %s: %s\n", symbols_paths[0], error);
+ return false;
+ }
+ return true;
}
#ifdef VEGA_SURFACE
@@ -899,7 +915,7 @@ __attribute__((visibility("default"))) int pure_surface_run(const PureSurface *s
fputs("vega: this surface was built for another Pure\n", stderr);
return 2;
}
- const char *symbols_path = NULL, *card = "/dev/dri/card1", *recorded = NULL;
+ const char *card = "/dev/dri/card1", *recorded = NULL;
unsigned width = 0, height = 0;
float size = 16;
bool on_display = argc > 1 && !strcmp(argv[1], "display"), pixels = argc > 1 && !strcmp(argv[1], "pixels");
@@ -911,7 +927,7 @@ __attribute__((visibility("default"))) int pure_surface_run(const PureSurface *s
continue;
}
if (i + 1 >= argc) usage = true;
- else if (!strcmp(argv[i], "--symbols")) symbols_path = argv[i + 1];
+ else if (!strcmp(argv[i], "--symbols")) usage = !add_symbols(argv[i + 1]);
else if (!strcmp(argv[i], "--size") && sscanf(argv[i + 1], "%f", &size) == 1) {}
else if (!strcmp(argv[i], "--card") && on_display) card = argv[i + 1];
else if (!strcmp(argv[i], "--input")) recorded = argv[i + 1];
@@ -922,15 +938,15 @@ __attribute__((visibility("default"))) int pure_surface_run(const PureSurface *s
writeback = on_display;
} else usage = true;
}
- if (usage || !symbols_path || (writeback && !frames_prefix) || (pixels && (!width || !frames_prefix)) ||
+ if (usage || !symbols_count || (writeback && !frames_prefix) || (pixels && (!width || !frames_prefix)) ||
(live_input && recorded)) {
- fputs("vega-surface.so display --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WIDTHxHEIGHT --out PREFIX]\n"
+ fputs("vega-surface.so display --symbols FONT.vsym... [--size PX] [--card PATH] [--writeback WIDTHxHEIGHT --out PREFIX]\n"
" [--live | --input EVENTS]\n"
- "vega-surface.so pixels --symbols FONT.vsym [--size PX] --grid WIDTHxHEIGHT --out PREFIX [--input EVENTS]\n",
+ "vega-surface.so pixels --symbols FONT.vsym... [--size PX] --grid WIDTHxHEIGHT --out PREFIX [--input EVENTS]\n",
stderr);
return 2;
}
- if (!open_face(symbols_path, size)) return 2;
+ if (!open_face(size)) return 2;
unsigned columns = width, rows = height, pixels_across = width * (unsigned)face.cell_width,
pixels_down = height * (unsigned)face.cell_height;
if (on_display) {
@@ -967,7 +983,7 @@ __attribute__((visibility("default"))) int pure_surface_run(const PureSurface *s
#else
int main(int argc, char **argv) {
unsigned width = 0, height = 0, port = 8080;
- const char *symbols_path = NULL, *card = "/dev/dri/card1";
+ const char *card = "/dev/dri/card1";
float size = 16;
bool writeback = false, live_input = false;
const char *recorded = NULL;
@@ -1003,24 +1019,24 @@ int main(int argc, char **argv) {
writeback = true;
else if (!strcmp(argv[i], "--card") && !strcmp(argv[1], "display")) card = argv[i + 1];
else if (!strcmp(argv[i], "--input") && !strcmp(argv[1], "display")) recorded = argv[i + 1];
- else if (!strcmp(argv[i], "--symbols") && glyphed(argv[1])) symbols_path = argv[i + 1];
+ else if (!strcmp(argv[i], "--symbols") && glyphed(argv[1])) usage = !add_symbols(argv[i + 1]);
else if (!strcmp(argv[i], "--out") && glyphed(argv[1])) frames_prefix = argv[i + 1];
else if (!strcmp(argv[i], "--size") && glyphed(argv[1]) && sscanf(argv[i + 1], "%f", &size) == 1) {}
else usage = true;
}
bool pixels = argc > 1 && !strcmp(argv[1], "pixels"), on_display = argc > 1 && !strcmp(argv[1], "display");
if (usage || (dumped && strcmp(argv[1], "tui") && !pixels) || (!strcmp(argv[1], "check") && argc != 3) ||
- (pixels && (!dumped || !symbols_path || !frames_prefix)) || (on_display && (!symbols_path || (writeback && !frames_prefix)))) {
+ (pixels && (!dumped || !symbols_count || !frames_prefix)) || (on_display && (!symbols_count || (writeback && !frames_prefix)))) {
fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n"
"vega tui MODULE.pmod [--store PATH] [--watch] | --dump|--paint WIDTHxHEIGHT < script\n"
"vega web MODULE.pmod [--store PATH] [--listen ADDRESS] [--port N] [--watch]\n"
- "vega pixels MODULE.pmod [--store PATH] --symbols FONT.vsym [--size PX] --grid WIDTHxHEIGHT --out PREFIX < script\n"
- "vega display MODULE.pmod [--store PATH] --symbols FONT.vsym [--size PX] [--card PATH]\n"
+ "vega pixels MODULE.pmod [--store PATH] --symbols FONT.vsym... [--size PX] --grid WIDTHxHEIGHT --out PREFIX < script\n"
+ "vega display MODULE.pmod [--store PATH] --symbols FONT.vsym... [--size PX] [--card PATH]\n"
" [--writeback WIDTHxHEIGHT --out PREFIX] [--live | --input EVENTS] [--access] [< script]\n"
"vega check MODULE.pmod\n", stderr);
return 2;
}
- if ((pixels || on_display) && !open_face(symbols_path, size)) return 2;
+ if ((pixels || on_display) && !open_face(size)) return 2;
if (on_display) {
char error[256];
if (!(display = vega_display_open(card, writeback, writeback ? width : 0, writeback ? height : 0, error, sizeof(error)))) {
diff --git a/surfaces/pixels.c b/surfaces/pixels.c
index 304e96d..d58d5b2 100644
--- a/surfaces/pixels.c
+++ b/surfaces/pixels.c
@@ -1,33 +1,93 @@
#include "vega.h"
+#include "text.h"
#include <math.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-/* The pixel surface: the TUI's grid of cells, each drawn with its glyph
- * (drawn once per size, the exact area of its curves) in its tone's colour.
- * The view's cells go straight into pixels: no page, no scene. */
+/* The pixel surface: the TUI's grid drawn in a typeface, glyphs drawn once
+ * per size (the exact area of their curves) in their tone's colour, straight
+ * into pixels: no page, no scene. Two ways:
+ * - cells: each cell's character in its cell, in the first font that has it;
+ * - type, when the first font shapes: the layout's spans set as lines of
+ * text (text/layout.h). A span starts at its column, or where the text it
+ * follows stopped; it may go on to its end, but not past the next span's
+ * column that starts afresh; one that would is cut with an ellipsis. So
+ * the grid lays out (rows, columns, what is focused) and type shapes. */
/* Colours, 0xRRGGBB: the background, a focused row's, and one per tone. */
static const uint32_t background = 0x1e1e2e, focused = 0x313244, strong = 0xffffff;
static const uint32_t tones[VEGA_TONES] = {0xcdd6f4, 0x7f849c, 0x89b4fa, 0xa6e3a1, 0xf9e2af, 0xf38ba8};
-bool vega_face_open(VegaFace *face, const VegaSymbols *symbols, float pixels) {
+/* Whether a font that shapes has a character: its character map does, and
+ * its file holds that glyph (a file holds the glyphs its codes reach). */
+static bool has(void *context, unsigned font, uint32_t code) {
+ const VegaFace *face = context;
+ unsigned k = face->font_face[font];
+ uint32_t glyph = vega_shaping_glyph(&face->shaping[k], code);
+ VegaSymbol symbol;
+ return glyph && vega_symbol(face->symbols[k], VEGA_GLYPH_SYMBOLS + glyph, &symbol);
+}
+
+bool vega_face_open(VegaFace *face, const VegaSymbols *const *symbols, unsigned count, float pixels, char *error,
+ size_t error_size) {
*face = (VegaFace){0};
- if (!symbols->units || symbols->ascent <= symbols->descent || pixels < 4 || pixels > 256) return false;
- float scale = pixels / symbols->units;
+ face->fonts = (VegaFonts){{0}, 0, has, face};
+ face->pixels = pixels;
+ if (!count || count > VEGA_FACES || pixels < 4 || pixels > 256) {
+ snprintf(error, error_size, "1 to %d fonts, at 4 to 256 pixels", VEGA_FACES);
+ return false;
+ }
/* A cell is as wide as a digit (the grid is monospace: wide characters
- * take two) and as tall as the font's line. */
+ * take two) and as tall as the first font's line. */
+ const VegaSymbols *first = symbols[0];
VegaSymbol zero;
- if (!vega_symbol(symbols, '0', &zero) || zero.advance <= 0) return false;
+ if (!first->units || first->ascent <= first->descent || !vega_symbol(first, '0', &zero) || zero.advance <= 0) {
+ snprintf(error, error_size, "no line and digit to lay a grid out with");
+ return false;
+ }
+ float scale = pixels / first->units;
face->cell_width = (int)lroundf(zero.advance * scale);
- face->baseline = (int)ceilf(symbols->ascent * scale);
- face->cell_height = face->baseline + (int)ceilf(-symbols->descent * scale);
- face->glyphs = vega_glyphs_open(symbols, scale);
- return face->glyphs && face->cell_width > 0;
+ face->baseline = (int)ceilf(first->ascent * scale);
+ face->cell_height = face->baseline + (int)ceilf(-first->descent * scale);
+ if (face->cell_width <= 0) {
+ snprintf(error, error_size, "a digit narrower than a pixel");
+ return false;
+ }
+ for (unsigned k = 0; k < count; ++k) {
+ face->symbols[k] = symbols[k];
+ face->glyphs[k] = symbols[k]->units ? vega_glyphs_open(symbols[k], pixels / symbols[k]->units) : NULL;
+ face->count = k + 1;
+ if (!face->glyphs[k]) {
+ snprintf(error, error_size, "font %u: no glyphs to draw", k + 1);
+ goto fail;
+ }
+ /* Type is set when the first font shapes, in the fonts that do. */
+ if (!symbols[k]->shaping || (k && !face->fonts.count)) continue;
+ char why[128];
+ if (!vega_shaping_open(&face->shaping[k], symbols[k]->shaping, symbols[k]->shaping_size, why, sizeof(why))) {
+ snprintf(error, error_size, "font %u: %s", k + 1, why);
+ goto fail;
+ }
+ face->font_face[face->fonts.count] = k;
+ face->fonts.fonts[face->fonts.count++] = &face->shaping[k];
+ }
+ if (face->fonts.count && !(face->shaper = vega_shaper_new())) {
+ snprintf(error, error_size, "out of memory");
+ goto fail;
+ }
+ return true;
+fail:
+ vega_face_close(face);
+ return false;
}
void vega_face_close(VegaFace *face) {
- vega_glyphs_close(face->glyphs);
+ for (unsigned k = 0; k < face->count; ++k) vega_glyphs_close(face->glyphs[k]);
+ for (unsigned f = 0; f < face->fonts.count; ++f) vega_shaping_close(&face->shaping[face->font_face[f]]);
+ vega_shaper_free(face->shaper);
+ vega_layout_free(&face->layout);
+ free(face->work);
*face = (VegaFace){0};
}
@@ -60,9 +120,10 @@ static void draw(uint32_t *out, size_t stride, int width, int height, const Vega
}
}
-/* A character the symbols lack: a box in its cell. */
+/* A character no font has: a box where it goes. */
static void missing(uint32_t *out, size_t stride, int x, int y, int width, int height, int limit, uint32_t color) {
int left = x + 1, right = (x + width < limit ? x + width : limit) - 2, top = y + height / 5, bottom = y + height - height / 5;
+ if (left < 0 || right < left) return;
for (int i = left; i <= right; ++i) out[(size_t)top * stride + (size_t)i] = out[(size_t)bottom * stride + (size_t)i] = color;
for (int j = top; j <= bottom; ++j) out[(size_t)j * stride + (size_t)left] = out[(size_t)j * stride + (size_t)right] = color;
}
@@ -71,6 +132,220 @@ void vega_pixels_clear(uint32_t *out, size_t stride, unsigned width, unsigned he
fill(out, stride, 0, 0, (int)width, (int)height, background);
}
+static uint32_t color_of(uint8_t tone, bool is_strong) { return is_strong ? strong : tones[tone < VEGA_TONES ? tone : 0]; }
+
+/* Cells [first, end) of a row, each character in the first font that has it. */
+static void draw_cells(const VegaGrid *grid, VegaFace *face, unsigned r, unsigned first, unsigned end, uint32_t *out,
+ size_t stride, int width, int height) {
+ for (unsigned c = first; c < end && c < grid->width; ++c) {
+ const VegaCell *cell = &grid->cells[(size_t)r * grid->width + c];
+ if (!cell->ch || cell->ch == ' ') continue;
+ uint32_t color = color_of(cell->tone, cell->strong);
+ int x = (int)c * face->cell_width, y = (int)r * face->cell_height;
+ const VegaBitmap *glyph = NULL;
+ unsigned k = 0;
+ while (k < face->count && !(glyph = vega_glyph(face->glyphs[k], cell->ch))) ++k;
+ unsigned cells = vega_width(cell->ch) == 2 ? 2 : 1;
+ /* A wide character's glyph is centred in its two cells: a CJK font's full width is
+ * less than two cells of a monospace Latin font. */
+ int centred = glyph && cells == 2 && glyph->advance < 2 * face->cell_width
+ ? (2 * face->cell_width - glyph->advance) / 2 : 0;
+ if (!glyph) missing(out, stride, x, y, face->cell_width * (int)cells, face->cell_height, width, color);
+ else draw(out, stride, width, height, glyph, x + centred, y + face->baseline, color);
+ if (!cell->mark) continue;
+ for (k = 0; k < face->count; ++k)
+ if ((glyph = vega_glyph(face->glyphs[k], cell->mark))) {
+ draw(out, stride, width, height, glyph, x, y + face->baseline, color);
+ break;
+ }
+ }
+}
+
+/* ---- setting type ----------------------------------------------------- */
+
+/* What setting works in: a line's characters, room for trying it cut, its
+ * grapheme boundaries, where each of its spans starts in it, and the spans
+ * in the order they are set. */
+typedef struct {
+ uint32_t *codes, *cut;
+ uint8_t *clusters;
+ size_t *order, *starts;
+} Work;
+
+static bool work_for(VegaFace *face, const VegaGrid *grid, Work *w) {
+ /* A line's characters are at most the text's bytes. */
+ size_t most = grid->text_used;
+ size_t need = 2 * grid->span_count * sizeof(size_t) + (2 * most + 2) * sizeof(uint32_t) + most + 1;
+ if (need > face->work_size) {
+ void *grown = realloc(face->work, need);
+ if (!grown) return false;
+ face->work = grown;
+ face->work_size = need;
+ }
+ w->order = face->work;
+ w->starts = w->order + grid->span_count;
+ w->codes = (uint32_t *)(w->starts + grid->span_count);
+ w->cut = w->codes + most + 1;
+ w->clusters = (uint8_t *)(w->cut + most + 1);
+ return true;
+}
+
+/* A line set: where its pen starts (in the layout's x), how far all its
+ * glyphs go and how much of that shows (spaces at its end left out), its
+ * direction, and how many of its characters it keeps (all, or those before
+ * an ellipsis). */
+typedef struct { int32_t start, advance, shows; bool rtl; size_t kept; } Set;
+typedef enum { SET, NO_ROOM, UNSET } Setting;
+
+static bool set_line(VegaFace *face, const uint32_t *codes, size_t count, Set *out) {
+ VegaLayout *l = &face->layout;
+ if (!vega_layout(l, face->shaper, &face->fonts, codes, count, VEGA_BIDI_AUTO, 0) || l->line_count != 1) return false;
+ const VegaLine *line = &l->lines[0];
+ int32_t advance = 0;
+ for (size_t g = line->first; g < line->first + line->count; ++g) advance += l->glyphs[g].advance;
+ /* Right to left, the spaces at its end hang past its left edge. */
+ out->rtl = l->level & 1;
+ out->start = out->rtl ? line->left - (advance - line->width) : line->left;
+ out->advance = advance;
+ out->shows = line->width;
+ out->kept = count;
+ return true;
+}
+
+/* codes[0, count) cut after n characters, spaces before the cut left out,
+ * with an ellipsis: in w->cut, its length returned. */
+static size_t cut_at(Work *w, size_t n) {
+ while (n && vega_bidi_class(w->codes[n - 1]) == VEGA_BIDI_WS) --n;
+ memcpy(w->cut, w->codes, n * sizeof(*w->cut));
+ w->cut[n] = 0x2026;
+ return n + 1;
+}
+
+/* Set codes[0, count) to show within room (the first font's units): whole,
+ * or the longest start of it, of whole grapheme clusters, that fits with an
+ * ellipsis after it. */
+static Setting set_within(VegaFace *face, Work *w, size_t count, int32_t room, Set *out) {
+ if (!set_line(face, w->codes, count, out)) return UNSET;
+ if (out->shows <= room) return SET;
+ vega_graphemes(w->codes, count, w->clusters);
+ size_t low = 0, high = count, best = SIZE_MAX; /* the cut is a cluster boundary in [low, high) */
+ while (low < high) {
+ size_t middle = low + (high - low) / 2;
+ while (middle > low && !w->clusters[middle]) --middle;
+ Set tried;
+ if (!set_line(face, w->cut, cut_at(w, middle), &tried)) return UNSET;
+ if (tried.shows <= room) {
+ best = middle;
+ for (low = middle + 1; low < high && !w->clusters[low];) ++low;
+ } else
+ high = middle;
+ }
+ if (best == SIZE_MAX) return NO_ROOM;
+ size_t kept = best;
+ if (!set_line(face, w->cut, cut_at(w, best), out)) return UNSET;
+ out->kept = kept;
+ return SET;
+}
+
+/* The layout's glyphs with their pen starting at x (pixels), each in the
+ * colour of the span its character came from (an ellipsis: the one it
+ * cuts). */
+static void draw_set(VegaFace *face, const Set *set, const VegaSpan *const *spans, const size_t *starts, size_t count,
+ double x, int top, uint32_t *out, size_t stride, int width, int height) {
+ const VegaLayout *l = &face->layout;
+ double scale = (double)face->pixels / face->fonts.fonts[0]->upem;
+ for (size_t g = 0; g < l->glyph_count; ++g) {
+ const VegaPlaced *p = &l->glyphs[g];
+ size_t character = p->cluster < set->kept ? p->cluster : set->kept ? set->kept - 1 : 0, k = 0;
+ while (k + 1 < count && starts[k + 1] <= character) ++k;
+ uint32_t color = color_of(spans[k]->tone, spans[k]->strong);
+ unsigned font = face->font_face[p->font];
+ const VegaBitmap *b = vega_glyph(face->glyphs[font], VEGA_GLYPH_SYMBOLS + p->glyph);
+ int gx = (int)lround(x + (p->x - set->start) * scale);
+ if (b) draw(out, stride, width, height, b, gx, top + face->baseline - (int)lround(p->y * scale), color);
+ else if (p->advance > 0)
+ missing(out, stride, gx, top, (int)lround(p->advance * scale), face->cell_height, width, color);
+ }
+}
+
+static bool before(const VegaSpan *spans, size_t a, size_t b) {
+ const VegaSpan *x = &spans[a], *y = &spans[b];
+ if (x->row != y->row) return x->row < y->row;
+ if (x->column != y->column) return x->column < y->column;
+ return a < b;
+}
+
+/* Spans by row, then column, then as written: an insertion sort, as they
+ * come nearly in order. */
+static void sort_spans(size_t *order, size_t count, const VegaSpan *spans) {
+ for (size_t i = 0; i < count; ++i) order[i] = i;
+ for (size_t i = 1; i < count; ++i)
+ for (size_t k = i; k && before(spans, order[k], order[k - 1]); --k) {
+ size_t t = order[k];
+ order[k] = order[k - 1];
+ order[k - 1] = t;
+ }
+}
+
+/* Each line of text: a span, and those that follow it. It starts at the
+ * span's column (a right one ends at its end) and has until the nearest of
+ * its spans' ends and the next span on the row. */
+static bool set_type(const VegaGrid *grid, VegaFace *face, uint32_t *out, size_t stride, int width, int height) {
+ Work w;
+ if (!work_for(face, grid, &w)) return false;
+ size_t n = grid->span_count;
+ sort_spans(w.order, n, grid->spans);
+ double scale = (double)face->pixels / face->fonts.fonts[0]->upem;
+ int caret = -1;
+ const VegaSpan *line[VEGA_TEXT];
+ for (size_t i = 0, j; i < n; i = j) {
+ const VegaSpan *s = &grid->spans[w.order[i]];
+ unsigned end = s->end, reach = s->reach;
+ size_t spans = 0, count = 0;
+ for (j = i; j < n && spans < VEGA_TEXT; ++j) {
+ const VegaSpan *t = &grid->spans[w.order[j]];
+ if (j > i && (t->row != s->row || !t->follows)) break;
+ if (t->end < end) end = t->end;
+ reach = t->reach;
+ line[spans] = t;
+ w.starts[spans++] = count;
+ for (size_t at = 0; at < t->length;) {
+ uint32_t code;
+ at += vega_decode(grid->text + t->at, t->length, at, &code);
+ code = vega_visible(code);
+ w.codes[count++] = code == 0x2028 || code == 0x2029 ? ' ' : code; /* one line: no breaks in it */
+ }
+ }
+ if (j < n && grid->spans[w.order[j]].row == s->row && grid->spans[w.order[j]].column < end)
+ end = grid->spans[w.order[j]].column;
+ if (end > grid->width) end = grid->width;
+ if (end <= s->column) continue;
+ double box = (double)s->column * face->cell_width, limit = (double)end * face->cell_width;
+ int top = (int)s->row * face->cell_height;
+ Set set;
+ Setting setting = set_within(face, &w, count, (int32_t)floor((limit - box) / scale), &set);
+ if (setting == UNSET) {
+ /* No font can set it (none has a plan for its script): its cells, then. */
+ draw_cells(grid, face, s->row, s->column, reach, out, stride, width, height);
+ continue;
+ }
+ if (setting == NO_ROOM) continue;
+ /* Right to left, the spaces at its end hang past its left edge. */
+ double hang = (set.advance - set.shows) * scale, x = s->right ? limit - set.advance * scale + (set.rtl ? 0 : hang)
+ : box - (set.rtl ? hang : 0);
+ draw_set(face, &set, line, w.starts, spans, x, top, out, stride, width, height);
+ /* The cursor at the end of the row's last line: right to left, its left. */
+ if ((int)s->row == grid->cursor_row) caret = (int)lround(set.rtl ? (x > box ? x : box) : x + set.advance * scale);
+ }
+ /* The text cursor, after the typed text. */
+ if (caret >= 0) {
+ int top = grid->cursor_row * face->cell_height, bar = face->cell_height / 16 + 1;
+ int x = caret < width - bar ? caret : width - bar;
+ fill(out, stride, x, top + face->cell_height / 8, bar, face->cell_height - face->cell_height / 4, tones[VEGA_NORMAL]);
+ }
+ return true;
+}
+
void vega_pixels(const VegaGrid *grid, VegaFace *face, uint32_t *out, size_t stride) {
int width = (int)grid->width * face->cell_width, height = (int)grid->height * face->cell_height;
/* Backgrounds first: a wide glyph spans its neighbour's cell. */
@@ -78,21 +353,6 @@ void vega_pixels(const VegaGrid *grid, VegaFace *face, uint32_t *out, size_t str
for (unsigned c = 0; c < grid->width; ++c)
fill(out, stride, (int)c * face->cell_width, (int)r * face->cell_height, face->cell_width, face->cell_height,
grid->cells[(size_t)r * grid->width + c].focus ? focused : background);
- for (unsigned r = 0; r < grid->height; ++r)
- for (unsigned c = 0; c < grid->width; ++c) {
- const VegaCell *cell = &grid->cells[(size_t)r * grid->width + c];
- if (!cell->ch || cell->ch == ' ') continue;
- uint32_t color = cell->strong ? strong : tones[cell->tone < VEGA_TONES ? cell->tone : 0];
- int x = (int)c * face->cell_width, y = (int)r * face->cell_height;
- const VegaBitmap *glyph = vega_glyph(face->glyphs, cell->ch);
- unsigned cells = vega_width(cell->ch) == 2 ? 2 : 1;
- /* A wide character's glyph is centred in its two cells: a CJK font's full width is
- * less than two cells of a monospace Latin font. */
- int centred = glyph && cells == 2 && glyph->advance < 2 * face->cell_width
- ? (2 * face->cell_width - glyph->advance) / 2 : 0;
- if (!glyph) missing(out, stride, x, y, face->cell_width * (int)cells, face->cell_height, width, color);
- else draw(out, stride, width, height, glyph, x + centred, y + face->baseline, color);
- if (cell->mark && (glyph = vega_glyph(face->glyphs, cell->mark)))
- draw(out, stride, width, height, glyph, x, y + face->baseline, color);
- }
+ if (face->fonts.count && set_type(grid, face, out, stride, width, height)) return;
+ for (unsigned r = 0; r < grid->height; ++r) draw_cells(grid, face, r, 0, grid->width, out, stride, width, height);
}
diff --git a/surfaces/vega.h b/surfaces/vega.h
index 9f30ee4..ff880c5 100644
--- a/surfaces/vega.h
+++ b/surfaces/vega.h
@@ -1,5 +1,6 @@
#ifndef VEGA_H
#define VEGA_H
+#include "layout.h"
#include "module.h"
#include "symbols.h"
#include <poll.h>
@@ -108,6 +109,18 @@ typedef struct {
uint8_t tone;
bool focus, strong;
} VegaCell;
+/* What the layout wrote as text, for a surface that sets type rather than
+ * filling cells: a piece of UTF-8 given the columns [column, end) of a row,
+ * whose cells it filled up to reach. It follows the span before it when it
+ * goes on where that one's text stopped (the same line of text, in another
+ * tone perhaps): set as text, it starts where that one's ends, not at its
+ * column. right: flush with its end (a badge). */
+typedef struct {
+ unsigned row, column, end, reach;
+ uint32_t at, length; /* in the grid's text */
+ uint8_t tone;
+ bool focus, strong, follows, right;
+} VegaSpan;
typedef struct {
unsigned width, height;
VegaCell *cells, *shown;
@@ -115,6 +128,10 @@ typedef struct {
bool painted; /* shown holds what the terminal shows */
size_t first[VEGA_PARTS];
int cursor_row, cursor_column; /* where the terminal cursor goes, or -1 */
+ VegaSpan *spans;
+ size_t span_count, spans_allocated;
+ char *text;
+ size_t text_used, text_allocated;
} VegaGrid;
bool vega_grid(VegaGrid *, unsigned width, unsigned height);
void vega_grid_free(VegaGrid *);
@@ -124,14 +141,38 @@ void vega_draw(VegaGrid *, PureApp *, const VegaScreen *, const char *prompt, si
/* The part and item a line of the last drawn grid shows. */
bool vega_hit(const VegaGrid *, unsigned line, unsigned *part, size_t *item);
-/* Pixels: the grid drawn with a font's glyphs (a symbol file, pixels per em).
- * A cell is as wide as a digit and as tall as the font's line; each glyph is
- * drawn once, the first time a cell shows it. */
+/* Pixels: the grid drawn in a typeface, fonts (symbol files) in order of
+ * preference at pixels per em. A cell is as wide as the first font's digit
+ * and as tall as its line; each glyph is drawn once, the first time it
+ * shows. When the first font shapes (its file has a shaping section), the
+ * pixels set type: each span the layout wrote is set as a line of text
+ * (text/layout.h: bidi, scripts, each grapheme cluster in the first of the
+ * fonts that shape and have it), from its column or from where the text it
+ * follows stopped, and cut with an ellipsis where it would pass its end or
+ * the next span's column; a text cursor after the typed text. Otherwise
+ * each cell's character is drawn in its cell, in the first font that has
+ * it. */
+enum { VEGA_FACES = 8 };
typedef struct {
- VegaGlyphs *glyphs;
+ unsigned count;
+ const VegaSymbols *symbols[VEGA_FACES]; /* kept by the caller while the face is open */
+ VegaGlyphs *glyphs[VEGA_FACES];
+ VegaShaping shaping[VEGA_FACES]; /* of the fonts that shape */
int cell_width, cell_height, baseline;
+ float pixels;
+ /* Setting type: the fonts that shape (VegaFonts, each one of the
+ * faces'), and what setting works in. */
+ VegaFonts fonts;
+ unsigned font_face[VEGA_LAYOUT_FONTS];
+ VegaShaper *shaper;
+ VegaLayout layout;
+ void *work;
+ size_t work_size;
} VegaFace;
-bool vega_face_open(VegaFace *, const VegaSymbols *, float pixels);
+/* Open count fonts as a face (it must not move while open: it points into
+ * itself); false with why. */
+bool vega_face_open(VegaFace *, const VegaSymbols *const *symbols, unsigned count, float pixels, char *error,
+ size_t error_size);
void vega_face_close(VegaFace *);
/* The grid into out: 0xRRGGBB pixels, grid->width * cell_width across and
* grid->height * cell_height down, stride pixels apart. */
diff --git a/symbols/symbols-tool.c b/symbols/symbols-tool.c
index 5b8595f..1c0f198 100644
--- a/symbols/symbols-tool.c
+++ b/symbols/symbols-tool.c
@@ -1163,9 +1163,9 @@ static size_t decode_utf8(const char *text, uint32_t *out, size_t room) {
return n;
}
-/* Set text in lines at most width pixels wide and draw it: vega-symbols
- * paragraph OUT.vsym PIXELS WIDTH OUT.pgm TEXT... (the texts are
- * paragraphs, one after another). */
+/* Set text in lines at most width pixels wide (0: as wide as its widest
+ * line) and draw it: vega-symbols paragraph OUT.vsym PIXELS WIDTH OUT.pgm
+ * TEXT... (the texts are paragraphs, one after another). */
static int paragraph(int argc, char **argv) {
if (argc < 7) fail("usage: vega-symbols paragraph OUT.vsym PIXELS WIDTH OUT.pgm TEXT...");
char *bytes, error[128];
@@ -1187,6 +1187,9 @@ static int paragraph(int argc, char **argv) {
fail("cannot lay out");
height += (int)layouts[k].line_count * line_height + line_height / 2;
}
+ if (!width)
+ for (int k = 0; k < paragraphs; ++k)
+ if ((int)ceil(layouts[k].width * per_unit) + 1 > width) width = (int)ceil(layouts[k].width * per_unit) + 1;
int total_width = width + 2 * margin;
unsigned char *image = malloc((size_t)total_width * (size_t)height);
memset(image, 255, (size_t)total_width * (size_t)height);
diff --git a/tests/check.py b/tests/check.py
index b63a728..af8ce62 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1078,6 +1078,94 @@ def pixels(vega, tool, module, work):
return {'frames': len(frames), 'cells': checked}
+def typeset(vega, tool, module, work):
+ """The pixel surface setting type, its first font shaping: fonts in order
+ of preference (DejaVu Sans for Latin and Arabic, Noto Sans CJK KR for
+ Hangul, FreeSans for Devanagari). The frames' text is the TUI's. A label
+ in one script, alone in its list, is pixel for pixel what the font that
+ has it sets by itself (vega-symbols paragraph): each cluster went to that
+ font, shaped and placed as the layout says (ink may pass its row, as
+ marks below a line do). With several labels, one too long for its row is
+ cut within it, rows without text stay blank, and the text cursor stands
+ after the prompt."""
+ fonts = {'latin': Path('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'),
+ 'hangul': Path('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'),
+ 'deva': Path('/usr/share/fonts/truetype/freefont/FreeSans.ttf')}
+ if not all(path.exists() for path in fonts.values()):
+ return None
+ faces = {name: work / f'type-{name}.vsym' for name in fonts}
+ for name, args in (('latin', ['--codes', '0x20-0x7e,0xa0-0xff,0x600-0x6ff,0x2010-0x2027,0x2190-0x21ff',
+ '--shape', 'Latn,Arab,Zyyy']),
+ ('hangul', ['--face', 1, '--codes', '0x3131-0x318e,0xac00-0xd7a3', '--shape', 'Hang']),
+ ('deva', ['--codes', '0x900-0x97f', '--shape', 'Deva'])):
+ built = run(tool, 'build', '--font', fonts[name], *args, '--runs', 300, '-o', faces[name])
+ assert built.returncode == 0, built.stderr
+ stack = [a for face in faces.values() for a in ('--symbols', face)]
+ columns, rows, size = 48, 12, 20
+ background, focused, normal = (0x1e, 0x1e, 0x2e), (0x31, 0x32, 0x44), (0xcd, 0xd6, 0xf4)
+ metrics = {name: json.loads(run(tool, 'info', face).stdout.split(',"symbols"')[0] + '}') for name, face in faces.items()}
+ baseline = lambda name: math.ceil(metrics[name]['ascent'] * size / metrics[name]['units'])
+
+ def frame(items):
+ script = ''.join(f'text {item.encode().hex()}\n' for item in items) + 'show\n'
+ text = run(vega, 'tui', module, '--dump', f'{columns}x{rows}', stdin=script)
+ drawn = run(vega, 'pixels', module, *stack, '--size', size, '--grid', f'{columns}x{rows}', '--out', work / 'type',
+ stdin=script)
+ assert text.returncode == 0 and drawn.returncode == 0, drawn.stderr
+ lines = text.stdout.splitlines()
+ assert [line for line in drawn.stdout.splitlines() if not line.startswith('frame ')] == lines
+ (_, path, _), = [line.split() for line in drawn.stdout.splitlines() if line.startswith('frame ')]
+ header, image = Path(path).read_bytes().split(b'255\n', 1)
+ width, height = map(int, header.split()[1:3])
+ return lines, image, width, width // columns, height // rows
+
+ compared = 0
+ for label, name in {'Kerning AV Wa To': 'latin', '우유 사기': 'hangul', 'مرحبا بالعالم': 'latin',
+ 'नमस्ते दुनिया': 'deva'}.items():
+ lines, image, width, cell_width, cell_height = frame([label])
+ row = next(r for r, line in enumerate(lines[:rows]) if line[1:].split('\t')[0][2:].rstrip() == label)
+ set_alone = run(tool, 'paragraph', faces[name], size, 0, work / 'alone.pgm', label)
+ assert set_alone.returncode == 0, set_alone.stderr
+ head, gray = (work / 'alone.pgm').read_bytes().split(b'255\n', 1)
+ alone_width, alone_height = map(int, head.split()[1:3])
+ # The paragraph's pen starts at (size, size + its ascent); the label's at column 2, on the row's baseline.
+ dx, dy = 2 * cell_width - size, row * cell_height + baseline('latin') - size - baseline(name)
+ # coverage of the label's tone over the background of the row the pixel is in
+ under = lambda y: (focused if lines[y // cell_height][0] == '>' else background)[2]
+ at = lambda x, y: round((image[3 * (y * width + x) + 2] - under(y)) * 255 / (normal[2] - under(y)))
+ ink = 0
+ for y in range(alone_height):
+ for x in range(alone_width):
+ if 255 - gray[y * alone_width + x]:
+ ink += 1
+ assert abs(at(x + dx, y + dy) - (255 - gray[y * alone_width + x])) <= 4, (label, x, y)
+ for y in range(row * cell_height, (row + 1) * cell_height):
+ for x in range(2 * cell_width, width):
+ px, py = x - dx, y - dy
+ expected = 255 - gray[py * alone_width + px] if 0 <= px < alone_width and 0 <= py < alone_height else 0
+ assert abs(at(x, y) - expected) <= 4, (label, x, y, at(x, y), expected)
+ compared += 1
+ assert ink > 50, label
+ # Several: the long label cut, its ink reaching into its row's last cells.
+ long = 'Pure 런타임 ' + 'and a tail far too long to fit ' * 3
+ lines, image, width, cell_width, cell_height = frame(['우유 사기', 'Pure 런타임 문서 읽기', long])
+ pixel = lambda x, y: tuple(image[3 * (y * width + x):3 * (y * width + x) + 3])
+ row = next(r for r, line in enumerate(lines[:rows]) if line[3:].startswith('Pure 런타임 and'))
+ color = focused if lines[row][0] == '>' else background
+ inked = [x for x in range(width) if any(pixel(x, y) != color for y in range(row * cell_height, (row + 1) * cell_height))]
+ assert inked and width - 3 * cell_width <= inked[-1] < width, (inked[-1:], width)
+ # Rows without text (and none after a row of text: nothing there passes its row) are blank.
+ for row, line in enumerate(lines[:rows]):
+ if not line[1:].split('\t')[0].strip() and (not row or not lines[row - 1][1:].split('\t')[0].strip()):
+ color = focused if line[0] == '>' else background
+ assert all(pixel(x, y) == color for y in range(row * cell_height, (row + 1) * cell_height)
+ for x in range(width)), row
+ prompt = rows - 1
+ assert any(sum(pixel(x, y) == normal for y in range(prompt * cell_height, (prompt + 1) * cell_height)) >= cell_height // 2
+ for x in range(width)), 'no text cursor'
+ return {'labels': 4, 'pixels': compared}
+
+
def display(vega, module, work):
"""The same frames on a display through KMS, on a writeback connector: each
comes back from the display controller exactly as drawn, and it is the
@@ -1600,6 +1688,7 @@ def main():
if args.symbols:
report['symbols'] = symbols(args.symbols.resolve(), work)
report['pixels'] = pixels(vega, args.symbols.resolve(), modules['todo'], work)
+ report['typeset'] = typeset(vega, args.symbols.resolve(), modules['todo'], work)
report['display'] = display(vega, modules['todo'], work)
report['accessibility'] = accessibility(vega, modules['todo'], work)
report['live_display'] = live_display(vega, modules['todo'], work)