From 23b8c97cff24583b5956fa4d6e78f3b93759a97f Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sun, 11 Oct 2026 16:02:12 +0200 Subject: [PATCH] The channel of a claim: view properties, .change handlers, EMIT (format 0.6) A claim is now also the channel between the application and its view: - properties (.prop variables) the application sets and reads by name - libdmview_prop_set_int/_set_string/_get_int/_get_string -, read from the view file when it is claimed, so a name or a type the view does not have is an error at once; names are the view's own, nothing global; - what the application sets is taken into the view on the service's thread (libdmview_update()), running the view's .change handlers; what the view sets itself is what the application reads; - events the view emits (EMIT "name", n) queue in the claim (16, the oldest dropped and counted) for libdmview_wait_event(). Claims are objects of their own now, so a thread waiting for events keeps one until it is done even when it is released. The dmview service attaches the view it shows to the display's newest claim (libdmview_attach()). Co-Authored-By: Claude Opus 5.5 --- README.md | 18 +- apps/dmview/src/main.c | 5 + docs/api-reference.md | 14 + docs/assembly.md | 51 ++++ docs/binary-format.md | 15 +- include/dmview_format.h | 12 +- include/libdmview.h | 42 +++ include/libdmview_types.h | 14 + src/claims.c | 518 ++++++++++++++++++++++++++++++++++-- src/exec.c | 4 + src/input.c | 3 + src/layout.h | 1 + src/private.h | 6 + src/validate.c | 8 +- src/view.c | 6 + tests/fixtures/channel.dmvs | 34 +++ tests/libdmview_test.c | 92 +++++++ 17 files changed, 802 insertions(+), 41 deletions(-) create mode 100644 tests/fixtures/channel.dmvs diff --git a/README.md b/README.md index 266aa5f..6383759 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,20 @@ libdmview_release(claim); ``` The service draws the view and handles its input; the application talks to -it through the view's `SIGNAL` (a dmhaman handler it registered) and `env:` -variables. Claims stack: the newest one is shown, a release - or the -application ending without one, noticed through its pid - returns the -display to the previous claim and finally to the service's own view. +it through the claim: the view's properties (`.prop`), which the application +sets and reads by name - a change runs the view's `.change` handlers - and +the events the view emits (`EMIT`), which it takes in order: + +```c +libdmview_prop_set_string(claim, "title", "Midnight Drive"); +libdmview_event_t ev; +libdmview_wait_event(claim, &ev, 1000); /* { "select", 3 } */ +``` + +See [docs/assembly.md](docs/assembly.md#the-channel-of-a-claim). Claims +stack: the newest one is shown, a release - or the application ending +without one, noticed through its pid - returns the display to the previous +claim and finally to the service's own view. ## libdmview diff --git a/apps/dmview/src/main.c b/apps/dmview/src/main.c index 5ec7a85..dc84904 100644 --- a/apps/dmview/src/main.c +++ b/apps/dmview/src/main.c @@ -377,6 +377,7 @@ static void show_view(service_t* s, char* path) uint16_t w = 0, h = 0; (void)libdmview_get_size(s->view, &w, &h); set_rotation(s, rotation_for(s, w, h)); + (void)libdmview_attach(s->view, s->display); /* The channel of the claim it is shown for */ DMOD_LOG_INFO("dmview: showing %s on %s\n", path, s->name); } } @@ -417,7 +418,11 @@ static void run(service_t* s) if (next != NULL) show_view(s, Dmod_StrDup(next)); if (libdmview_display_generation(s->display) != s->generation) + { show_view(s, choose_view(s)); + if (s->view != NULL) + (void)libdmview_attach(s->view, s->display); /* (the same view, another claim) */ + } libdmview_rect_t changed; if (s->view != NULL && libdmview_render(s->view, &s->surface, &changed) > 0) diff --git a/docs/api-reference.md b/docs/api-reference.md index 5985f8d..36cd2d7 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -47,3 +47,17 @@ view sees is its time 0 (`$time`, `.timer`). A claim remembers the pid of the calling process - never a callback or a pointer into it - so a process that ends without releasing leaves nothing behind. + +## The channel of a claim + +The view's properties (`.prop`) and events (`EMIT`) - see +[assembly.md](assembly.md#the-channel-of-a-claim). + +| Function | Description | +|----------|-------------| +| `int libdmview_prop_set_int(uint32_t claim, const char* name, int32_t value)` | Set an integer property; the view takes it in (and runs its `.change` handlers) on the service's thread. `-ENOENT`: no such claim or property, `-EINVAL`: not an integer | +| `int libdmview_prop_set_string(claim, name, const char* value)` | Set a string property, cut to its size | +| `int libdmview_prop_get_int(claim, name, int32_t* value)` | As the view or the application last set it | +| `int libdmview_prop_get_string(claim, name, char* value, size_t size)` | `-ERANGE`: cut to `size` | +| `int libdmview_wait_event(claim, libdmview_event_t* event, int32_t timeout_ms)` | The next event (`name`, `value`, `dropped` before it); 0: no wait, -1: no limit. `-ETIMEDOUT`, `-ENOENT` (released meanwhile) | +| `int libdmview_attach(libdmview_t view, libdmview_display_t display)` | For the service: connect the view it shows to the display's newest claim; `-ENOENT` without a claim | diff --git a/docs/assembly.md b/docs/assembly.md index e1be06d..dd96891 100644 --- a/docs/assembly.md +++ b/docs/assembly.md @@ -293,6 +293,9 @@ variable - `RECT 0, 0, $width, 8, #3D85F5` takes its width from `$width`. | `.entry label` | Start of the draw pass (required, once) | | `.var $name, int, init [, env:NAME]` | 32-bit signed integer variable | | `.var $name, str[N], "init" [, env:NAME]` | String variable holding up to N bytes | +| `.prop $name, int, init, "name"` | Integer **property**: a variable the application that claimed the display reads and sets by `name` - see [The channel of a claim](#the-channel-of-a-claim) (version 0.6) | +| `.prop $name, str[N], "init", "name"` | String property | +| `.change $var, label` | Handler run when the application changed the property `$var` (version 0.6) | | `.font name, "spec"` | Font used by `TEXT`; `spec` is resolved by `libdmview` (e.g. `"sans-16"`) - see [Fonts](#fonts) | | `.gradient name, LINEAR [, angle], stop, stop [, ...]` | Linear gradient - see [Gradients](#gradients) | | `.gradient name, RADIAL [, cx, cy, rx, ry], stop, stop [, ...]` | Radial gradient - see [Gradients](#gradients) | @@ -535,6 +538,7 @@ each other; one box has at most one handler per event. | 0x84 | `SCROLLTO` | `@id, x, y` | Set the scroll offset of a scroll box (clamped to its content) | | 0x85 | `RELOAD` | `str` | Load the image file `str` again (it changed on disk) and redraw where it is shown | | 0x86 | `SETFOCUS` | `@id` | Move the focus to a focusable box | +| 0x87 | `EMIT` | `str [, n]` | Send the event `str` with the value `n` (default 0) to the application that claimed the display - see [The channel of a claim](#the-channel-of-a-claim) (version 0.6) | Opcodes 0xF0-0xFF are reserved for extensions (e.g. `dmjs` script calls). @@ -614,6 +618,53 @@ One mask then fits every theme and every state; an A4 mask takes half a byte per pixel. A gradient is placed on the rectangle x/y/w/h. `IMAGE` draws only images with colors - a mask shows nothing there. +## The channel of a claim + +An application shows its view on a display by claiming it +(`libdmview_claim()`); the claim is also the channel between the two: + +- **Properties** - `.prop` variables - are the view's side of a contract: + the application sets and reads them by their names + (`libdmview_prop_set_int()`, `_set_string()`, `_get_int()`, + `_get_string()`), and the view uses them like any variable. The names are + the view's own: two views with a property `"title"` never meet. They are + read from the view file when it is claimed, so a name or a type the view + does not have is an error at once (`-ENOENT`, `-EINVAL`). +- A change the application makes runs the view's **`.change`** handlers of + that property (after the variable has the new value) and redraws what + reads it. A change the view makes itself (`SET`, ...) is what the + application reads next, without a handler. +- **Events** - `EMIT "name", n` - go into the claim's queue in order; the + application takes them with `libdmview_wait_event()`. 16 are kept; when + the application is slower, the oldest go and the next event it takes + tells how many (`dropped`). + +The service applies what the application set on its own thread, the only one +that touches the view - when it updates the view, within 100 ms. Properties +set before the view is shown are taken in when it is. + +``` +.prop $title, str[48], "-", "title" ; the application sets it +.prop $playing, int, 0, "playing" +.change $playing, show_state + +play_clicked: + EMIT "toggle" + RET +row_clicked: + EMIT "select", $row + RET +``` + +```c +uint32_t claim; +libdmview_claim(NULL, "/app/player.dmv", &claim); +libdmview_prop_set_string(claim, "title", "Midnight Drive"); +libdmview_event_t ev; +if (libdmview_wait_event(claim, &ev, 1000) == 0 && strcmp(ev.name, "select") == 0) + play(ev.value); +``` + ## dmenv change listener dmenv itself needs an addition for `env:` variables (draft, to be added to diff --git a/docs/binary-format.md b/docs/binary-format.md index f0dbda6..1c90870 100644 --- a/docs/binary-format.md +++ b/docs/binary-format.md @@ -1,7 +1,8 @@ # dmview Binary Format (`.dmv`) -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`. +Status: **version 0.6** - follows the [assembly draft](assembly.md). Version +0.2 added gradients, 0.3 the `OPACITY` instruction, 0.4 `ICON`, 0.5 `APPEND`, 0.6 +the channel of a claim (`.prop`, `.change`, `EMIT`). A `.dmv` file is what `libtodmv` produces from `.dmvs` assembly and what `libdmview` executes. Everything is **little-endian**; every table starts at a @@ -34,7 +35,7 @@ reader finds them only through the header, never by position. |--------|------|-------|---------| | 0 | 4 | magic | `'D' 'M' 'V' 0` | | 4 | 2 | version_major | 0 - a reader rejects any other | -| 6 | 2 | version_minor | 4 - a reader accepts this or lower | +| 6 | 2 | version_minor | 6 - a reader accepts this or lower (a writer puts the oldest version its view needs) | | 8 | 4 | file_size | Size of the whole file | | 12 | 2 | width | `.size` width, 0 = not given | | 14 | 2 | height | `.size` height, 0 = not given | @@ -100,10 +101,10 @@ variables, fonts, boxes, labels and dmenv bindings. | Offset | Size | Field | Meaning | |--------|------|-------|---------| | 0 | 1 | type | 0 = integer, 1 = string | -| 1 | 1 | flags | bit 0: bound to dmenv (`env:`) | +| 1 | 1 | flags | bit 0: bound to dmenv (`env:`); bit 1: a property (`.prop`, version 0.6) - not both | | 2 | 2 | capacity | Bytes of a string variable, 0 for an integer | | 4 | 2 | name | String index of the name, without `$` | -| 6 | 2 | env | String index of the dmenv name, 0xFFFF without binding | +| 6 | 2 | env | String index of the dmenv name (bit 0) or of the property's name (bit 1), 0xFFFF without either | | 8 | 4 | init | Initial integer, or string index of the initial string | **Font** (4 bytes): name (string index), spec (string index, e.g. `sans-16`). @@ -117,10 +118,10 @@ variables, fonts, boxes, labels and dmenv bindings. | Offset | Size | Field | Meaning | |--------|------|-------|---------| -| 0 | 1 | kind | 0 `.init`, 1 `.timer`, 2 `.key`, 3 `.navkeys` | +| 0 | 1 | kind | 0 `.init`, 1 `.timer`, 2 `.key`, 3 `.navkeys`, 4 `.change` (version 0.6) | | 1 | 1 | arg | `.navkeys`: role (`NEXT` = 0 ... `OK` = 6) | | 2 | 2 | label | Handler code word offset, 0xFFFF for `.navkeys` | -| 4 | 4 | value | `.timer`: milliseconds, `.key` / `.navkeys`: button index | +| 4 | 4 | value | `.timer`: milliseconds, `.key` / `.navkeys`: button index, `.change`: variable index | **Gradient** (16 bytes) diff --git a/include/dmview_format.h b/include/dmview_format.h index fd09951..2f1ec2e 100644 --- a/include/dmview_format.h +++ b/include/dmview_format.h @@ -25,7 +25,7 @@ /** Format version this header describes. */ #define DMV_VERSION_MAJOR 0 -#define DMV_VERSION_MINOR 5 +#define DMV_VERSION_MINOR 6 /** Header size: version 0.2 added the gradient tables at its end (0.3: * OPACITY, 0.4: ICON, 0.5: APPEND - no change to the header). */ @@ -119,6 +119,7 @@ typedef enum DMV_OP_SCROLLTO = 0x84, DMV_OP_RELOAD = 0x85, DMV_OP_SETFOCUS = 0x86, + DMV_OP_EMIT = 0x87, /**< Version 0.6 */ } dmv_opcode_t; /** Instruction categories. */ @@ -279,6 +280,7 @@ typedef enum /** Variable flags. */ #define DMV_VARF_ENV 0x01u /**< Bound to the dmenv variable `env` */ +#define DMV_VARF_PROP 0x02u /**< A property of the view (.prop) - `env` is its name - version 0.6 */ /** View-level items (directives with a handler). */ typedef enum @@ -287,6 +289,7 @@ typedef enum DMV_ITEM_TIMER, /**< .timer value=ms, label */ DMV_ITEM_KEY, /**< .key value=button, label */ DMV_ITEM_NAVKEY, /**< .navkeys arg=role, value=button, no label */ + DMV_ITEM_CHANGE, /**< .change value=variable index, label - version 0.6 */ DMV_ITEM_COUNT } dmv_item_kind_t; @@ -345,7 +348,7 @@ typedef struct uint8_t flags; /**< DMV_VARF_* */ uint16_t capacity; /**< Bytes of a string variable, 0 for an integer */ uint16_t name; /**< String index of the name (without '$') */ - uint16_t env; /**< String index of the dmenv name, or DMV_NONE */ + uint16_t env; /**< String index of the dmenv name (DMV_VARF_ENV) or of the property name (DMV_VARF_PROP), or DMV_NONE */ int32_t init; /**< Initial integer, or string index of the initial string */ } dmv_var_t; @@ -396,7 +399,7 @@ typedef struct uint8_t kind; /**< dmv_item_kind_t */ uint8_t arg; /**< DMV_ITEM_NAVKEY: dmv_nav_role_t */ uint16_t label; /**< Handler code word offset, or DMV_NONE */ - uint32_t value; /**< TIMER: ms, KEY / NAVKEY: button index */ + uint32_t value; /**< TIMER: ms, KEY / NAVKEY: button index, CHANGE: variable index */ } dmv_item_t; /** Symbol record (labels, for disassembly and debugging). */ @@ -566,7 +569,7 @@ typedef struct #define F_SCR DMV_FLAGS_SCROLL #define F_ALIGN DMV_FLAGS_ALIGN -#define DMV_OPCODE_TABLE_SIZE (DMV_OP_SETFOCUS + 1) +#define DMV_OPCODE_TABLE_SIZE (DMV_OP_EMIT + 1) static const dmv_opcode_info_t dmv_opcode_table[DMV_OPCODE_TABLE_SIZE] = { OP(DMV_OP_NOP, "NOP", FLOW, 0, 0, F_NONE, false, 0), @@ -619,6 +622,7 @@ static const dmv_opcode_info_t dmv_opcode_table[DMV_OPCODE_TABLE_SIZE] = { OP(DMV_OP_SCROLLTO, "SCROLLTO", ACTION, 3, 0, F_NONE, false, BOX, V16, V16), OP(DMV_OP_RELOAD, "RELOAD", ACTION, 1, 0, F_NONE, false, STR), OP(DMV_OP_SETFOCUS, "SETFOCUS", ACTION, 1, 0, F_NONE, false, BOX), + OP(DMV_OP_EMIT, "EMIT", ACTION, 2, 1, F_NONE, false, STR, V32), }; #undef V16 diff --git a/include/libdmview.h b/include/libdmview.h index e7f5fd1..9a02c73 100644 --- a/include/libdmview.h +++ b/include/libdmview.h @@ -141,4 +141,46 @@ dmod_libdmview_api(1.0, int, _claim, ( const char* display, const char* view_pat /** @brief Drop a claim. -ENOENT when there is none with this id. */ dmod_libdmview_api(1.0, int, _release, ( uint32_t claim )); +/* ---- The channel of a claim ---- + * + * A claim is also a channel between the application and its view: the + * view's properties (`.prop $var, type, init, "name"`), which both may set + * and read - a change made by the application runs the view's `.change` + * handlers -, and the events the view emits (`EMIT "name", value`), which + * the application takes in order. Names are the view's own: two + * applications with a property "title" do not meet. + * + * The properties are read from the view file when it is claimed, so a + * name or a type the view does not have is an error at once. What the + * application sets reaches the view on the service's thread within + * 100 ms (the service's poll) - also before the view is shown. + */ + +/** @brief Set an integer property of the claim's view. -ENOENT (no claim, no such property), -EINVAL (not an integer). */ +dmod_libdmview_api(1.0, int, _prop_set_int, ( uint32_t claim, const char* name, int32_t value )); + +/** @brief Set a string property (cut to its size). -ENOENT, -EINVAL (not a string). */ +dmod_libdmview_api(1.0, int, _prop_set_string, ( uint32_t claim, const char* name, const char* value )); + +/** @brief An integer property - as the view or the application last set it. -ENOENT, -EINVAL. */ +dmod_libdmview_api(1.0, int, _prop_get_int, ( uint32_t claim, const char* name, int32_t* value )); + +/** @brief A string property. -ENOENT, -EINVAL, -ERANGE (@p value too small: cut). */ +dmod_libdmview_api(1.0, int, _prop_get_string, ( uint32_t claim, const char* name, char* value, size_t size )); + +/** + * @brief Take the next event the claim's view emitted, waiting for one at + * most @p timeout_ms (0: does not wait, -1: no limit). + * + * @return 0, -ETIMEDOUT, -ENOENT (no such claim, or it was released while waiting) + */ +dmod_libdmview_api(1.0, int, _wait_event, ( uint32_t claim, libdmview_event_t* event, int32_t timeout_ms )); + +/** + * @brief Connect a view to the display's newest claim (called by the + * service after it showed the view the claim is for): its properties + * and events go through the claim from now on. No claim: none. + */ +dmod_libdmview_api(1.0, int, _attach, ( libdmview_t view, libdmview_display_t display )); + #endif /* LIBDMVIEW_H */ diff --git a/include/libdmview_types.h b/include/libdmview_types.h index a1f33f0..ce081e6 100644 --- a/include/libdmview_types.h +++ b/include/libdmview_types.h @@ -31,6 +31,20 @@ typedef struct uint16_t h; } libdmview_rect_t; +/** Longest name of an event (EMIT) with its terminating zero. */ +#define LIBDMVIEW_EVENT_NAME_MAX 32u + +/** Events of a claim kept until the application takes them (the oldest are dropped). */ +#define LIBDMVIEW_EVENT_QUEUE 16u + +/** An event the view of a claim emitted (EMIT name, value). */ +typedef struct +{ + char name[LIBDMVIEW_EVENT_NAME_MAX]; + int32_t value; + uint32_t dropped; /**< Events dropped before this one - the application was too slow */ +} libdmview_event_t; + /** libdmview_update() result when nothing is pending. */ #define LIBDMVIEW_NO_DEADLINE 0xFFFFFFFFu diff --git a/src/claims.c b/src/claims.c index 8b7a17f..0ce2015 100644 --- a/src/claims.c +++ b/src/claims.c @@ -11,15 +11,44 @@ * or a pointer into it: when the process is gone, its claim is dropped the * next time the display is asked (libdmview_display_view()), and the display * goes back to the previous claim or the service's own view. + * + * A claim is also the channel between the application and its view: the + * view's properties - read from the view file when it is claimed - and the + * events it emits. The application writes properties into the claim; the + * service's thread takes them into the view attached to the claim + * (claims_apply(), from libdmview_update()), which is the only thread that + * touches the view. Everything is found by the claim's id under g_lock - + * nobody keeps a pointer to a claim but a thread waiting for its events, + * which holds it (`waiters`) until it is done. */ #define DISPLAY_MAGIC 0x44535059u /* 'DSPY' */ typedef struct { - uint32_t id; - dmosi_process_id_t pid; - char* path; + char* name; + uint8_t type; /* dmv_var_type_t */ + uint16_t capacity; /* Strings */ + int32_t ival; + char* sval; /* capacity + 1 bytes */ + bool pending; /* Set by the application, not in the view yet */ +} prop_t; + +typedef struct claim +{ + uint32_t id; + dmosi_process_id_t pid; + char* path; + prop_t* props; + uint32_t prop_count; + bool pending; /* A property is */ + + libdmview_event_t events[LIBDMVIEW_EVENT_QUEUE]; + uint32_t first, count; /* Queue */ + uint32_t dropped; /* Since the last event taken */ + dmosi_semaphore_t ready; /* One post per queued event */ + uint32_t waiters; /* Threads in libdmview_wait_event() */ + bool released; /* Out of its display, freed by the last waiter */ } claim_t; struct libdmview_display @@ -28,7 +57,7 @@ struct libdmview_display struct libdmview_display* next; char* name; uint32_t generation; - claim_t* claims; /* Oldest first */ + claim_t** claims; /* Oldest first */ uint32_t count; uint32_t capacity; }; @@ -63,14 +92,188 @@ static dmosi_process_id_t current_pid(void) return (process != NULL) ? dmosi_process_get_id(process) : 0; } +/* ---- Claims ---- */ + +static void claim_free(claim_t* c) +{ + for (uint32_t i = 0; i < c->prop_count; i++) + { + Dmod_Free(c->props[i].name); + if (c->props[i].sval != NULL) + Dmod_Free(c->props[i].sval); + } + if (c->props != NULL) + Dmod_Free(c->props); + if (c->ready != NULL) + dmosi_semaphore_destroy(c->ready); + if (c->path != NULL) + Dmod_Free(c->path); + Dmod_Free(c); +} + +/* Out of its display; freed now, or by the last thread waiting for its events. Under g_lock */ +static void claim_release(claim_t* c) +{ + if (c->waiters > 0) + { + c->released = true; + (void)dmosi_semaphore_post(c->ready, 1); /* (room: one more than the queue) */ + } + else + claim_free(c); +} + static void drop_claim(struct libdmview_display* d, uint32_t i) { - Dmod_Free(d->claims[i].path); - memmove(&d->claims[i], &d->claims[i + 1], (d->count - i - 1U) * sizeof(claim_t)); + claim_t* c = d->claims[i]; + memmove(&d->claims[i], &d->claims[i + 1], (d->count - i - 1U) * sizeof(claim_t*)); d->count--; d->generation++; + claim_release(c); +} + +/* The claim with this id; under g_lock */ +static claim_t* find_claim(uint32_t id) +{ + for (struct libdmview_display* d = g_displays; d != NULL; d = d->next) + { + for (uint32_t i = 0; i < d->count; i++) + { + if (d->claims[i]->id == id) + return d->claims[i]; + } + } + return NULL; +} + +static prop_t* find_prop(claim_t* c, const char* name) +{ + for (uint32_t i = 0; c != NULL && name != NULL && i < c->prop_count; i++) + { + if (strcmp(c->props[i].name, name) == 0) + return &c->props[i]; + } + return NULL; +} + +/* ---- The properties of a view file ---- */ + +static int file_read(void* ctx, uint32_t offset, void* buffer, size_t size) +{ + if (Dmod_FileSeek(ctx, (Dmod_FileOffset_t)offset, DMOD_SEEK_SET) != 0) + return -EIO; + return (Dmod_FileRead(buffer, 1, size, ctx) == size) ? 0 : -EIO; +} + +static bool read_at(void* f, uint32_t offset, void* buffer, size_t size) +{ + return file_read(f, offset, buffer, size) == 0; +} + +/* String `index` of the table at `table` (a valid view: it is terminated); allocated */ +static char* read_string(void* f, uint32_t table, uint32_t index) +{ + uint8_t w[4]; + if (!read_at(f, table + index * 4U, w, sizeof(w))) + return NULL; + uint32_t at = table + rd32(w); + size_t len = 0, size = 32; + char* s = Dmod_Malloc(size); + while (s != NULL) + { + if (len + 1U >= size) + { + char* bigger = Dmod_Realloc(s, size * 2U); + if (bigger == NULL) + break; + s = bigger; + size *= 2U; + } + if (!read_at(f, at + (uint32_t)len, s + len, 1)) + break; + if (s[len] == '\0') + return s; + len++; + } + if (s != NULL) + Dmod_Free(s); + return NULL; +} + +/* The `.prop` variables of the view at `path` - none when it cannot be read or is not valid */ +static void load_props(claim_t* c, const char* path) +{ + size_t size = 0; + void* f = Dmod_FileOpen(path, "rb"); + if (f == NULL || !Dmod_FileSizeToSizeT(Dmod_FileSize(f), &size) || size > 0xFFFFFFFFu) + { + if (f != NULL) + Dmod_FileClose(f); + return; + } + dmv_input_t in = { file_read, f, (uint32_t)size }; + uint8_t h[DMV_HEADER_SIZE_0_1]; + if (dmv_validate(&in, NULL) != DMV_VALID || !read_at(f, 0, h, sizeof(h)) || rd16(h + 6) < 6) + { + Dmod_FileClose(f); + return; + } + uint32_t strings = rd32(h + 32), vars = rd32(h + 40), var_count = rd32(h + 44); + for (uint32_t pass = 0; pass < 2; pass++) + { + /* Counted first, then read */ + uint32_t n = 0; + for (uint32_t i = 0; i < var_count; i++) + { + uint8_t p[sizeof(dmv_var_t)]; + if (!read_at(f, vars + i * (uint32_t)sizeof(dmv_var_t), p, sizeof(p)) || (p[1] & DMV_VARF_PROP) == 0) + continue; + if (pass == 1) + { + prop_t* prop = &c->props[n]; + prop->type = p[0]; + prop->capacity = rd16(p + 2); + prop->name = read_string(f, strings, rd16(p + 6)); + if (prop->type == DMV_VAR_STR) + { + char* init = read_string(f, strings, rd32(p + 8)); + prop->sval = Dmod_Malloc((size_t)prop->capacity + 1U); + if (prop->sval != NULL) + { + strncpy(prop->sval, (init != NULL) ? init : "", prop->capacity); + prop->sval[prop->capacity] = '\0'; + } + if (init != NULL) + Dmod_Free(init); + } + else + prop->ival = (int32_t)rd32(p + 8); + if (prop->name == NULL || (prop->type == DMV_VAR_STR && prop->sval == NULL)) + { + if (prop->name != NULL) + Dmod_Free(prop->name); + if (prop->sval != NULL) + Dmod_Free(prop->sval); + memset(prop, 0, sizeof(*prop)); + continue; + } + } + n++; + } + if (pass == 0) + { + if (n == 0 || (c->props = Dmod_Malloc(n * sizeof(prop_t))) == NULL) + break; + memset(c->props, 0, n * sizeof(prop_t)); + } + else + c->prop_count = n; + } + Dmod_FileClose(f); } +/* ---- Displays ---- */ + dmod_libdmview_api_declaration(1.0, libdmview_display_t, _display_register, ( const char* name )) { if (name == NULL || g_lock == NULL) @@ -120,10 +323,10 @@ dmod_libdmview_api_declaration(1.0, void, _display_unregister, ( libdmview_displ break; } } - dmosi_mutex_unlock(g_lock); - while (display->count > 0) drop_claim(display, display->count - 1U); + dmosi_mutex_unlock(g_lock); + if (display->claims != NULL) Dmod_Free(display->claims); Dmod_Free(display->name); @@ -136,25 +339,30 @@ dmod_libdmview_api_declaration(1.0, uint32_t, _display_generation, ( libdmview_d return is_display(display) ? display->generation : 0; } +/* Claims of processes that are gone; under g_lock */ +static void drop_dead(struct libdmview_display* d) +{ + for (uint32_t i = d->count; i > 0; i--) + { + if (dmosi_process_find_by_id(d->claims[i - 1U]->pid) == NULL) + drop_claim(d, i - 1U); + } +} + dmod_libdmview_api_declaration(1.0, int, _display_view, ( libdmview_display_t display, uint32_t* generation, char* path, size_t size, size_t* length )) { if (!is_display(display)) return -EINVAL; dmosi_mutex_lock(g_lock); - /* Claims of processes that are gone */ - for (uint32_t i = display->count; i > 0; i--) - { - if (dmosi_process_find_by_id(display->claims[i - 1U].pid) == NULL) - drop_claim(display, i - 1U); - } + drop_dead(display); if (generation != NULL) *generation = display->generation; int ret = -ENOENT; if (display->count > 0) { - const char* top = display->claims[display->count - 1U].path; + const char* top = display->claims[display->count - 1U]->path; size_t len = strlen(top); if (length != NULL) *length = len; @@ -175,10 +383,19 @@ dmod_libdmview_api_declaration(1.0, int, _claim, ( const char* display, const ch { if (view_path == NULL || claim == NULL || g_lock == NULL) return -EINVAL; - char* copy = Dmod_StrDup(view_path); - if (copy == NULL) + claim_t* c = Dmod_Malloc(sizeof(*c)); + if (c == NULL) + return -ENOMEM; + memset(c, 0, sizeof(*c)); + c->path = Dmod_StrDup(view_path); + c->ready = dmosi_semaphore_create(0, LIBDMVIEW_EVENT_QUEUE + 1U); + c->pid = current_pid(); + if (c->path == NULL || c->ready == NULL) + { + claim_free(c); return -ENOMEM; - dmosi_process_id_t pid = current_pid(); + } + load_props(c, view_path); /* (outside the lock: it reads the file) */ dmosi_mutex_lock(g_lock); /* NULL: the first registered display - the last one in the list */ @@ -194,7 +411,7 @@ dmod_libdmview_api_declaration(1.0, int, _claim, ( const char* display, const ch if (ret == 0 && d->count == d->capacity) { uint32_t capacity = d->capacity ? d->capacity * 2U : 4U; - claim_t* p = Dmod_Realloc(d->claims, capacity * sizeof(claim_t)); + claim_t** p = Dmod_Realloc(d->claims, capacity * sizeof(claim_t*)); if (p == NULL) ret = -ENOMEM; else @@ -205,16 +422,14 @@ dmod_libdmview_api_declaration(1.0, int, _claim, ( const char* display, const ch } if (ret == 0) { - claim_t* c = &d->claims[d->count++]; c->id = g_next_claim++; - c->pid = pid; - c->path = copy; + d->claims[d->count++] = c; d->generation++; *claim = c->id; } dmosi_mutex_unlock(g_lock); if (ret != 0) - Dmod_Free(copy); + claim_free(c); return ret; } @@ -228,7 +443,7 @@ dmod_libdmview_api_declaration(1.0, int, _release, ( uint32_t claim )) { for (uint32_t i = 0; i < d->count; i++) { - if (d->claims[i].id == claim) + if (d->claims[i]->id == claim) { drop_claim(d, i); ret = 0; @@ -239,3 +454,258 @@ dmod_libdmview_api_declaration(1.0, int, _release, ( uint32_t claim )) dmosi_mutex_unlock(g_lock); return ret; } + +/* ---- The channel: the application's side ---- */ + +static int prop_set(uint32_t claim, const char* name, uint8_t type, int32_t ival, const char* sval) +{ + if (name == NULL || (type == DMV_VAR_STR && sval == NULL) || g_lock == NULL) + return -EINVAL; + dmosi_mutex_lock(g_lock); + claim_t* c = find_claim(claim); + prop_t* p = find_prop(c, name); + int ret = (p == NULL) ? -ENOENT : (p->type != type) ? -EINVAL : 0; + if (ret == 0) + { + if (type == DMV_VAR_STR) + { + strncpy(p->sval, sval, p->capacity); + p->sval[p->capacity] = '\0'; + } + else + p->ival = ival; + p->pending = true; + c->pending = true; + } + dmosi_mutex_unlock(g_lock); + return ret; +} + +dmod_libdmview_api_declaration(1.0, int, _prop_set_int, ( uint32_t claim, const char* name, int32_t value )) +{ + return prop_set(claim, name, DMV_VAR_INT, value, NULL); +} + +dmod_libdmview_api_declaration(1.0, int, _prop_set_string, ( uint32_t claim, const char* name, const char* value )) +{ + return prop_set(claim, name, DMV_VAR_STR, 0, value); +} + +dmod_libdmview_api_declaration(1.0, int, _prop_get_int, ( uint32_t claim, const char* name, int32_t* value )) +{ + if (value == NULL || g_lock == NULL) + return -EINVAL; + dmosi_mutex_lock(g_lock); + prop_t* p = find_prop(find_claim(claim), name); + int ret = (p == NULL) ? -ENOENT : (p->type != DMV_VAR_INT) ? -EINVAL : 0; + if (ret == 0) + *value = p->ival; + dmosi_mutex_unlock(g_lock); + return ret; +} + +dmod_libdmview_api_declaration(1.0, int, _prop_get_string, ( uint32_t claim, const char* name, char* value, size_t size )) +{ + if (value == NULL || size == 0 || g_lock == NULL) + return -EINVAL; + dmosi_mutex_lock(g_lock); + prop_t* p = find_prop(find_claim(claim), name); + int ret = (p == NULL) ? -ENOENT : (p->type != DMV_VAR_STR) ? -EINVAL : 0; + if (ret == 0) + { + size_t len = strlen(p->sval); + if (len >= size) + { + len = size - 1U; + ret = -ERANGE; + } + memcpy(value, p->sval, len); + value[len] = '\0'; + } + dmosi_mutex_unlock(g_lock); + return ret; +} + +dmod_libdmview_api_declaration(1.0, int, _wait_event, ( uint32_t claim, libdmview_event_t* event, int32_t timeout_ms )) +{ + if (event == NULL || g_lock == NULL) + return -EINVAL; + dmosi_mutex_lock(g_lock); + claim_t* c = find_claim(claim); + if (c != NULL) + c->waiters++; + dmosi_mutex_unlock(g_lock); + if (c == NULL) + return -ENOENT; + + int ret = -ETIMEDOUT; + if (dmosi_semaphore_wait(c->ready, 1, timeout_ms) == 0) + { + dmosi_mutex_lock(g_lock); + if (c->released) + ret = -ENOENT; + else if (c->count > 0) + { + *event = c->events[c->first]; + event->dropped = c->dropped; + c->dropped = 0; + c->first = (c->first + 1U) % LIBDMVIEW_EVENT_QUEUE; + c->count--; + ret = 0; + } + dmosi_mutex_unlock(g_lock); + } + + dmosi_mutex_lock(g_lock); + c->waiters--; + bool last = c->released && c->waiters == 0; + if (c->released && c->waiters > 0) + (void)dmosi_semaphore_post(c->ready, 1); /* The next waiter learns it too */ + dmosi_mutex_unlock(g_lock); + if (last) + claim_free(c); + return ret; +} + +/* ---- The channel: the view's side (the service's thread) ---- */ + +dmod_libdmview_api_declaration(1.0, int, _attach, ( libdmview_t view, libdmview_display_t display )) +{ + if (view == NULL || view->magic != VIEW_MAGIC || !is_display(display)) + return -EINVAL; + dmosi_mutex_lock(g_lock); + claim_t* c = (display->count > 0) ? display->claims[display->count - 1U] : NULL; + view->claim = (c != NULL) ? c->id : 0; + /* What the view holds is what the application reads - unless it set it meanwhile */ + for (uint32_t i = 0; c != NULL && i < view->var_count; i++) + { + const dmv_var_t* var = &view->vars[i]; + prop_t* p = ((var->flags & DMV_VARF_PROP) != 0) ? find_prop(c, view_string(view, var->env)) : NULL; + if (p == NULL || p->pending || p->type != var->type) + continue; + if (p->type == DMV_VAR_STR) + { + strncpy(p->sval, view->strs[i], p->capacity); + p->sval[p->capacity] = '\0'; + } + else + p->ival = view->ints[i]; + } + dmosi_mutex_unlock(g_lock); + return (c != NULL) ? 0 : -ENOENT; +} + +/* A property the view changed itself */ +void claims_publish(struct libdmview* v, uint32_t index) +{ + if (v->claim == 0 || g_lock == NULL) + return; + const dmv_var_t* var = &v->vars[index]; + dmosi_mutex_lock(g_lock); + prop_t* p = find_prop(find_claim(v->claim), view_string(v, var->env)); + if (p != NULL && !p->pending && p->type == var->type) + { + if (p->type == DMV_VAR_STR) + { + strncpy(p->sval, v->strs[index], p->capacity); + p->sval[p->capacity] = '\0'; + } + else + p->ival = v->ints[index]; + } + dmosi_mutex_unlock(g_lock); +} + +/* EMIT */ +void claims_emit(struct libdmview* v, const char* name, int32_t value) +{ + if (v->claim == 0 || g_lock == NULL) + return; + dmosi_mutex_lock(g_lock); + claim_t* c = find_claim(v->claim); + if (c != NULL) + { + bool full = c->count == LIBDMVIEW_EVENT_QUEUE; + if (full) + { + /* The oldest goes - its post stays for the new one */ + c->first = (c->first + 1U) % LIBDMVIEW_EVENT_QUEUE; + c->count--; + c->dropped++; + } + libdmview_event_t* e = &c->events[(c->first + c->count) % LIBDMVIEW_EVENT_QUEUE]; + strncpy(e->name, name, sizeof(e->name) - 1U); + e->name[sizeof(e->name) - 1U] = '\0'; + e->value = value; + e->dropped = 0; + c->count++; + if (!full) + (void)dmosi_semaphore_post(c->ready, 1); + } + dmosi_mutex_unlock(g_lock); +} + +/* What the application set, into the view, with the view's .change handlers */ +void claims_apply(struct libdmview* v) +{ + if (v->claim == 0 || g_lock == NULL) + return; + for (;;) + { + /* One property at a time, out of the lock: the handlers it runs may publish and emit */ + dmosi_mutex_lock(g_lock); + claim_t* c = find_claim(v->claim); + prop_t* p = NULL; + for (uint32_t i = 0; c != NULL && c->pending && i < c->prop_count && p == NULL; i++) + { + if (c->props[i].pending) + p = &c->props[i]; + } + if (c != NULL && p == NULL) + c->pending = false; + int32_t ival = 0; + char* sval = NULL; + char* name = NULL; + uint8_t type = 0; + if (p != NULL) + { + p->pending = false; + type = p->type; + ival = p->ival; + name = Dmod_StrDup(p->name); + if (type == DMV_VAR_STR) + sval = Dmod_StrDup(p->sval); + } + dmosi_mutex_unlock(g_lock); + if (p == NULL) + return; + if (name == NULL || (type == DMV_VAR_STR && sval == NULL)) + { + if (name != NULL) + Dmod_Free(name); + if (sval != NULL) + Dmod_Free(sval); + return; + } + + for (uint32_t i = 0; i < v->var_count; i++) + { + const dmv_var_t* var = &v->vars[i]; + if ((var->flags & DMV_VARF_PROP) == 0 || var->type != type || + strcmp(view_string(v, var->env), name) != 0) + continue; + if (type == DMV_VAR_STR) + view_set_string(v, i, sval); + else + view_set_int(v, i, ival); + for (uint32_t k = 0; k < v->item_count; k++) + { + if (v->items[k].kind == DMV_ITEM_CHANGE && v->items[k].value == i) + exec_handler(v, ROOT, v->items[k].label); + } + } + Dmod_Free(name); + if (sval != NULL) + Dmod_Free(sval); + } +} diff --git a/src/exec.c b/src/exec.c index 28b3c85..c94d04f 100644 --- a/src/exec.c +++ b/src/exec.c @@ -598,6 +598,10 @@ static void run(struct libdmview* v, uint32_t pc, bool box_region) break; case DMV_OP_SETFOCUS: break; + case DMV_OP_EMIT: + if (!v->drawing) + claims_emit(v, vstr(v, insn, 0, OPOFF(EMIT, 0)), v32(v, insn, 1, OPOFF(EMIT, 1))); + break; default: return; diff --git a/src/input.c b/src/input.c index d3662eb..75d34ee 100644 --- a/src/input.c +++ b/src/input.c @@ -234,6 +234,9 @@ dmod_libdmview_api_declaration(1.0, uint32_t, _update, ( libdmview_t view, uint3 set_now(v, now_ms); uint32_t t = now_ms - v->start_ms, next = LIBDMVIEW_NO_DEADLINE; + /* What the application set through the claim, and the .change handlers */ + claims_apply(v); + for (uint32_t i = 0; i < v->item_count; i++) { item_t* item = &v->items[i]; diff --git a/src/layout.h b/src/layout.h index eddfc33..6bf75b5 100644 --- a/src/layout.h +++ b/src/layout.h @@ -56,6 +56,7 @@ static inline uint8_t layout_offset(uint8_t op, unsigned i) case DMV_OP_RELOAD: case DMV_OP_SETFOCUS: { static const uint8_t o[] = { 4 }; return o[i]; } case DMV_OP_SCROLLTO: { static const uint8_t o[] = { 4, 6, 8 }; return o[i]; } + case DMV_OP_EMIT: { static const uint8_t o[] = { 4, 8 }; return o[i]; } default: return 0; } } diff --git a/src/private.h b/src/private.h index 8c1edf1..e4e6dbf 100644 --- a/src/private.h +++ b/src/private.h @@ -290,6 +290,9 @@ struct libdmview char* goto_path; char* goto_taken; + /* The claim the view is attached to (libdmview_attach()), 0: none */ + uint32_t claim; + /* Images */ char* dir; /* Directory of the view file - relative image paths start there */ uint32_t slot_count; @@ -302,6 +305,9 @@ dmv_status_t dmv_validate(const dmv_input_t* input, uint32_t* error_offset); /* claims.c */ int claims_init(void); void claims_deinit(void); +void claims_publish(struct libdmview* v, uint32_t index); +void claims_emit(struct libdmview* v, const char* name, int32_t value); +void claims_apply(struct libdmview* v); /* fontfile.c */ int fonts_init(void); diff --git a/src/validate.c b/src/validate.c index 1665416..382866d 100644 --- a/src/validate.c +++ b/src/validate.c @@ -152,8 +152,11 @@ static dmv_status_t check_vars(validator_t *v) bool ok = (type == DMV_VAR_INT && capacity == 0) || (type == DMV_VAR_STR && capacity > 0 && string_valid(v, init)); - ok = ok && (flags & ~DMV_VARF_ENV) == 0 && string_valid(v, name); - ok = ok && ((flags & DMV_VARF_ENV) ? string_valid(v, env) : env == DMV_NONE); + /* Bound to dmenv or a property (version 0.6) - not both; `env` names it */ + uint8_t binding = flags & (DMV_VARF_ENV | DMV_VARF_PROP); + ok = ok && (flags & ~(DMV_VARF_ENV | DMV_VARF_PROP)) == 0 && binding != (DMV_VARF_ENV | DMV_VARF_PROP); + ok = ok && string_valid(v, name); + ok = ok && ((binding != 0) ? string_valid(v, env) : env == DMV_NONE); if (!ok) return fail(v, DMV_ERR_TABLE, at); v->var_types[i] = type; @@ -440,6 +443,7 @@ static dmv_status_t check_items(validator_t *v) case DMV_ITEM_TIMER: ok = arg == 0 && value > 0 && is_boundary(v, label); break; case DMV_ITEM_KEY: ok = arg == 0 && value < 32 && is_boundary(v, label); break; case DMV_ITEM_NAVKEY: ok = arg < DMV_NAV_COUNT && value < 32 && label == DMV_NONE; break; + case DMV_ITEM_CHANGE: ok = arg == 0 && value < v->h.vars.count && is_boundary(v, label); break; default: ok = false; break; } if (!ok) diff --git a/src/view.c b/src/view.c index c5e157c..ad9d1a9 100644 --- a/src/view.c +++ b/src/view.c @@ -144,6 +144,8 @@ void view_set_int(struct libdmview* v, uint32_t index, int32_t value) view_invalidate_deps(v, index / 32U, 1U << (index % 32U)); if ((v->vars[index].flags & DMV_VARF_ENV) != 0) env_write_int(v, index); + else if ((v->vars[index].flags & DMV_VARF_PROP) != 0) + claims_publish(v, index); } void view_set_string(struct libdmview* v, uint32_t index, const char* value) @@ -160,6 +162,8 @@ void view_set_string(struct libdmview* v, uint32_t index, const char* value) 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); + else if ((v->vars[index].flags & DMV_VARF_PROP) != 0) + claims_publish(v, index); } /* The string after what the variable holds - as much of it as its size takes */ @@ -184,6 +188,8 @@ void view_append_string(struct libdmview* v, uint32_t index, const char* tail) 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); + else if ((v->vars[index].flags & DMV_VARF_PROP) != 0) + claims_publish(v, index); } /* ---- Loading ---- */ diff --git a/tests/fixtures/channel.dmvs b/tests/fixtures/channel.dmvs new file mode 100644 index 0000000..868f09f --- /dev/null +++ b/tests/fixtures/channel.dmvs @@ -0,0 +1,34 @@ +; The channel of a claim: properties, their .change handlers, EMIT +.view channel +.entry draw + +.prop $title, str[16], "start", "title" +.prop $count, int, 5, "count" +.var $changes, int, 0 +.var $clicks, int, 0 + +.change $title, on_title +.change $count, on_count + +draw: + BOX @all, 0, 0, 64, 48, OPAQUE + ON CLICK, clicked + FILL #000000 + END + RET + +on_title: + ADD $changes, 1 + RET + +on_count: + ADD $changes, 1 + EMIT "count_seen", $count + RET + +clicked: + ADD $clicks, 1 + SET $count, 42 + EMIT "clicked", $clicks + EMIT "plain" + RET diff --git a/tests/libdmview_test.c b/tests/libdmview_test.c index e458cd1..b3722ba 100644 --- a/tests/libdmview_test.c +++ b/tests/libdmview_test.c @@ -904,6 +904,98 @@ DMOD_TEST_STEP(libdmview_claims_stack_per_display) DMOD_TEST_EXPECT_EQ(libdmview_claim(NULL, "/a.dmv", &a), -ENODEV); } +/* ---- The channel of a claim ---- */ + +static void click(int x, int y, uint32_t now) +{ + touch(1, x, y, now); + touch(0, x, y, now + 10); +} + +DMOD_TEST_STEP(libdmview_claims_are_channels) +{ + char text[32]; + int32_t n = 0; + uint32_t claim = 0; + libdmview_event_t ev; + + libdmview_display_t display = libdmview_display_register("chan"); + DMOD_TEST_EXPECT_NOT_NULL(display); + DMOD_TEST_EXPECT_EQ(libdmview_claim("chan", FIXTURE("channel.dmv"), &claim), 0); + + /* The contract is the view's: read from the file when it is claimed */ + DMOD_TEST_EXPECT_EQ(libdmview_prop_get_string(claim, "title", text, sizeof(text)), 0); + DMOD_TEST_EXPECT_EQ(strcmp(text, "start"), 0); + DMOD_TEST_EXPECT_EQ(libdmview_prop_get_int(claim, "count", &n), 0); + DMOD_TEST_EXPECT_EQ(n, 5); + DMOD_TEST_EXPECT_EQ(libdmview_prop_set_int(claim, "nope", 1), -ENOENT); + DMOD_TEST_EXPECT_EQ(libdmview_prop_set_string(claim, "count", "x"), -EINVAL); + DMOD_TEST_EXPECT_EQ(libdmview_prop_get_int(claim, "title", &n), -EINVAL); + DMOD_TEST_EXPECT_EQ(libdmview_prop_get_int(claim, "changes", &n), -ENOENT); /* A .var is not a property */ + DMOD_TEST_EXPECT_EQ(libdmview_prop_get_string(claim, "title", text, 3), -ERANGE); + DMOD_TEST_EXPECT_EQ(strcmp(text, "st"), 0); + DMOD_TEST_EXPECT_EQ(libdmview_prop_set_int(claim + 1000u, "count", 1), -ENOENT); + + /* Set before the view is shown: taken in when it is, with its .change handler */ + DMOD_TEST_EXPECT_EQ(libdmview_prop_set_string(claim, "title", "a much longer title than 16"), 0); + DMOD_TEST_EXPECT_TRUE(open_fixture(FIXTURE("channel.dmv"))); + DMOD_TEST_EXPECT_EQ(libdmview_attach(g_view, display), 0); + libdmview_update(g_view, 0); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "changes", &n), 0); + DMOD_TEST_EXPECT_EQ(n, 1); + DMOD_TEST_EXPECT_EQ(libdmview_prop_get_string(claim, "title", text, sizeof(text)), 0); + DMOD_TEST_EXPECT_EQ(strcmp(text, "a much longer ti"), 0); /* Cut to str[16] */ + DMOD_TEST_EXPECT_EQ(libdmview_wait_event(claim, &ev, 0), -ETIMEDOUT); + + /* A change runs the handler once; an update without one runs nothing */ + DMOD_TEST_EXPECT_EQ(libdmview_prop_set_int(claim, "count", 9), 0); + libdmview_update(g_view, 10); + libdmview_update(g_view, 20); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "count", &n), 0); + DMOD_TEST_EXPECT_EQ(n, 9); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "changes", &n), 0); + DMOD_TEST_EXPECT_EQ(n, 2); + DMOD_TEST_EXPECT_EQ(libdmview_wait_event(claim, &ev, 0), 0); + DMOD_TEST_EXPECT_EQ(strcmp(ev.name, "count_seen"), 0); + DMOD_TEST_EXPECT_EQ(ev.value, 9); + + /* The view's own changes reach the application - not as a change (no handler) */ + libdmview_rect_t r; + libdmview_render(g_view, &g_s32, &r); + click(10, 10, 100); + DMOD_TEST_EXPECT_EQ(libdmview_prop_get_int(claim, "count", &n), 0); + DMOD_TEST_EXPECT_EQ(n, 42); + DMOD_TEST_EXPECT_EQ(libdmview_get_int(g_view, "changes", &n), 0); + DMOD_TEST_EXPECT_EQ(n, 2); + DMOD_TEST_EXPECT_EQ(libdmview_wait_event(claim, &ev, 10), 0); + DMOD_TEST_EXPECT_EQ(strcmp(ev.name, "clicked"), 0); + DMOD_TEST_EXPECT_EQ(ev.value, 1); + DMOD_TEST_EXPECT_EQ(libdmview_wait_event(claim, &ev, 10), 0); + DMOD_TEST_EXPECT_EQ(strcmp(ev.name, "plain"), 0); + DMOD_TEST_EXPECT_EQ(ev.value, 0); + DMOD_TEST_EXPECT_EQ(ev.dropped, 0u); + + /* Too many events: the oldest go, the next one says how many */ + for (uint32_t i = 0; i < 10; i++) + click(10, 10, 200 + i * 100); /* 20 events, 16 kept */ + DMOD_TEST_EXPECT_EQ(libdmview_wait_event(claim, &ev, 0), 0); + DMOD_TEST_EXPECT_EQ(ev.dropped, 4u); + DMOD_TEST_EXPECT_EQ(strcmp(ev.name, "clicked"), 0); + DMOD_TEST_EXPECT_EQ(ev.value, 4); /* The 4th click (one before the loop) */ + uint32_t left = 0; + while (libdmview_wait_event(claim, &ev, 0) == 0) + left++; + DMOD_TEST_EXPECT_EQ(left, 15u); + + /* Released: the channel is gone, the view sends nowhere */ + DMOD_TEST_EXPECT_EQ(libdmview_release(claim), 0); + DMOD_TEST_EXPECT_EQ(libdmview_prop_set_int(claim, "count", 1), -ENOENT); + DMOD_TEST_EXPECT_EQ(libdmview_wait_event(claim, &ev, 0), -ENOENT); + click(10, 10, 2000); + DMOD_TEST_EXPECT_EQ(libdmview_attach(g_view, display), -ENOENT); /* No claim on the display */ + libdmview_display_unregister(display); +} + /* ---- Images ---- */ /* A .dmvi file (docs/image-format.md): `rows` rows of `row` bytes of