aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/design/001-first-component.md8
-rw-r--r--examples/today.aup39
-rw-r--r--src/draw.c12
-rw-r--r--src/main.c6
-rw-r--r--src/screen.c17
-rw-r--r--src/vega.h10
-rw-r--r--src/web.c10
-rw-r--r--tests/check.py56
8 files changed, 139 insertions, 19 deletions
diff --git a/docs/design/001-first-component.md b/docs/design/001-first-component.md
index df462e5..50a516d 100644
--- a/docs/design/001-first-component.md
+++ b/docs/design/001-first-component.md
@@ -40,6 +40,12 @@ view tasks_list_focus = at.position
| NUI | `vega say app.pmod < script` | 문장. 이벤트마다 포커스 항목을 말하고(`Item 2 of 3: danger, milk, urgent.`), 실패하면 `Could not toggle.`, `show`에서 화면 전체 |
| GUI | `vega web app.pmod [--listen 주소] [--port N]` | 웹 페이지. 제목·필드·목록(`listbox`)·표·키 목록·입력 칸. 키, 행 클릭, 휠, 입력을 받고 열린 페이지마다 변경을 알린다. 보이는 행만 계산한다 |
+목록은 걸러질 수 있다(`each t in tasks where !t.done`). 한 리스트에서 걸러진 목록 여럿이 포커스 하나를 나눠 쓴다.
+포커스 보기(`open_focus = at.position`)는 항목을 가리키므로 Vega는 Pure에게 그 항목의 행을 묻고
+(`pure_app_item_row`), 그 항목을 가진 목록에만 포커스를 보인다. 다른 목록에는 포커스가 없다(웹 JSON의 `focus`는
+null, 음성은 포커스를 가진 목록의 항목을 말한다). 행을 클릭하거나 가리키면 그 행의 항목을 가리킨다
+(`pure_app_row_item`). `examples/today.aup`가 열린 일과 끝난 일 두 목록을 이렇게 보인다.
+
포커스는 표면에서도 옮긴다. TUI에서 목록·표의 행을 클릭하면 그 행이 포커스가 되고, 휠은 포커스를 한 행씩 옮긴다
(SGR 마우스 보고). 스크립트는 `point 보기 행`(0부터)과 `click 줄 칸`(마지막으로 보인 프레임, 1부터)으로 같은 일을 한다.
앱에 코드가 필요 없다. `<표>_focus` 보기가 `at.position`처럼 인덱스 상태의 위치 그대로이면 Vega는 Pure에게 그 상태를
@@ -130,6 +136,8 @@ CLI나 문장 표면이 그대로 이어받는다. Vega에도 앱에도 저장 �
- aup와 Vega의 예제 11개를 세 표면에 무작위 입력으로 돌려 비정상 종료가 없다. sanitizer 빌드로도 같은 검사를 통과한다.
- 규약을 여덟 가지로 어긴 명세에서 `vega check`가 아홉 곳을 모두 정확한 문장으로 알리고, 예제에서는 아무것도 알리지
않는다.
+- 걸러진 두 목록(`examples/today.aup`)의 CLI 출력이 무작위 추가·이동·완료·양쪽 목록 가리키기 1,600단계에서 모델과
+ 같다(포커스는 그 항목을 가진 목록에만 있다). 웹 JSON은 다른 목록의 포커스를 null로 준다.
- 무작위 스크립트에 `point`(범위 밖 포함)와 TUI `click`을 섞어도 CLI·문장·프레임이 모델과 같다. 가상 터미널에서
마우스로 행을 클릭하면 포커스가 옮겨지고, 저장소를 쓰는 다음 실행이 그 포커스에서 시작한다.
diff --git a/examples/today.aup b/examples/today.aup
new file mode 100644
index 0000000..e5db53b
--- /dev/null
+++ b/examples/today.aup
@@ -0,0 +1,39 @@
+# Two lists over one list state: what is open and what is done. Each shows
+# the items its where keeps; the focus (at) is one item, shown in whichever
+# list holds it, and a click in either list points at that item.
+use vega
+
+record Task {
+ title: text
+ done = 0
+}
+
+state tasks: list of Task
+state at: index of tasks
+
+input add = text
+input up = key ArrowUp
+input down = key ArrowDown
+input toggle = key Space
+
+on add {
+ tasks.append(Task { title: add })
+ at = tasks.last
+}
+on up.down { at = at - 1 }
+on down.down { at = at + 1 }
+on toggle.down { at.done = !at.done }
+
+view title = "Today"
+view left = "{count(t in tasks where !t.done)} open"
+
+view open = each t in tasks where !t.done {
+ label = t.title
+}
+view open_focus = at.position
+
+view done = each t in tasks where t.done {
+ label = t.title
+ tone = vega.muted
+}
+view done_focus = at.position
diff --git a/src/draw.c b/src/draw.c
index 738c2db..31188c7 100644
--- a/src/draw.c
+++ b/src/draw.c
@@ -138,6 +138,7 @@ static void say_focus(FILE *out, PureApp *app, const VegaPart *part) {
size_t length, rows = vega_rows(app, part);
if (!rows) { fprintf(out, "%s is empty.\n", m->view_names[part->view]); return; }
size_t focus = vega_focus(app, part);
+ if (focus >= rows) { fprintf(out, "%s: none of %zu focused.\n", m->view_names[part->view], rows); return; }
fprintf(out, "Item %zu of %zu: ", focus + 1, rows);
VegaTone tone = vega_tone(app, part, focus);
if (tone >= VEGA_ACCENT) fprintf(out, "%s, ", tone_names[tone]);
@@ -164,8 +165,15 @@ void vega_say(FILE *out, PureApp *app, const VegaScreen *screen, bool all) {
char text[VEGA_TEXT];
size_t length;
if (!all) {
- for (unsigned i = 0; i < screen->count; ++i)
- if (screen->parts[i].kind != VEGA_FIELD) { say_focus(out, app, &screen->parts[i]); return; }
+ /* The focused item where a list shows it, else the first list. */
+ const VegaPart *first = NULL;
+ for (unsigned i = 0; i < screen->count; ++i) {
+ const VegaPart *part = &screen->parts[i];
+ if (part->kind == VEGA_FIELD) continue;
+ if (!first) first = part;
+ if (vega_focus(app, part) < vega_rows(app, part)) { say_focus(out, app, part); return; }
+ }
+ if (first) say_focus(out, app, first);
return;
}
if (screen->title >= 0 && vega_text(app, (uint32_t)screen->title, 0, text, &length)) {
diff --git a/src/main.c b/src/main.c
index 46b672f..b9ea91e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -95,8 +95,10 @@ static bool mouse(unsigned button, unsigned line) {
const VegaPart *part = &screen.parts[index];
if (button == 64 || button == 65) {
size_t focus = vega_focus(&app, part), rows = vega_rows(&app, part);
- if (button == 64 ? !focus : focus + 1 >= rows) return false;
- item = button == 64 ? focus - 1 : focus + 1;
+ if (focus < rows) { /* else the wheel focuses the row under it */
+ if (button == 64 ? !focus : focus + 1 >= rows) return false;
+ item = button == 64 ? focus - 1 : focus + 1;
+ }
} else if (button) return false;
return vega_point(&app, part, item);
}
diff --git a/src/screen.c b/src/screen.c
index bb50de1..26b9fb0 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -146,20 +146,23 @@ bool vega_number(PureApp *app, uint32_t view, size_t row, double *out) {
return app->module->view_table[view] ? pure_app_cell(app, view, row, out) : pure_app_view(app, view, out);
}
-size_t vega_rows(const PureApp *app, const VegaPart *part) {
+size_t vega_rows(PureApp *app, const VegaPart *part) {
return part->kind == VEGA_FIELD ? 0 : pure_app_rows(app, part->view);
}
size_t vega_focus(PureApp *app, const VegaPart *part) {
- size_t rows = vega_rows(app, part);
+ size_t rows = vega_rows(app, part), row;
double focus;
if (part->focus < 0 || !rows || !vega_number(app, (uint32_t)part->focus, 0, &focus) || !(focus >= 0)) return 0;
- return focus >= (double)(rows - 1) ? rows - 1 : (size_t)focus;
+ bool filtered = app->module->view_code[part->view].length > 0;
+ size_t item = focus < (double)SIZE_MAX ? (size_t)focus : SIZE_MAX;
+ if (pure_app_item_row(app, part->view, item, &row)) return row;
+ return filtered ? VEGA_NO_ROW : rows - 1;
}
size_t vega_scroll(size_t first, size_t focus, size_t visible, size_t rows) {
- if (focus < first) first = focus;
- if (visible && focus >= first + visible) first = focus - visible + 1;
+ if (focus < rows && focus < first) first = focus;
+ if (focus < rows && visible && focus >= first + visible) first = focus - visible + 1;
if (first + visible > rows) first = rows > visible ? rows - visible : 0;
return first;
}
@@ -184,7 +187,9 @@ bool vega_commit(PureApp *app, const VegaScreen *screen, const char *text, size_
}
bool vega_point(PureApp *app, const VegaPart *part, size_t row) {
- return part->pointer >= 0 && pure_app_point(app, (uint32_t)part->pointer, row) == PURE_OK;
+ size_t item; /* a filtered list's row shows some item of the list */
+ return part->pointer >= 0 && pure_app_row_item(app, part->view, row, &item) &&
+ pure_app_point(app, (uint32_t)part->pointer, item) == PURE_OK;
}
VegaTone vega_tone(PureApp *app, const VegaPart *part, size_t row) {
diff --git a/src/vega.h b/src/vega.h
index 98ed410..59a663e 100644
--- a/src/vega.h
+++ b/src/vega.h
@@ -45,14 +45,18 @@ unsigned vega_check(const PureModule *, void (*report)(void *, const char *name,
* shortest form. false when it has no value (for example a missing item). */
bool vega_text(PureApp *, uint32_t view, size_t row, char out[VEGA_TEXT], size_t *length);
bool vega_number(PureApp *, uint32_t view, size_t row, double *out);
-size_t vega_rows(const PureApp *, const VegaPart *);
-/* The focused row, clamped to the rows; 0 without a focus view. */
+size_t vega_rows(PureApp *, const VegaPart *);
+/* The row of the focused item: the focus view names an item (an index's
+ * position), shown on the row that holds it; VEGA_NO_ROW when a filtered
+ * list leaves that item out. Clamped to the rows without a filter; 0
+ * without a focus view. */
+#define VEGA_NO_ROW SIZE_MAX
size_t vega_focus(PureApp *, const VegaPart *);
/* Focus a row, as a user pointing at it (a click); false when the part's
* focus is not a plain index position or the row is not there. */
bool vega_point(PureApp *, const VegaPart *, size_t row);
/* The first row to show of `rows` when `visible` fit: the least scrolling
- * from first that keeps the focused row in view. */
+ * from first that keeps the focused row in view (any, without one). */
size_t vega_scroll(size_t first, size_t focus, size_t visible, size_t rows);
/* The key input answering a key name (Pure's names, as KeyboardEvent.code
* spells them), or -1; press sends its press and release. */
diff --git a/src/web.c b/src/web.c
index 78da0ab..149e888 100644
--- a/src/web.c
+++ b/src/web.c
@@ -123,8 +123,10 @@ static void screen_json(Buffer *b, PureApp *app, const VegaScreen *screen, size_
puts_(b, comma ? ",{\"part\":" : "{\"part\":");
putf(b, "%u,\"name\":", i);
json_name(b, m->view_names[part->view]);
- putf(b, ",\"kind\":\"%s\",\"rows\":%zu,\"focus\":%zu,\"first\":%zu,\"pointable\":%s,\"columns\":[",
- part->kind == VEGA_LIST ? "list" : "table", rows, rows ? focus : 0, first, part->pointer >= 0 ? "true" : "false");
+ putf(b, ",\"kind\":\"%s\",\"rows\":%zu,\"focus\":", part->kind == VEGA_LIST ? "list" : "table", rows);
+ if (focus < rows) putf(b, "%zu", focus);
+ else puts_(b, "null");
+ putf(b, ",\"first\":%zu,\"pointable\":%s,\"columns\":[", first, part->pointer >= 0 ? "true" : "false");
if (part->kind == VEGA_TABLE)
for (unsigned c = 0; c < part->column_count; ++c) {
if (c) puts_(b, ",");
@@ -224,12 +226,12 @@ static const char *const page_parts[] = {
"el('span',{className:'label',textContent:it.label?\?''}),it.detail?el('span',{className:'detail',textContent:it.detail}):null,"
"it.badge?el('span',{className:'badge',textContent:it.badge}):null);item.setAttribute('role','option');"
"item.setAttribute('aria-selected',it.row===p.focus);list.append(item)}"
-"list.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,p.focus+Math.sign(e.deltaY))))};"
+"list.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,(p.focus?\?-1)+Math.sign(e.deltaY))))};"
"return el('section',{},head,list)}"
"const table=el('table',{},el('thead',{},el('tr',{},...p.columns.map(c=>el('th',{textContent:c})))));"
"const body=el('tbody');for(const it of p.items){const tr=el('tr',{className:it.tone,onclick:()=>pick(it.row)},"
"...it.cells.map(c=>el('td',{textContent:c?\?'-'})));tr.setAttribute('aria-selected',it.row===p.focus);body.append(tr)}"
-"table.append(body);table.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,p.focus+Math.sign(e.deltaY))))};"
+"table.append(body);table.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,(p.focus?\?-1)+Math.sign(e.deltaY))))};"
"return el('section',{},head,table)}));"
"$('prompt').hidden=!s.prompt;if(s.prompt)$('entry').placeholder=s.prompt;"
"const symbols={ArrowUp:'\u2191',ArrowDown:'\u2193',ArrowLeft:'\u2190',ArrowRight:'\u2192',Backspace:'Bksp',"
diff --git a/tests/check.py b/tests/check.py
index 1e1289f..48487a3 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -431,7 +431,7 @@ def web(vega, module, work, seeds, steps=150, visible=4):
if model.at >= first + visible: first = model.at - visible + 1
if first + visible > n: first = max(n - visible, 0)
assert screen['title'] == 'To do' and screen['fields'] == [{'name': 'left', 'text': model.left()}], screen
- assert (listed['rows'], listed['focus'], listed['first']) == (n, model.at, first), (seed, listed, model.at)
+ assert (listed['rows'], listed['focus'], listed['first']) == (n, model.at if n else None, first), (seed, listed)
window = tasks[first:first + visible]
assert [(item['row'], item['label'], item['badge'], item['tone']) for item in listed['items']] == \
[(first + i, task['title'], Todo.badge(task), Todo.tone(task)) for i, task in enumerate(window)], seed
@@ -489,6 +489,57 @@ def browser(vega, module, work):
return len(outcome['steps'])
+def split(vega, module, work, seeds, steps=200):
+ """examples/today.aup: two lists filtered from one list share one focus.
+ The CLI shows each list's items in order with the focus in whichever
+ list holds it; point takes a row of either list; the web surface gives
+ the other list no focus."""
+ compared = 0
+ for seed in seeds:
+ rng, tasks, at, script, expected = random.Random(seed + 700), [], 0, [], []
+ for _ in range(steps):
+ roll = rng.random()
+ opened = [i for i, t in enumerate(tasks) if not t['done']]
+ closed = [i for i, t in enumerate(tasks) if t['done']]
+ if roll < 0.3:
+ word = rng.choice(WORDS)
+ script.append('text ' + (word.encode().hex() or '-'))
+ tasks.append({'title': word, 'done': 0})
+ at = len(tasks) - 1
+ elif roll < 0.5:
+ name, rows = rng.choice([('open', opened), ('done', closed)])
+ row = rng.randrange(len(rows) + 1)
+ script.append(f'point {name} {row}')
+ if row < len(rows): at = rows[row]
+ else:
+ key = rng.choice(['ArrowUp', 'ArrowDown', 'Space'])
+ script.append(f'key {key}')
+ if key == 'ArrowUp': at = max(at - 1, 0)
+ elif key == 'ArrowDown': at = min(at + 1, max(len(tasks) - 1, 0))
+ elif tasks: tasks[at]['done'] = int(not tasks[at]['done'])
+ if rng.random() < 0.3:
+ script.append('show')
+ opened = [i for i, t in enumerate(tasks) if not t['done']]
+ closed = [i for i, t in enumerate(tasks) if t['done']]
+ expected += ['Today', f'left: {len(opened)} open', f'open ({len(opened)})']
+ expected += [('> ' if i == at else ' ') + pictures(tasks[i]['title']) for i in opened]
+ expected += [f'done ({len(closed)})']
+ expected += [('> ' if i == at else ' ') + pictures(tasks[i]['title']) + ' (muted)' for i in closed]
+ out = run(vega, 'cli', module, stdin='\n'.join(script) + '\n')
+ assert out.returncode == 0 and out.stdout.splitlines() == expected, \
+ (seed, [(a, b) for a, b in zip(out.stdout.splitlines(), expected) if a != b][:3])
+ compared += len(expected)
+ surface = WebSurface(vega, module)
+ try:
+ for word in ('a', 'b', 'c'): surface.call('POST', '/text', word)
+ surface.call('POST', '/key', 'Space') # c is done: the focus moves to the done list
+ parts = json.loads(surface.call('GET', '/screen?rows=5')[1])['parts']
+ finally:
+ surface.stop()
+ assert [(p['name'], p['rows'], p['focus']) for p in parts] == [('open', 2, None), ('done', 1, 0)], parts
+ return compared
+
+
def scores(vega, module):
joins = ['ann', '범수', 'Zoë']
script = [f'text {name.encode().hex()}' for name in joins]
@@ -680,7 +731,7 @@ def main():
work = Path(directory)
env = dict(os.environ, AUP_PATH=str(ROOT / 'lib'))
modules = {}
- for name in ('todo', 'scores'):
+ for name in ('todo', 'scores', 'today'):
modules[name] = work / f'{name}.pmod'
built = run(aup, 'build', ROOT / 'examples' / f'{name}.aup', '-o', modules[name], env=env)
assert built.returncode == 0, built.stderr
@@ -688,6 +739,7 @@ def main():
cli_and_say(vega, modules['todo'], range(args.seeds))
report['tui_frames'] = tui(vega, modules['todo'], range(args.seeds), [(40, 12), (24, 9), (80, 30), (16, 6)])
report['table_lines'] = scores(vega, modules['scores'])
+ report['split_list_lines'] = split(vega, modules['today'], work, range(args.seeds))
report['painted_frames'], report['first_paint_bytes'], report['later_paint_bytes'] = \
painting(vega, modules['todo'], range(args.seeds), [(40, 12), (24, 9), (80, 30)])
report['web_screens'] = web(vega, modules['todo'], work, range(args.seeds))