From 13240980cf051fbad140bc415e99438ecf006b9c Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 08:55:30 +0200 Subject: [PATCH 1/2] APPEND: text put together from pieces (format 0.5) APPEND $d, str appends a string - a literal or a string variable - to a string variable, as much of it as the variable's size takes (in place, without a buffer of that size). FORMAT formats one number; a number and its unit, a name and its value need several pieces - what the JavaScript compiler (dmvs_js) makes of innerText = `${speed} km/h`. Views using it are version 0.5 (todmv marks them); the operand layout has its entry (the offsets test checks it). Co-Authored-By: Claude Opus 5.5 --- docs/assembly.md | 10 ++++++++++ docs/binary-format.md | 6 +++--- include/dmview_format.h | 6 ++++-- src/exec.c | 7 +++++++ src/layout.h | 1 + src/private.h | 1 + src/validate.c | 1 + src/view.c | 24 ++++++++++++++++++++++++ tests/fixtures/append.dmvs | 27 +++++++++++++++++++++++++++ tests/libdmview_test.c | 10 ++++++++++ 10 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/append.dmvs diff --git a/docs/assembly.md b/docs/assembly.md index d3f2a89..7c1111c 100644 --- a/docs/assembly.md +++ b/docs/assembly.md @@ -489,6 +489,7 @@ variable declared with `.var`; built-in variables are read-only. | 0x48 | `CLAMP` | `$d, lo, hi` | d = min(max(d, lo), hi) | | 0x49 | `TOGGLE` | `$d` | d = (d == 0) ? 1 : 0 | | 0x4A | `FORMAT` | `$d, str, n` | Format n into string variable d: `str` with one `%d` or `%x` - optionally with a width, padded with spaces or, after a `0`, with zeros (`%02d`, `%4x`) - and `%%` for `%` | +| 0x4B | `APPEND` | `$d, str` | Append `str` (a literal or a string variable) to string variable d, as much of it as d's size takes - text put together from pieces (format 0.5) | `str` can be a string variable, so a format can be made by `FORMAT` itself - a clock from minutes: @@ -502,6 +503,15 @@ a clock from minutes: FORMAT $clock, $text, $n ; 12:05 ``` +Text from several pieces - a number and a unit, a name and a value - is +`FORMAT` or `SET` followed by `APPEND`s: + +``` + FORMAT $speed, "%d", $kmh ; "68" + APPEND $speed, " " + APPEND $speed, $unit ; "68 km/h" +``` + A variable is "changed" only when its value differs from the previous one - `SET $v, 5` on a `$v` that already holds 5 invalidates nothing. diff --git a/docs/binary-format.md b/docs/binary-format.md index 205d6d1..f0dbda6 100644 --- a/docs/binary-format.md +++ b/docs/binary-format.md @@ -1,7 +1,7 @@ # dmview Binary Format (`.dmv`) -Status: **version 0.4** - follows the [assembly draft](assembly.md). Version -0.2 added gradients, 0.3 the `OPACITY` instruction, 0.4 `ICON`. +Status: **version 0.5** - follows the [assembly draft](assembly.md). Version +0.2 added gradients, 0.3 the `OPACITY` instruction, 0.4 `ICON`, 0.5 `APPEND`. A `.dmv` file is what `libtodmv` produces from `.dmvs` assembly and what `libdmview` executes. Everything is **little-endian**; every table starts at a @@ -57,7 +57,7 @@ start of the file. An empty table has count 0. A version 0.1 header ends at 80, without the gradient tables. `libtodmv` always writes the 96-byte header, but marks a view with the oldest version -that has what it uses - 0.4 with `ICON`, 0.3 with `OPACITY`, 0.2 with gradients, else 0.1: a +that has what it uses - 0.5 with `APPEND`, 0.4 with `ICON`, 0.3 with `OPACITY`, 0.2 with gradients, else 0.1: a reader that knows only 0.1 runs a 0.1 view, it finds every table through the header and never looks at bytes 80 ... 95. diff --git a/include/dmview_format.h b/include/dmview_format.h index 10d2221..fd09951 100644 --- a/include/dmview_format.h +++ b/include/dmview_format.h @@ -25,10 +25,10 @@ /** Format version this header describes. */ #define DMV_VERSION_MAJOR 0 -#define DMV_VERSION_MINOR 4 +#define DMV_VERSION_MINOR 5 /** Header size: version 0.2 added the gradient tables at its end (0.3: - * OPACITY, 0.4: ICON - no change to the header). */ + * OPACITY, 0.4: ICON, 0.5: APPEND - no change to the header). */ #define DMV_HEADER_SIZE 96u #define DMV_HEADER_SIZE_0_1 80u @@ -106,6 +106,7 @@ typedef enum DMV_OP_CLAMP = 0x48, DMV_OP_TOGGLE = 0x49, DMV_OP_FORMAT = 0x4A, + DMV_OP_APPEND = 0x4B, /* Input */ DMV_OP_ON = 0x60, @@ -607,6 +608,7 @@ static const dmv_opcode_info_t dmv_opcode_table[DMV_OPCODE_TABLE_SIZE] = { OP(DMV_OP_CLAMP, "CLAMP", VARIA, 3, 0, F_NONE, false, VAR, V32, V32), OP(DMV_OP_TOGGLE, "TOGGLE", VARIA, 1, 0, F_NONE, false, VAR), OP(DMV_OP_FORMAT, "FORMAT", VARIA, 3, 0, F_NONE, false, VAR, STR, V32), + OP(DMV_OP_APPEND, "APPEND", VARIA, 2, 0, F_NONE, false, VAR, STR), OP(DMV_OP_ON, "ON", INPUT, 2, 0, F_NONE, false, EVENT, LABEL), diff --git a/src/exec.c b/src/exec.c index 8408e91..f158a1d 100644 --- a/src/exec.c +++ b/src/exec.c @@ -544,6 +544,13 @@ static void run(struct libdmview* v, uint32_t pc, bool box_region) view_set_string(v, rd16(insn + OPOFF(FORMAT, 0)), text); break; } + case DMV_OP_APPEND: + { + uint16_t d = rd16(insn + OPOFF(APPEND, 0)); + record(v, d / 32U, 1U << (d % 32U)); + view_append_string(v, d, vstr(v, insn, 1, OPOFF(APPEND, 1))); + break; + } /* ---- Input ---- */ case DMV_OP_ON: diff --git a/src/layout.h b/src/layout.h index f288500..eddfc33 100644 --- a/src/layout.h +++ b/src/layout.h @@ -47,6 +47,7 @@ static inline uint8_t layout_offset(uint8_t op, unsigned i) case DMV_OP_CLAMP: { static const uint8_t o[] = { 4, 8, 12 }; return o[i]; } case DMV_OP_TOGGLE: { static const uint8_t o[] = { 4 }; return o[i]; } case DMV_OP_FORMAT: { static const uint8_t o[] = { 4, 6, 8 }; return o[i]; } + case DMV_OP_APPEND: { static const uint8_t o[] = { 4, 6 }; return o[i]; } case DMV_OP_ON: { static const uint8_t o[] = { 4, 6 }; return o[i]; } case DMV_OP_REDRAW: case DMV_OP_EXEC: diff --git a/src/private.h b/src/private.h index 76b5a9f..be603aa 100644 --- a/src/private.h +++ b/src/private.h @@ -321,6 +321,7 @@ void draw_icon(const libdmview_surface_t* s, const rect_t* clip, int3 const char* view_string(const struct libdmview* v, uint32_t index); void view_set_int(struct libdmview* v, uint32_t index, int32_t value); void view_set_string(struct libdmview* v, uint32_t index, const char* value); +void view_append_string(struct libdmview* v, uint32_t index, const char* tail); void view_invalidate_deps(struct libdmview* v, uint32_t word, uint32_t bit); void view_mark_dirty(struct libdmview* v, int32_t box); diff --git a/src/validate.c b/src/validate.c index e985506..1665416 100644 --- a/src/validate.c +++ b/src/validate.c @@ -274,6 +274,7 @@ static int dest_type(uint8_t opcode) { case DMV_OP_SET: return -1; /* Either */ case DMV_OP_FORMAT: return DMV_VAR_STR; + case DMV_OP_APPEND: return DMV_VAR_STR; default: return DMV_VAR_INT; } } diff --git a/src/view.c b/src/view.c index 147c8ca..6694a6f 100644 --- a/src/view.c +++ b/src/view.c @@ -162,6 +162,30 @@ void view_set_string(struct libdmview* v, uint32_t index, const char* value) (void)Dmod_SetEnv(view_string(v, v->vars[index].env), dst, 1); } +/* The string after what the variable holds - as much of it as its size takes */ +void view_append_string(struct libdmview* v, uint32_t index, const char* tail) +{ + char* dst = v->strs[index]; + size_t n = strlen(dst), room = v->vars[index].capacity; + size_t m = strlen(tail); + if (n >= room || m == 0) + return; + if (m > room - n) + m = room - n; + if (tail >= dst && tail <= dst + room) + { + /* Appended to itself: copy it from before it grows */ + memmove(dst + n, tail, m); + } + else + memcpy(dst + n, tail, m); + dst[n + m] = '\0'; + if (!v->drawing) + view_invalidate_deps(v, index / 32U, 1U << (index % 32U)); + if ((v->vars[index].flags & DMV_VARF_ENV) != 0) + (void)Dmod_SetEnv(view_string(v, v->vars[index].env), dst, 1); +} + /* ---- Loading ---- */ static void free_view(struct libdmview* v) diff --git a/tests/fixtures/append.dmvs b/tests/fixtures/append.dmvs new file mode 100644 index 0000000..d688276 --- /dev/null +++ b/tests/fixtures/append.dmvs @@ -0,0 +1,27 @@ +; APPEND: text put together from pieces - every line is drawn above the same +; text as a literal, in the built-in font, on a 64x32 surface +.view append +.size 64, 32 +.entry draw + +.font fixed, "builtin-8" + +.var $a, str[12], "" +.var $b, str[4], "" +.var $t, str[8], "km" +.var $n, str[8], "" + +draw: + FILL #000000 + FORMAT $n, "%d", 68 + SET $a, $n + APPEND $a, " " + APPEND $a, $t + APPEND $a, "/h" + TEXT 0, 0, 64, 8, $a, fixed, #FFFFFF, LEFT ; "68 km/h" + TEXT 0, 8, 64, 8, "68 km/h", fixed, #FFFFFF, LEFT + SET $b, "ab" + APPEND $b, "cdef" + TEXT 0, 16, 64, 8, $b, fixed, #FFFFFF, LEFT ; "abcd": its size + TEXT 0, 24, 64, 8, "abcd", fixed, #FFFFFF, LEFT + RET diff --git a/tests/libdmview_test.c b/tests/libdmview_test.c index 9e0e9ba..d61c98d 100644 --- a/tests/libdmview_test.c +++ b/tests/libdmview_test.c @@ -389,6 +389,16 @@ DMOD_TEST_STEP(libdmview_formats_with_padding) DMOD_TEST_EXPECT_EQ(libdmview_get_size(NULL, &w, &h), -EINVAL); } +DMOD_TEST_STEP(libdmview_appends_strings) +{ + DMOD_TEST_EXPECT_TRUE(open_fixture(FIXTURE("append.dmv"))); + if (g_view == NULL) + return; + DMOD_TEST_EXPECT_EQ(libdmview_render(g_view, &g_s32, NULL), 1); + DMOD_TEST_EXPECT_TRUE(same_as_below(0, 64, 0)); /* "68" " " "km" "/h": "68 km/h" */ + DMOD_TEST_EXPECT_TRUE(same_as_below(0, 64, 16)); /* "ab" + "cdef" into str[4]: "abcd" */ +} + /* ---- Antialiasing ---- */ From d13d761bd79b5b2b0ca9e2c154964c038e988256 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 14:56:07 +0200 Subject: [PATCH 2/2] libdmview: a box not seen takes no contact A page's screens are boxes one on another, the hidden ones with an OPACITY of 0 (visibility: hidden) - the last one on top. The topmost box under a contact was taken even when not seen, and as it handles nothing its parents were asked: the screen hidden on top took every touch of the one shown beneath it (the car HMI's playlist and climate buttons did nothing). A box remembers its opacity times the boxes' around it as last drawn; one of 0 is passed over. Co-Authored-By: Claude Opus 5.5 --- src/exec.c | 4 ++++ src/input.c | 5 +++-- src/private.h | 1 + src/view.c | 2 ++ tests/fixtures/hidden.dmvs | 37 +++++++++++++++++++++++++++++++++++++ tests/libdmview_test.c | 31 +++++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/hidden.dmvs diff --git a/src/exec.c b/src/exec.c index f158a1d..6e3ec32 100644 --- a/src/exec.c +++ b/src/exec.c @@ -258,6 +258,8 @@ static void enter_box(struct libdmview* v, const uint8_t* insn) f->view_clip = b->clip; f->box = index; f->alpha = parent->alpha; + if (v->drawing) + b->seen = (uint8_t)((parent->alpha > 255u) ? 255u : parent->alpha); } /* ---- The interpreter ---- */ @@ -362,6 +364,8 @@ static void run(struct libdmview* v, uint32_t pc, bool box_region) int32_t a = v16(v, insn, 0, OPOFF(OPACITY, 0)); a = (a < 0) ? 0 : (a > DMV_OPACITY_MAX) ? DMV_OPACITY_MAX : a; f->alpha = (v->frames[v->depth - 1].alpha * (uint32_t)a + 127u) / 255u; + if (f->box != ROOT) + v->boxes[f->box].seen = (uint8_t)((f->alpha > 255u) ? 255u : f->alpha); } break; diff --git a/src/input.c b/src/input.c index 1124904..8188c59 100644 --- a/src/input.c +++ b/src/input.c @@ -72,14 +72,15 @@ static bool contains(const rect_t* r, int32_t x, int32_t y) /* The topmost visible box under (x, y) - boxes are drawn in code order, so * the one starting last is on top - or the nearest parent of it that takes - * the contact. */ + * the contact. A box not seen (its opacity, or one around it, 0: a screen + * hidden behind the shown one) takes none - what is seen beneath does. */ static int32_t hit_test(const struct libdmview* v, int32_t x, int32_t y) { int32_t hit = ROOT; for (uint32_t i = 0; i < v->box_count; i++) { const rbox_t* b = &v->boxes[i]; - if ((b->flags & BOXF_VISIBLE) != 0 && contains(&b->clip, x, y) && + if ((b->flags & BOXF_VISIBLE) != 0 && b->seen != 0 && contains(&b->clip, x, y) && (hit == ROOT || b->begin > v->boxes[hit].begin)) hit = (int32_t)i; } diff --git a/src/private.h b/src/private.h index be603aa..4883345 100644 --- a/src/private.h +++ b/src/private.h @@ -203,6 +203,7 @@ typedef struct int32_t sx, sy; /* Scroll offset */ int32_t cw, ch; /* Scrolled content size */ uint16_t handlers[DMV_EVENT_COUNT]; /* Label word offsets, DMV_NONE */ + uint8_t seen; /* Its opacity times the boxes' around it, as last drawn: 0 - not seen, takes no contact */ } rbox_t; /** One level of box nesting while executing. */ diff --git a/src/view.c b/src/view.c index 6694a6f..febbf13 100644 --- a/src/view.c +++ b/src/view.c @@ -319,6 +319,8 @@ static int load_tables(struct libdmview* v, const dmv_input_t* in, const uint8_t return -ENOMEM; } memset(v->boxes, 0, v->box_count * sizeof(rbox_t)); + for (uint32_t i = 0; i < v->box_count; i++) + v->boxes[i].seen = 255u; /* Seen until a draw says otherwise */ for (uint32_t i = 0; i < v->box_count; i++) { const uint8_t* p = raw + i * sizeof(dmv_box_t); diff --git a/tests/fixtures/hidden.dmvs b/tests/fixtures/hidden.dmvs new file mode 100644 index 0000000..6b88859 --- /dev/null +++ b/tests/fixtures/hidden.dmvs @@ -0,0 +1,37 @@ +; Two screens one on another, as a page's views: the one below shown, with +; a button; the one on top hidden by its opacity ($top), with a box taking +; no contact over the button - and a button of its own elsewhere. +.view hidden +.entry draw + +.var $top, int, 0 +.var $below, int, 0 +.var $above, int, 0 + +draw: + FILL #000000 + BOX @screen1, 0, 0, 64, 32 + BOX @button, 10, 10, 20, 10 ; 10..29 x 10..19 + ON CLICK, press_below + FILL #0000FF + END + END + BOX @screen2, 0, 0, 64, 32 + OPACITY $top + BOX @cover, 0, 0, 40, 32 ; over the button, no handler + FILL #FF0000 + END + BOX @other, 44, 10, 10, 10 + ON CLICK, press_above + FILL #00FF00 + END + END + RET + +press_below: + ADD $below, 1 + RET + +press_above: + ADD $above, 1 + RET diff --git a/tests/libdmview_test.c b/tests/libdmview_test.c index d61c98d..c5758e3 100644 --- a/tests/libdmview_test.c +++ b/tests/libdmview_test.c @@ -734,6 +734,37 @@ DMOD_TEST_STEP(libdmview_runs_long_press_and_timers) touch(0, 0, 0, 1700); } +DMOD_TEST_STEP(libdmview_hidden_boxes_take_no_contact) +{ + int32_t value = 0; + DMOD_TEST_EXPECT_TRUE(open_fixture(FIXTURE("hidden.dmv"))); + if (g_view == NULL) + return; + DMOD_TEST_EXPECT_EQ(libdmview_render(g_view, &g_s32, NULL), 1); + + /* The screen on top is not seen: the button beneath it is clicked */ + touch(1, 15, 15, 10); + touch(0, 0, 0, 20); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "below", &value), 0); + DMOD_TEST_EXPECT_EQ(value, 1); + touch(1, 48, 15, 30); /* Nor its own button */ + touch(0, 0, 0, 40); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "above", &value), 0); + DMOD_TEST_EXPECT_EQ(value, 0); + + /* Shown: it covers the button, its own takes the contact */ + DMOD_TEST_EXPECT_EQ(libdmview_set_int(g_view, "top", 255), 0); + DMOD_TEST_EXPECT_EQ(libdmview_render(g_view, &g_s32, NULL), 1); + touch(1, 15, 15, 50); + touch(0, 0, 0, 60); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "below", &value), 0); + DMOD_TEST_EXPECT_EQ(value, 1); + touch(1, 48, 15, 70); + touch(0, 0, 0, 80); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "above", &value), 0); + DMOD_TEST_EXPECT_EQ(value, 1); +} + DMOD_TEST_STEP(libdmview_env_variables_and_goto) { int32_t value = 0;