From fe7e40d22a3b2b1c0befc105ca35030cb19b0703 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Mon, 12 Oct 2026 13:03:24 +0200 Subject: [PATCH 1/2] dmvs_html: a script's texts in every look; own visibility in a change Two things a list on a hidden screen (a page's screen not shown yet) got wrong, both seen in the car HMI's queue: - The text a script sets (innerText) was its variable only in the element as the page has it; in its other looks - laid out in another state, a class the script changes (the playing row's) - the text was the page's, as it was before the script: clicking a row showed the design's titles. A look's elements now keep the element they are a look of (`like`, set as the looks are matched), and a text is painted from its variable. - A class that hides the element (visibility) was not converted when the element was hidden anyway - by its screen, as the page is: hidden then and hidden after, nothing changed. The opacity a change compares, binds and fades to is now the element's own (its own visibility; what the boxes around it inherit stays theirs), and an element hidden by itself is revealed by a change that shows it - its inside by its own visibility, or as its parent then is. The rows a list had too many of stayed on it. Co-Authored-By: Claude Opus 5.5 --- src/paint.c | 27 ++++++++++++++---- src/private.h | 8 ++++++ src/script.c | 35 +++++++++++++++-------- src/style.c | 7 ++++- tests/dmvs_html_test.c | 63 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 18 deletions(-) diff --git a/src/paint.c b/src/paint.c index 971f1d9..876f0be 100644 --- a/src/paint.c +++ b/src/paint.c @@ -498,11 +498,25 @@ static void paint_decoration(painter_t* p, const style_t* st, dmvsi_rect_t r, co } /* The element (n or inside it) whose text a script sets that a text is of - NULL: none */ +/* + * The text a script sets in an element: its own variable, or - the element + * as it looks in another state (a class a script changes) - the variable of + * the element it is a look of: what the script set shows in every look. + */ +static const dynamic_t* text_dynamic(const node_t* e) +{ + if (e->dynamic != NULL && e->dynamic->text != 0) + return e->dynamic; + if (e->like != NULL && e->like->dynamic != NULL && e->like->dynamic->text != 0) + return e->like->dynamic; + return NULL; +} + static const node_t* text_set(const node_t* text, const node_t* n) { for (const node_t* e = text; e != NULL; e = e->parent) { - if (e->kind == NODE_ELEMENT && e->dynamic != NULL && e->dynamic->text != 0) + if (e->kind == NODE_ELEMENT && text_dynamic(e) != NULL) return e; if (e == n) break; @@ -521,15 +535,16 @@ static void paint_text_var(painter_t* p, const node_t* block, const node_t* e, c const style_t* st = e->style; if (st == NULL || st->hidden || (st->color >> 24) == 0) return; + const dynamic_t* d = text_dynamic(e); dmvsi_var_info_t info; - if (dmvsi_var_info(p->c->doc, e->dynamic->text, &info) != 0) + if (d == NULL || dmvsi_var_info(p->c->doc, d->text, &info) != 0) return; dmvsi_text_t t; memset(&t, 0, sizeof(t)); - t.var = e->dynamic->text; + t.var = d->text; t.text = info.text; t.length = strlen(info.text); - t.chars = e->dynamic->chars; + t.chars = d->chars; t.font = (f != NULL) ? f->font : style_font(p->c, (style_t*)st); t.paint.color = p->shadow ? times_alpha(p->shadow_color, st->color >> 24) : st->color; if (t.font == NULL) @@ -804,7 +819,7 @@ static void paint_inner(painter_t* p, node_t* n, uint32_t depth) for (; layers != NULL && i < layers->count && layers->items[i].z < 0; i++) paint_context(p, layers->items[i].node, depth + 1U); paint_lines(p, n); - if (n->dynamic != NULL && n->dynamic->text != 0 && n->box.frags == NULL) + if (text_dynamic(n) != NULL && n->box.frags == NULL) paint_text_var(p, n, n, NULL); /* A script's text in what has none yet */ paint_flow(p, n, depth + 1U); for (; layers != NULL && i < layers->count; i++) @@ -899,7 +914,7 @@ static void paint_flow_element(painter_t* p, node_t* k, uint32_t depth) else if (node_is(k, "svg")) paint_svg(p, k); paint_lines(p, k); - if (k->dynamic != NULL && k->dynamic->text != 0 && k->box.frags == NULL && k->style->display != DISPLAY_INLINE) + if (text_dynamic(k) != NULL && k->box.frags == NULL && k->style->display != DISPLAY_INLINE) paint_text_var(p, k, k, NULL); /* A script's text in what has none yet */ } paint_flow(p, k, depth + 1U); diff --git a/src/private.h b/src/private.h index e833e62..651c2b2 100644 --- a/src/private.h +++ b/src/private.h @@ -103,6 +103,11 @@ typedef struct bool placed; /* Its absolute position is known */ } box_t; +/* style_t::visibility: what the element's own style says (inherited: its parent's hidden) */ +#define VISIBILITY_INHERITED 0u +#define VISIBILITY_VISIBLE 1u +#define VISIBILITY_HIDDEN 2u + /* One look of an element: the element as laid out in a state of the page, shown on its conditions */ #define MAX_VARIANTS 8u @@ -131,6 +136,8 @@ struct node uint8_t kind; uint32_t index; /* Of the elements, in document order (as parsed) */ dynamic_t* dynamic; /* NULL: what it is it stays */ + const struct node* like; /* A look of an element (its tree in a state): that element - + the text a script sets in it is the element's */ bool active; /* Pressed: :active holds (MOD_ACTIVE) */ uint8_t pseudo; char* tag; /* Lowercase; NULL for text */ @@ -361,6 +368,7 @@ struct style uint8_t overflow_x, overflow_y; bool border_box; /* box-sizing */ bool hidden; /* visibility */ + uint8_t visibility; /* ... as the element's own style gives it: VISIBILITY_* */ len_t inset[4]; /* top, right, bottom, left */ int32_t z_index; bool z_auto; diff --git a/src/script.c b/src/script.c index 1c9cd0d..a56e722 100644 --- a/src/script.c +++ b/src/script.c @@ -1228,10 +1228,16 @@ static uint32_t look_of(const node_t* e) return inside(e, e, h, 0); } -/* How opaque an element is seen: not at all while it is hidden (visibility) */ -static int32_t seen_opacity(const style_t* st) +/* + * How opaque an element makes itself: hidden by its own visibility, not by + * a box around it - what a change of its classes can change. A row of a + * list on a screen hidden as the page is (its screen shows it later) is + * shown or hidden by its own class all the same. + */ +static int32_t own_opacity(const style_t* st) { - return st->hidden ? 0 : st->opacity; + bool hidden = (st->visibility == VISIBILITY_INHERITED) ? false : st->visibility == VISIBILITY_HIDDEN; + return hidden ? 0 : st->opacity; } /* "+a -b": whether the element has all that already */ @@ -1279,6 +1285,8 @@ static void seen_like(node_t* look, const node_t* e, uint32_t depth) return; if (look->style != NULL && e->style != NULL && !e->style->hidden) look->style->hidden = false; + if (look != e) + look->like = e; const node_t* g = e->first; for (node_t* k = look->first; k != NULL; k = k->next) { @@ -1293,7 +1301,9 @@ static void seen_like(node_t* look, const node_t* e, uint32_t depth) } /* An element a change shows that is hidden (visibility) as the page is: shown, its opacity - * what hides it - so that it can fade in. Its inside hidden as it is then */ + * what hides it - so that it can fade in. Its inside hidden as it is then: by its own + * visibility, or inheriting it - not by the boxes around the element (a screen hidden as + * the page is shows it later) */ static void reveal(node_t* e, const node_t* f, uint32_t depth) { if (e->style != NULL && f->style != NULL && e->kind == NODE_ELEMENT && depth == 0) @@ -1305,7 +1315,9 @@ static void reveal(node_t* e, const node_t* f, uint32_t depth) break; if (k->kind == NODE_ELEMENT && k->style != NULL && g->style != NULL) { - if (k->style->hidden && !g->style->hidden) + bool hidden = (g->style->visibility == VISIBILITY_INHERITED) ? e->style->hidden + : g->style->visibility == VISIBILITY_HIDDEN; + if (k->style->hidden && !hidden) k->style->hidden = false; reveal(k, g, depth + 1U); } @@ -1319,7 +1331,7 @@ static int lay_out_change(script_t* sc, change_t* ch) conv_t* c = sc->c; node_t* e = ch->element; ch->laid_out = true; - ch->opacity = seen_opacity(e->style); + ch->opacity = own_opacity(e->style); const style_t* after = e->style; /* The style it has then: its transition */ if ((ch->mod.kind == MOD_CLASS_ADD && has_class(e, ch->mod.name)) || (ch->mod.kind == MOD_CLASS_REMOVE && !has_class(e, ch->mod.name)) || @@ -1346,9 +1358,10 @@ static int lay_out_change(script_t* sc, change_t* ch) view_origin(s, &sx, &sy); ch->dx = (f->box.ax - sx) - (e->box.ax - ox); ch->dy = (f->box.ay - sy) - (e->box.ay - oy); - ch->opacity = seen_opacity(f->style); - if (e->style->hidden && !f->style->hidden) - ch->reveals = f; + ch->opacity = own_opacity(f->style); + if (e->style->hidden && (!f->style->hidden || (e->style->visibility == VISIBILITY_HIDDEN && + f->style->visibility != VISIBILITY_HIDDEN))) + ch->reveals = f; /* (hidden by itself, or hidden no more) */ if (look_of(f) != look_of(e) || !e->box.placed) { ch->changed = true; /* Another look */ @@ -1428,7 +1441,7 @@ static int bind_element(script_t* sc, node_t* e) continue; moves_x = moves_x || ch->dx != 0; moves_y = moves_y || ch->dy != 0; - fades = fades || ch->opacity != seen_opacity(e->style); + fades = fades || ch->opacity != own_opacity(e->style); } if (!moves_x && !moves_y && !fades) return 0; @@ -1452,7 +1465,7 @@ static int bind_element(script_t* sc, node_t* e) if (fades) { Dmod_SnPrintf(name, sizeof(name), "%s_alpha", base); - e->dynamic->bind[DMVSI_BIND_OPACITY] = dmvsi_add_var(sc->c->doc, name, seen_opacity(e->style)); + e->dynamic->bind[DMVSI_BIND_OPACITY] = dmvsi_add_var(sc->c->doc, name, own_opacity(e->style)); } return 0; } diff --git a/src/style.c b/src/style.c index 05410c1..d864700 100644 --- a/src/style.c +++ b/src/style.c @@ -1717,7 +1717,12 @@ static void apply(apply_t* a, const char* name, const char* v) return; } if (strcmp(name, "box-sizing") == 0) { st->border_box = word_is(s, "border-box"); return; } - if (strcmp(name, "visibility") == 0) { st->hidden = inherit ? pa->hidden : !word_is(s, "visible"); return; } + if (strcmp(name, "visibility") == 0) + { + st->hidden = inherit ? pa->hidden : !word_is(s, "visible"); + st->visibility = inherit ? VISIBILITY_INHERITED : st->hidden ? VISIBILITY_HIDDEN : VISIBILITY_VISIBLE; + return; + } if (strcmp(name, "z-index") == 0) { int64_t n; diff --git a/tests/dmvs_html_test.c b/tests/dmvs_html_test.c index de61452..aa04581 100644 --- a/tests/dmvs_html_test.c +++ b/tests/dmvs_html_test.c @@ -633,6 +633,69 @@ static uint32_t clicks_setting(dmvsi_doc_t doc, const dmvsi_node_t* n, dmvsi_var return count; } +/* + * A list on a screen hidden as the page is: the texts a script sets show in + * every look of a row (a class it changes), and a row hidden by its own class + * is faded in and out by it - not as hidden all the time as its screen is. + */ +DMOD_TEST_STEP(dmvs_html_converts_a_list_on_a_hidden_screen) +{ + int status = -1; + DMOD_TEST_EXPECT_TRUE(write_file(TEST_FILE("list.html"), + "
" + "
Demo A
" + "
Demo B
" + "
Demo C
" + "
")); + dmvsi_doc_t doc = convert(TEST_FILE("list.html"), "screen", 0, 0, &status); + DMOD_TEST_EXPECT_EQ(status, 0); + if (doc == NULL) + return; + const dmvsi_node_t* root = dmvsi_root(doc); + + /* No look of a row shows the text the page had before the script set its own */ + DMOD_TEST_EXPECT_EQ(count_texts(root, "Demo A"), 0u); + DMOD_TEST_EXPECT_EQ(count_texts(root, "Demo B"), 0u); + DMOD_TEST_EXPECT_EQ(count_texts(root, "Demo C"), 0u); + + /* Each row is faded by its opacity's variable, shown as the page has it */ + static const char rows[3][8] = { "row-0", "row-1", "row-2" }; + for (uint32_t i = 0; i < 3U; i++) + { + const dmvsi_node_t* r = group_named(root->first, rows[i]); + DMOD_TEST_EXPECT_TRUE(r != NULL && r->bind[DMVSI_BIND_OPACITY] != 0); + DMOD_TEST_EXPECT_TRUE(r != NULL && var_initial(doc, r->bind[DMVSI_BIND_OPACITY]) == 255); + } + dmvsi_free(doc); +} + DMOD_TEST_STEP(dmvs_html_converts_what_scripts_build) { int status = -1; From d9e3d43b5e1ed62ec51c598ac81bd50ba8f7cab4 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Mon, 12 Oct 2026 13:17:37 +0200 Subject: [PATCH 2/2] dmvs_html: a change of a class puts only what the class changes A change put every variable its element is bound to - its place and its opacity - as the change lays it out. An element's classes change independently, though: the car HMI's rows are faded by `invisible` (the list is shorter) and given `row-active` (the one playing); picking a track removed row-active from the hidden rows and put their opacity back, and they showed again. A change of a class now puts what adding or removing that class does to the element: its place if one of them moves it, its opacity if one of them fades it. Co-Authored-By: Claude Opus 5.5 --- src/script.c | 31 ++++++++++++++++++++++++++++--- tests/dmvs_html_test.c | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/script.c b/src/script.c index a56e722..a040e4a 100644 --- a/src/script.c +++ b/src/script.c @@ -1516,6 +1516,28 @@ static void put_to(script_t* sc, dmvsi_var_t var, int32_t value, uint16_t ms, co } } +/* + * What a class does to an element: whether adding or removing it (any of + * its changes) moves it or fades it. A change of a class puts only that - + * an element's classes are changed independently (the playing row of a + * list, a row hidden as the list is shorter), and one that does not fade + * it must not undo the opacity another put. + */ +static void class_effects(script_t* sc, const change_t* ch, bool* moves_x, bool* moves_y, bool* fades) +{ + *moves_x = *moves_y = *fades = false; + for (uint32_t i = 0; i < sc->change_count; i++) + { + const change_t* k = &sc->changes[i]; + if (k->element != ch->element || k->changed || !k->laid_out || + (k->mod.kind != MOD_CLASS_ADD && k->mod.kind != MOD_CLASS_REMOVE) || strcmp(k->mod.name, ch->mod.name) != 0) + continue; + *moves_x = *moves_x || k->dx != 0; + *moves_y = *moves_y || k->dy != 0; + *fades = *fades || k->opacity != own_opacity(k->element->style); + } +} + /* The actions of a change of one element: its variables to where the change puts it */ static void apply(script_t* sc, node_t* e, uint8_t kind, const char* name, const char* value) { @@ -1541,11 +1563,14 @@ static void apply(script_t* sc, node_t* e, uint8_t kind, const char* name, const int32_t ox, oy; view_origin(sc->c, &ox, &oy); dmvsi_rect_t r = group_rect(sc->c, e, ox, oy); - if (e->dynamic->bind[DMVSI_BIND_X] != 0) + bool moves_x = true, moves_y = true, fades = true; + if (kind == MOD_CLASS_ADD || kind == MOD_CLASS_REMOVE) + class_effects(sc, ch, &moves_x, &moves_y, &fades); + if (e->dynamic->bind[DMVSI_BIND_X] != 0 && moves_x) put_to(sc, e->dynamic->bind[DMVSI_BIND_X], r.x + ch->dx, ch->move_ms, ch->move_easing); - if (e->dynamic->bind[DMVSI_BIND_Y] != 0) + if (e->dynamic->bind[DMVSI_BIND_Y] != 0 && moves_y) put_to(sc, e->dynamic->bind[DMVSI_BIND_Y], r.y + ch->dy, ch->move_ms, ch->move_easing); - if (e->dynamic->bind[DMVSI_BIND_OPACITY] != 0) + if (e->dynamic->bind[DMVSI_BIND_OPACITY] != 0 && fades) put_to(sc, e->dynamic->bind[DMVSI_BIND_OPACITY], ch->opacity, ch->fade_ms, ch->fade_easing); } diff --git a/tests/dmvs_html_test.c b/tests/dmvs_html_test.c index aa04581..70c02a9 100644 --- a/tests/dmvs_html_test.c +++ b/tests/dmvs_html_test.c @@ -693,6 +693,26 @@ DMOD_TEST_STEP(dmvs_html_converts_a_list_on_a_hidden_screen) DMOD_TEST_EXPECT_TRUE(r != NULL && r->bind[DMVSI_BIND_OPACITY] != 0); DMOD_TEST_EXPECT_TRUE(r != NULL && var_initial(doc, r->bind[DMVSI_BIND_OPACITY]) == 255); } + + /* Picking a row changes no row's opacity: row-active does not fade, invisible does */ + const dmvsi_node_t* r1 = group_named(root->first, "row-1"); + if (r1 != NULL && r1->click != 0) + { + const dmvsi_action_t* acts = NULL; + uint32_t n = flat_actions(doc, r1->click, &acts); + uint32_t fades = 0; + for (uint32_t i = 0; i < n; i++) + { + for (uint32_t k = 0; k < 3U; k++) + { + const dmvsi_node_t* r = group_named(root->first, rows[k]); + if (r != NULL && acts[i].var == r->bind[DMVSI_BIND_OPACITY] && + (acts[i].kind == DMVSI_ACT_SET || acts[i].kind == DMVSI_ACT_ANIMATE)) + fades++; + } + } + DMOD_TEST_EXPECT_EQ(fades, 0u); + } dmvsi_free(doc); }