diff --git a/docs/assembly.md b/docs/assembly.md index d3f2a89..e1be06d 100644 --- a/docs/assembly.md +++ b/docs/assembly.md @@ -118,7 +118,7 @@ the invalidated area. The view's root is always treated as opaque. - `a` can be a variable: changing it redraws the box. A translucent box and everything in it is never opaque - a redraw starts beneath it, so the blend never adds up. `OPAQUE` on such a box is ignored. -- `OPACITY 0` draws nothing; the box is still there for input. +- `OPACITY 0` draws nothing, and the box (or one inside it) takes no contact: a touch goes to what is seen beneath it, as with a page's screen hidden behind the one shown. ### Scrolling @@ -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..28b3c85 100644 --- a/src/exec.c +++ b/src/exec.c @@ -224,6 +224,11 @@ static void scroll_to(struct libdmview* v, uint16_t box, int32_t x, int32_t y) view_invalidate_deps(v, v->dep_words - 1U, DEP_SCROLL); } +void view_scroll_to(struct libdmview* v, uint16_t box, int32_t x, int32_t y) +{ + scroll_to(v, box, x, y); +} + /* ---- Boxes ---- */ static void enter_box(struct libdmview* v, const uint8_t* insn) @@ -258,6 +263,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 +369,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; @@ -544,6 +553,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/input.c b/src/input.c index 1124904..d3662eb 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; } @@ -88,6 +89,33 @@ static int32_t hit_test(const struct libdmview* v, int32_t x, int32_t y) return hit; } +/* Whether a box scrolls along an axis: SCROLL, its content larger than it, the axis allowed */ +static bool scrolls(const rbox_t* b, bool vertical) +{ + if ((b->flags & BOXF_VISIBLE) == 0 || b->seen == 0) + return false; + uint8_t axis = vertical ? DMV_SCROLL_VERTICAL : DMV_SCROLL_HORIZONTAL; + bool allowed = (b->scroll_flags & (DMV_SCROLL_VERTICAL | DMV_SCROLL_HORIZONTAL)) == 0 || (b->scroll_flags & axis) != 0; + return allowed && (vertical ? b->ch > b->bounds.y1 - b->bounds.y0 : b->cw > b->bounds.x1 - b->bounds.x0); +} + +/* The scroll box a contact at (x, y) moving along an axis drags: the topmost seen box there, or the nearest box + * around it that scrolls that way */ +static int32_t scroll_box_at(const struct libdmview* v, int32_t x, int32_t y, bool vertical) +{ + int32_t top = ROOT; + for (uint32_t i = 0; i < v->box_count; i++) + { + const rbox_t* b = &v->boxes[i]; + if ((b->flags & BOXF_VISIBLE) != 0 && b->seen != 0 && contains(&b->clip, x, y) && + (top == ROOT || b->begin > v->boxes[top].begin)) + top = (int32_t)i; + } + while (top != ROOT && !scrolls(&v->boxes[top], vertical)) + top = v->boxes[top].parent; + return top; +} + static void fire(struct libdmview* v, int32_t box, uint8_t event, int32_t x, int32_t y, int32_t dx, int32_t dy) { uint16_t label = (box != ROOT) ? v->boxes[box].handlers[event] : DMV_NONE; @@ -128,6 +156,9 @@ dmod_libdmview_api_declaration(1.0, int, _input, ( libdmview_t view, const dmdrv v->press_ms = now_ms; v->last_x = x; v->last_y = y; + v->start_x = x; + v->start_y = y; + v->scrolling = ROOT; v->captured = hit_test(v, x, y); pressed_changed(v, v->captured); fire(v, v->captured, DMV_EVENT_PRESS, x, y, 0, 0); @@ -137,12 +168,35 @@ dmod_libdmview_api_declaration(1.0, int, _input, ( libdmview_t view, const dmdrv int32_t dx = x - v->last_x, dy = y - v->last_y; v->last_x = x; v->last_y = y; - fire(v, v->captured, DMV_EVENT_DRAG, x, y, dx, dy); + int32_t mx = x - v->start_x, my = y - v->start_y; + int32_t ax = (mx < 0) ? -mx : mx, ay = (my < 0) ? -my : my; + if (v->scrolling == ROOT && (ax > (int32_t)v->scrollslop || ay > (int32_t)v->scrollslop)) + { + /* Past the threshold: a scroll box around the contact takes it over - the box pressed is released, not clicked */ + int32_t box = scroll_box_at(v, v->start_x, v->start_y, ay >= ax); + if (box == ROOT) + box = scroll_box_at(v, v->start_x, v->start_y, ay < ax); + if (box != ROOT) + { + int32_t held = v->captured; + v->scrolling = box; + v->scroll_sx = v->boxes[box].sx; + v->scroll_sy = v->boxes[box].sy; + v->captured = ROOT; + pressed_changed(v, held); + fire(v, held, DMV_EVENT_RELEASE, x, y, 0, 0); + } + } + if (v->scrolling != ROOT) + view_scroll_to(v, (uint16_t)v->scrolling, v->scroll_sx - mx, v->scroll_sy - my); + else + fire(v, v->captured, DMV_EVENT_DRAG, x, y, dx, dy); } else if (!down && v->down) { int32_t box = v->captured; v->down = false; + v->scrolling = ROOT; v->captured = ROOT; pressed_changed(v, box); if (box != ROOT) 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..8c1edf1 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. */ @@ -277,6 +278,10 @@ struct libdmview bool down; bool long_fired; int32_t last_x, last_y; + int32_t start_x, start_y; /* Where the contact went down */ + int32_t scrolling; /* The scroll box the contact drags (past .scrollslop), ROOT: none */ + int32_t scroll_sx, scroll_sy; /* Its offset when it took the contact over */ + uint16_t scrollslop; /* .scrollslop, px */ uint32_t press_ms; uint32_t buttons; bool have_now; @@ -321,6 +326,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); @@ -352,4 +358,7 @@ uint8_t font_scale_for(const char* spec); void draw_text(const libdmview_surface_t* s, const rect_t* clip, int32_t x, int32_t y, int32_t w, int32_t h, const char* text, const font_t* font, const paint_t* paint, uint8_t align); +/* exec.c: a scroll box's offset (clamped to its content) */ +void view_scroll_to(struct libdmview* v, uint16_t box, int32_t x, int32_t y); + #endif /* LIBDMVIEW_PRIVATE_H */ 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..c5e157c 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) @@ -215,6 +239,10 @@ static int load_tables(struct libdmview* v, const dmv_input_t* in, const uint8_t v->height = rd16(h + 14); v->entry = rd16(h + 18); v->longpress_ms = rd16(h + 20); + v->scrollslop = rd16(h + 22); + if (v->scrollslop == 0) + v->scrollslop = DMV_DEFAULT_SCROLLSLOP; + v->scrolling = ROOT; if ((v->code = read_block(in, code_at, v->code_size, &status)) == NULL || (v->strings = read_strings(in, strings_at, v->string_count, &status)) == NULL) @@ -295,6 +323,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/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/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/fixtures/scroll.dmvs b/tests/fixtures/scroll.dmvs new file mode 100644 index 0000000..7420ff0 --- /dev/null +++ b/tests/fixtures/scroll.dmvs @@ -0,0 +1,25 @@ +; A list in a scroll box: 20 px of it seen of 40 - red, then green - the +; red row a button. Dragging it scrolls it (past .scrollslop, 8 px), a tap +; clicks the button. +.view scroll +.entry draw + +.var $clicks, int, 0 + +draw: + FILL #000000 + BOX @list, 0, 0, 32, 20 + SCROLL 32, 40, VERTICAL + BOX @red, 0, 0, 32, 20 + ON CLICK, click + FILL #FF0000 + END + BOX @green, 0, 20, 32, 20 + FILL #00FF00 + END + END + RET + +click: + ADD $clicks, 1 + RET diff --git a/tests/libdmview_test.c b/tests/libdmview_test.c index 9e0e9ba..e458cd1 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 ---- */ @@ -724,6 +734,70 @@ 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_drags_scroll_boxes) +{ + int32_t value = 0; + DMOD_TEST_EXPECT_TRUE(open_fixture(FIXTURE("scroll.dmv"))); + if (g_view == NULL) + return; + DMOD_TEST_EXPECT_EQ(libdmview_render(g_view, &g_s32, NULL), 1); + DMOD_TEST_EXPECT_EQ(px(10, 10), 0xFFFF0000u); + + /* A tap: the button inside */ + touch(1, 10, 10, 10); + touch(0, 0, 0, 20); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "clicks", &value), 0); + DMOD_TEST_EXPECT_EQ(value, 1); + + /* Moved less than the threshold: still the button's */ + touch(1, 10, 15, 30); + touch(1, 10, 10, 40); + touch(0, 0, 0, 50); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "clicks", &value), 0); + DMOD_TEST_EXPECT_EQ(value, 2); + + /* Dragged up past it: the list scrolls (as far as it goes), the button is not clicked */ + touch(1, 10, 18, 60); + touch(1, 10, 8, 70); + touch(1, 10, 0, 80); + touch(0, 0, 0, 90); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "clicks", &value), 0); + DMOD_TEST_EXPECT_EQ(value, 2); + DMOD_TEST_EXPECT_EQ(libdmview_render(g_view, &g_s32, NULL), 1); + DMOD_TEST_EXPECT_EQ(px(10, 10), 0xFF00FF00u); +} + DMOD_TEST_STEP(libdmview_env_variables_and_goto) { int32_t value = 0;