diff options
| -rw-r--r-- | packages/kernel/include/unbox/kernel/server.hpp | 13 | ||||
| -rw-r--r-- | packages/kernel/include/unbox/kernel/ui.hpp | 61 | ||||
| -rw-r--r-- | packages/kernel/src/server.cpp | 8 | ||||
| -rw-r--r-- | packages/kernel/src/ui_substrate.cpp | 290 | ||||
| -rw-r--r-- | packages/kernel/src/ui_substrate.hpp | 17 | ||||
| -rw-r--r-- | packages/kernel/tests/test_kernel.cpp | 130 |
6 files changed, 515 insertions, 4 deletions
diff --git a/packages/kernel/include/unbox/kernel/server.hpp b/packages/kernel/include/unbox/kernel/server.hpp index 190d4d5..4cb5230 100644 --- a/packages/kernel/include/unbox/kernel/server.hpp +++ b/packages/kernel/include/unbox/kernel/server.hpp @@ -103,6 +103,19 @@ public: // known source color reached the expected spot inside an <img>. [[nodiscard]] auto ui_pixel(int x, int y) const -> unsigned int; + // Count elements with the given tag name in the first ui surface's loaded + // document. 0 if no surface / no document yet. Lets the suite assert that a + // data-for list rendered the expected number of rows (slice 10 / b2 list + // bindings) without synthesizing input. Test instrumentation; single-thread. + [[nodiscard]] auto ui_element_count(const char* tag) const -> int; + + // Dispatch a click on the `index`-th element with tag `tag` in the first ui + // surface's document (RmlUi Element::Click). Returns false if no such + // element. Lets the suite fire a list row's data-event-click and assert the + // per-row event callback got the right index — no real input device needed. + // Test instrumentation; single-thread only. + auto ui_click_element(const char* tag, int index) -> bool; + // Pin the substrate's touch-mode for tests (none = automatic). Mirrors // UiSubstrate::TouchModeOverride; lets the suite drive the state machine and // its on_touch_mode_changed notification. Test instrumentation; diff --git a/packages/kernel/include/unbox/kernel/ui.hpp b/packages/kernel/include/unbox/kernel/ui.hpp index 3a704cd..7f7609a 100644 --- a/packages/kernel/include/unbox/kernel/ui.hpp +++ b/packages/kernel/include/unbox/kernel/ui.hpp @@ -2,6 +2,7 @@ #include <unbox/kernel/host.hpp> // SceneLayer (and, transitively, wlr.hpp-free types) +#include <cstddef> #include <functional> #include <memory> #include <string> @@ -22,9 +23,10 @@ // DEFERRED (documented, not built this slice): // - Keyboard into ui surfaces (text input + focus): OUT of slice 5. OSK is // slice 8, launcher text slice 6 — those slices add the keyboard path. -// - List / container data bindings: see UiSurface::bind_* notes. Slice 6's -// taskbar will change-request the exact list shape; only scalar + event -// bindings ship now. +// - Keyboard into ui surfaces only (text input + focus); see above. List / +// container data bindings now SHIP (slice 10's stage dock forced the shape): +// see UiSurface::bind_list / bind_list_<type> / bind_list_event. NESTED +// lists (a list field whose value is itself a list) remain unsupported. // // Everything runs on the single wl_event_loop thread. RML assets live under // assets/<unit>/ per the harness; pass either inline RML or an asset path. @@ -91,9 +93,60 @@ public: // Ignoring it is fine — the surface simply looks the same in both modes. virtual void on_touch_mode_changed(std::function<void(bool touch)> callback) = 0; + // ---- List bindings (typed, RMLUi-free) ---- + // A LIST is a runtime-sized, indexed sequence the document iterates with + // <div data-for="row : <name>"> … {{ row.<field> }} … </div> + // (the iterator alias — "row" above — is the author's choice; the count and + // the per-field getters you register here drive what each row renders). + // + // bind_list(name, count): declare the list. `count()` is called by the + // substrate every time the list re-renders (after dirty(name) or dirty()) + // to size the loop; rows are indexed 0..count()-1. It must be cheap and + // pure (no event-loop blocking). A count() that throws is caught and + // isolates your extension (the list then renders as empty for that frame). + // + // bind_list_<type>(list, field, getter): declare a per-row FIELD `<field>` + // of the named list, read in RML as {{ row.<field> }}. For each visible row + // the substrate calls getter(row_index) to produce that cell's value. Same + // contract as the scalar getters: cheap, pure, called for the surface's + // lifetime, capturing only state that outlives this surface; a throwing + // getter isolates your extension. Register a field before the first frame; + // re-registering the same (list, field) replaces the getter. string is the + // workhorse (preview/favicon URIs, titles); int/double/bool are provided + // for numeric/flag cells. + // + // bind_list_event(list, event, callback): declare a per-row EVENT. Author + // it on a row element as e.g. data-event-click="<event>(it_index)" — the + // `it_index` argument is the row index the document supplies, and the + // substrate delivers it to callback(row_index). (Omit the argument and you + // get row 0; always pass it_index for the real row.) Invoked on the + // event-loop thread; a throwing callback is caught at the substrate + // boundary and disables YOUR extension only — never the session. Register + // before the first frame; re-registering the same (list, event) replaces + // the callback. + // + // ALL of bind_list* must be called BEFORE the first frame (same rule as the + // scalar bind_*: RmlUi needs the data model complete before it parses the + // document). Binding a field/event/list after the first frame is a no-op. + // Row fields may be added in any order relative to bind_list for the same + // name. NESTED lists (a list field that is itself a list) are NOT supported. + virtual void bind_list(std::string_view name, std::function<std::size_t()> count) = 0; + virtual void bind_list_string(std::string_view list, std::string_view field, + std::function<std::string(std::size_t row)> getter) = 0; + virtual void bind_list_int(std::string_view list, std::string_view field, + std::function<int(std::size_t row)> getter) = 0; + virtual void bind_list_double(std::string_view list, std::string_view field, + std::function<double(std::size_t row)> getter) = 0; + virtual void bind_list_bool(std::string_view list, std::string_view field, + std::function<bool(std::size_t row)> getter) = 0; + virtual void bind_list_event(std::string_view list, std::string_view event, + std::function<void(std::size_t row)> callback) = 0; + // Mark a bound scalar changed so the substrate re-reads its getter and // re-renders on the next frame. dirty() with no name marks ALL bound - // scalars dirty (use sparingly). + // scalars dirty (use sparingly). For a list, dirty(<list-name>) re-reads + // its count() and re-reads every visible row's field getters, growing or + // shrinking the rendered rows to match the new count on the next frame. virtual void dirty(std::string_view name) = 0; virtual void dirty() = 0; diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp index b302c68..95631df 100644 --- a/packages/kernel/src/server.cpp +++ b/packages/kernel/src/server.cpp @@ -89,6 +89,14 @@ auto Server::ui_pixel(int x, int y) const -> unsigned int { return impl_->substrate != nullptr ? impl_->substrate->surface_pixel(x, y) : 0U; } +auto Server::ui_element_count(const char* tag) const -> int { + return impl_->substrate != nullptr ? impl_->substrate->element_count(tag) : 0; +} + +auto Server::ui_click_element(const char* tag, int index) -> bool { + return impl_->substrate != nullptr && impl_->substrate->click_element(tag, index); +} + void Server::ui_set_touch_override(UiTouchOverride ov) { if (impl_->substrate == nullptr) { return; diff --git a/packages/kernel/src/ui_substrate.cpp b/packages/kernel/src/ui_substrate.cpp index 9c53770..bbe0600 100644 --- a/packages/kernel/src/ui_substrate.cpp +++ b/packages/kernel/src/ui_substrate.cpp @@ -5,9 +5,11 @@ #include <RmlUi/Core/Context.h> #include <RmlUi/Core/Core.h> #include <RmlUi/Core/DataModelHandle.h> +#include <RmlUi/Core/DataVariable.h> #include <RmlUi/Core/Element.h> #include <RmlUi/Core/ElementDocument.h> #include <RmlUi/Core/SystemInterface.h> +#include <RmlUi/Core/Variant.h> // The kernel owns GL; system EGL/GLES headers are allowed here (same as the // retired spike). wlr.hpp already pulled <EGL/egl.h>+<EGL/eglext.h> via @@ -342,6 +344,26 @@ struct Surface { }; std::list<EventBinding> event_bindings; + // List bindings (slice 10 / b2). A bound list is a runtime-sized indexed + // sequence the document iterates with data-for; each row exposes named + // string/int/double/bool FIELDS read as {{ row.<field> }}. The shape maps + // onto RmlUi's data-binding type system via three owned VariableDefinitions + // per list (Array -> row Struct -> per-field Scalar); the row index is + // smuggled through the DataVariable `void* ptr` (no per-row heap object). + // All getters/count follow the scalar contract (cheap, pure, lifetime = + // surface, throw => isolate). Stored in a std::list so addresses are stable + // (the VariableDefinitions hold a ListBinding*). Defined below the Surface. + struct ListBinding; + std::list<ListBinding> list_bindings; + // Per-list event callbacks (keyed by event name). A row event delivers the + // row index extracted from the data expression's first argument (it_index). + struct ListEventBinding { + std::function<void(std::size_t)> cb; + ExtensionId who; + Substrate::Impl* owner; + }; + std::list<ListEventBinding> list_event_bindings; + // touch-mode-changed notification (one per surface; see ui.hpp). Fired on a // transition, error-isolated to `who`. touch-mode does NO visual scaling // (user decision) — this is purely an opt-in signal for extensions. @@ -353,6 +375,137 @@ struct Surface { int frame_count = 0; }; +// ---- List bindings: the RMLUi-free list shape -> RmlUi data-binding types --- +// +// data-for="row : <name>" makes RmlUi ask the named variable for its Size() and +// then a Child per index; {{ row.<field> }} asks that child (a row) for a Child +// per field name; the field child is a scalar that yields a Variant. We satisfy +// all three with custom VariableDefinitions (NonCopyMoveable, owned by the +// Surface for its whole life — they outlive the RmlUi context, which is torn +// down first in destroy_surface). The row index is carried through the +// DataVariable's `void* ptr` as an encoded integer, so there is NO per-row heap +// object and rows cost nothing until rendered. count()/getters are called +// straight out of the ListBinding; a throw is isolated to the owning extension. +namespace { +// Encode/decode a row index in the opaque DataVariable ptr (index + 1 so the +// encoded value is never the null we hand RmlUi for an out-of-range child). +inline auto encode_row(std::size_t row) -> void* { + return reinterpret_cast<void*>(static_cast<std::uintptr_t>(row) + 1); +} +inline auto decode_row(void* ptr) -> std::size_t { + return static_cast<std::size_t>(reinterpret_cast<std::uintptr_t>(ptr)) - 1; +} +} // namespace + +// One bound list's full state: the count getter, the per-field getters (by +// name+type), and the three VariableDefinitions wired Array -> Struct -> Scalar. +// `isolate` lets a getter throw without taking down the session (it calls the +// substrate's DisableSink for `who`). Lives in Surface::list_bindings. +struct Surface::ListBinding { + std::string name; + std::function<std::size_t()> count; + ExtensionId who{}; + // A copy of the substrate's DisableSink so a throwing count/getter isolates + // the owning extension WITHOUT this struct (defined before Substrate::Impl) + // needing the complete Impl type. + SubstrateDisableFn disable; + + std::unordered_map<std::string, std::function<bool(std::size_t, Rml::Variant&)>> fields; + + // Run a field/count call, isolating a throw to the owning extension. + template <typename Fn> + auto isolate(Fn&& fn) -> bool { + try { + fn(); + return true; + } catch (...) { + if (disable) { + disable(who); + } + return false; + } + } + + // The scalar at (row, field): decode the row, call the field getter. + struct FieldDef final : Rml::VariableDefinition { + FieldDef(ListBinding* b, std::function<bool(std::size_t, Rml::Variant&)>* f) + : Rml::VariableDefinition(Rml::DataVariableType::Scalar), binding(b), field(f) {} + bool Get(void* ptr, Rml::Variant& variant) override { + const std::size_t row = decode_row(ptr); + bool got = false; + binding->isolate([&] { got = (*field)(row, variant); }); + return got; + } + ListBinding* binding; + std::function<bool(std::size_t, Rml::Variant&)>* field; + }; + + // The row struct: a Child per field name (passing the encoded row through). + struct RowDef final : Rml::VariableDefinition { + explicit RowDef(ListBinding* b) + : Rml::VariableDefinition(Rml::DataVariableType::Struct), binding(b) {} + Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override { + auto it = binding->field_defs.find(address.name); + if (it == binding->field_defs.end()) { + return Rml::DataVariable(); + } + return Rml::DataVariable(it->second.get(), ptr); // ptr already encodes the row + } + Rml::StringList ReflectMemberNames() override { + Rml::StringList names; + for (const auto& [n, def] : binding->field_defs) { + names.push_back(n); + } + return names; + } + ListBinding* binding; + }; + + // The array: Size() = count(); Child(i) = a row encoding index i. + struct ArrayDef final : Rml::VariableDefinition { + explicit ArrayDef(ListBinding* b) + : Rml::VariableDefinition(Rml::DataVariableType::Array), binding(b) {} + int Size(void* /*ptr*/) override { + std::size_t n = 0; + if (binding->count) { + binding->isolate([&] { n = binding->count(); }); + } + return static_cast<int>(n); + } + Rml::DataVariable Child(void* /*ptr*/, const Rml::DataAddressEntry& address) override { + if (address.index < 0) { + return Rml::DataVariable(); + } + return Rml::DataVariable(&binding->row_def, encode_row(static_cast<std::size_t>(address.index))); + } + ListBinding* binding; + }; + + // The owned definitions (constructed in init(); addresses stable thereafter + // because ListBinding lives in a std::list). field_defs maps field name -> + // its scalar definition; row_def/array_def are the single struct/array. + std::unordered_map<std::string, std::unique_ptr<FieldDef>> field_defs; + RowDef row_def{nullptr}; + ArrayDef array_def{nullptr}; + + void init() { + // Re-seat the back-pointers now that the ListBinding has its final + // address (it was emplaced into the std::list before init()). + row_def.binding = this; + array_def.binding = this; + } + auto add_field(const std::string& field, std::function<bool(std::size_t, Rml::Variant&)> fn) + -> void { + auto [it, inserted] = fields.insert_or_assign(field, std::move(fn)); + auto def_it = field_defs.find(field); + if (def_it == field_defs.end()) { + field_defs.emplace(field, std::make_unique<FieldDef>(this, &it->second)); + } else { + def_it->second->field = &it->second; // re-seat after insert_or_assign + } + } +}; + // ---- PreviewState ----------------------------------------------------------- // // A frozen snapshot of a scene subtree, imported as a sampled GL texture in the @@ -1267,6 +1420,34 @@ auto Substrate::surface_pixel(int x, int y) const -> std::uint32_t { return 0; } +auto Substrate::element_count(const char* tag) const -> int { + for (const Surface& s : impl_->surfaces) { + if (s.document == nullptr) { + continue; + } + Rml::ElementList elements; + s.document->GetElementsByTagName(elements, Rml::String(tag)); + return static_cast<int>(elements.size()); + } + return 0; +} + +auto Substrate::click_element(const char* tag, int index) -> bool { + for (Surface& s : impl_->surfaces) { + if (s.document == nullptr) { + continue; + } + Rml::ElementList elements; + s.document->GetElementsByTagName(elements, Rml::String(tag)); + if (index < 0 || index >= static_cast<int>(elements.size())) { + return false; + } + elements[static_cast<std::size_t>(index)]->Click(); + return true; + } + return false; +} + auto Substrate::orientation() const -> int { for (const Surface& s : impl_->surfaces) { if (s.dmabuf || s.shm == nullptr || s.frame_count == 0) { @@ -1409,6 +1590,115 @@ void SurfaceHandle::bind_event(std::string_view name, std::function<void()> call }); } +namespace { +// Find an existing list binding by name in the surface, or nullptr. +auto find_list(Surface& s, std::string_view name) -> Surface::ListBinding* { + for (auto& b : s.list_bindings) { + if (b.name == name) { + return &b; + } + } + return nullptr; +} +} // namespace + +void SurfaceHandle::bind_list(std::string_view name, std::function<std::size_t()> count) { + Surface& s = *surface_; + if (!s.ctor) { + return; + } + Surface::ListBinding* b = find_list(s, name); + if (b == nullptr) { + s.list_bindings.emplace_back(); + b = &s.list_bindings.back(); + b->name = std::string(name); + b->who = s.who; + b->disable = s.owner->disable; + b->init(); // stable address now -> seat the definition back-pointers + // Bind the array variable under the list name; data-for reads its + // Size()/Child() to iterate, and each row's Child() resolves the fields. + s.ctor.BindCustomDataVariable(b->name, + Rml::DataVariable(&b->array_def, nullptr)); + } + b->count = std::move(count); +} + +// One template for the four typed field binds: wrap the typed getter in a +// Variant-producing closure (Variant's templated setter handles each type), +// then register it on the list's row struct under `field`. +namespace { +template <typename T, typename Getter> +void bind_list_field_impl(Surface& s, std::string_view list, std::string_view field, + Getter getter) { + if (!s.ctor) { + return; + } + Surface::ListBinding* b = find_list(s, list); + if (b == nullptr) { + // The list must be declared first (bind_list); a field on an unknown + // list is dropped (documented: register the list before its fields... + // they may interleave, but the list name must exist). + wlr_log(WLR_INFO, "ui-substrate: bind_list field '%.*s' for unknown list '%.*s'", + static_cast<int>(field.size()), field.data(), + static_cast<int>(list.size()), list.data()); + return; + } + b->add_field(std::string(field), + [getter = std::move(getter)](std::size_t row, Rml::Variant& out) -> bool { + out = static_cast<T>(getter(row)); + return true; + }); +} +} // namespace + +void SurfaceHandle::bind_list_string(std::string_view list, std::string_view field, + std::function<std::string(std::size_t)> getter) { + bind_list_field_impl<Rml::String>(*surface_, list, field, std::move(getter)); +} +void SurfaceHandle::bind_list_int(std::string_view list, std::string_view field, + std::function<int(std::size_t)> getter) { + bind_list_field_impl<int>(*surface_, list, field, std::move(getter)); +} +void SurfaceHandle::bind_list_double(std::string_view list, std::string_view field, + std::function<double(std::size_t)> getter) { + bind_list_field_impl<double>(*surface_, list, field, std::move(getter)); +} +void SurfaceHandle::bind_list_bool(std::string_view list, std::string_view field, + std::function<bool(std::size_t)> getter) { + bind_list_field_impl<bool>(*surface_, list, field, std::move(getter)); +} + +void SurfaceHandle::bind_list_event(std::string_view /*list*/, std::string_view event, + std::function<void(std::size_t)> callback) { + // A row event is a normal data-event callback; the row index arrives as the + // first data-expression argument (author it as data-event-click="ev(it_index)"). + // The event name is model-global (RmlUi has no per-list event namespace), so + // `list` is documentary only — keep names unique per surface. + Surface& s = *surface_; + if (!s.ctor) { + return; + } + s.list_event_bindings.push_back({std::move(callback), s.who, s.owner}); + Surface::ListEventBinding* binding = &s.list_event_bindings.back(); + s.ctor.BindEventCallback( + std::string(event), + [binding](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) { + try { + if (binding->cb) { + std::size_t row = 0; + if (!args.empty()) { + row = static_cast<std::size_t>(args[0].Get<int>()); + } + binding->cb(row); + } + } catch (...) { + if (binding->owner->disable) { + binding->owner->disable(binding->who); + } + } + }); +} + void SurfaceHandle::on_touch_mode_changed(std::function<void(bool)> callback) { surface_->touch_mode_cb = std::move(callback); } diff --git a/packages/kernel/src/ui_substrate.hpp b/packages/kernel/src/ui_substrate.hpp index cdba997..33dfd4e 100644 --- a/packages/kernel/src/ui_substrate.hpp +++ b/packages/kernel/src/ui_substrate.hpp @@ -97,6 +97,17 @@ public: void bind_bool(std::string_view name, std::function<bool()> getter) override; void bind_string(std::string_view name, std::function<std::string()> getter) override; void bind_event(std::string_view name, std::function<void()> callback) override; + void bind_list(std::string_view name, std::function<std::size_t()> count) override; + void bind_list_string(std::string_view list, std::string_view field, + std::function<std::string(std::size_t)> getter) override; + void bind_list_int(std::string_view list, std::string_view field, + std::function<int(std::size_t)> getter) override; + void bind_list_double(std::string_view list, std::string_view field, + std::function<double(std::size_t)> getter) override; + void bind_list_bool(std::string_view list, std::string_view field, + std::function<bool(std::size_t)> getter) override; + void bind_list_event(std::string_view list, std::string_view event, + std::function<void(std::size_t)> callback) override; void on_touch_mode_changed(std::function<void(bool)> callback) override; void dirty(std::string_view name) override; void dirty() override; @@ -178,6 +189,12 @@ public: // / out of bounds. A position-aware probe (like orientation()) so the suite // can assert a preview's known source color landed at the expected spot. [[nodiscard]] auto surface_pixel(int x, int y) const -> std::uint32_t; + // Count elements with `tag` in the first surface's loaded document (0 if no + // surface / not loaded yet). Proves a data-for list rendered N rows. + [[nodiscard]] auto element_count(const char* tag) const -> int; + // Click the index-th `tag` element in the first surface's document (fires + // its data-event-click). False if no such element. Drives a row event. + auto click_element(const char* tag, int index) -> bool; struct Impl; diff --git a/packages/kernel/tests/test_kernel.cpp b/packages/kernel/tests/test_kernel.cpp index 20cd559..2c9b2fd 100644 --- a/packages/kernel/tests/test_kernel.cpp +++ b/packages/kernel/tests/test_kernel.cpp @@ -575,6 +575,136 @@ TEST_CASE("preview: known source color composites into an <img> (position-aware) } // ============================================================================ +// slice-10 / b2 LIST BINDINGS. The stage dock is one document iterating a +// VARIABLE list of slots with data-for; each row reads string fields and a +// per-row click event delivers the row index back to the extension. The suite +// proves through the PUBLIC Host::ui() path: (1) a list of N rows renders N row +// elements, (2) mutating the backing vector + dirty(list) changes the rendered +// row count on the next tick, and (3) clicking a row fires the per-row callback +// with the correct index. Headless+gles2 exercises the GL bridge. +// ============================================================================ + +namespace { + +// The dock document: a row <p> per slot, each carrying the slot's title text +// and a per-row click that calls restore(it_index). The row tag is <p> so the +// element-count probe counts exactly the rows (no other <p> in the body). +const char* kListRml = R"RML(<rml> +<head> +<style> +body { font-family: "Noto Sans"; background: #1e2230; color: #e8ecff; + width: 320px; height: 240px; } +p { display: block; width: 320px; height: 24px; } +</style> +</head> +<body data-model="ui"> +<p data-for="row : slots" data-event-click="restore(it_index)"><span>{{ row.title }} {{ row.fav }}</span></p> +</body> +</rml>)RML"; + +// A test extension owning a ui surface bound to a runtime-sized slot list. +class ListTestExtension : public unbox::kernel::Extension { +public: + auto manifest() const -> const Manifest& override { return manifest_; } + + void activate(Host& host) override { + titles = {"alpha", "beta", "gamma"}; + UiSurfaceSpec spec; + spec.rml_inline = kListRml; + spec.x = 0; + spec.y = 0; + spec.width = 320; + spec.height = 240; + spec.visible = true; + surface_ = host.ui().create_surface(spec); + if (surface_ != nullptr) { + surface_->bind_list("slots", [this] { return titles.size(); }); + surface_->bind_list_string("slots", "title", + [this](std::size_t r) { return titles.at(r); }); + // A second string field proves multiple per-row fields coexist. + surface_->bind_list_string("slots", "fav", + [](std::size_t r) { return "icon" + std::to_string(r); }); + surface_->bind_list_event("slots", "restore", + [this](std::size_t r) { + last_restored = static_cast<int>(r); + ++restore_calls; + }); + } + } + + void set_rows(std::vector<std::string> rows) { + titles = std::move(rows); + if (surface_ != nullptr) { + surface_->dirty("slots"); + } + } + + std::vector<std::string> titles; + int last_restored = -1; + int restore_calls = 0; + [[nodiscard]] auto has_surface() const -> bool { return surface_ != nullptr; } + +private: + Manifest manifest_{"list-test", Tier::standard, {}}; + std::unique_ptr<UiSurface> surface_; +}; + +} // namespace + +TEST_CASE("substrate: data-for list renders N rows, re-renders on dirty, routes row events") { + setenv("WLR_BACKENDS", "headless", 1); + setenv("WLR_RENDERER", "gles2", 1); + setenv("WLR_HEADLESS_OUTPUTS", "1", 1); + setenv("UNBOX_UI_SUBSTRATE_FORCE_SHM", "1", 1); + + auto server = unbox::kernel::Server::create({}); + auto* ext = new ListTestExtension(); + server->install(std::unique_ptr<unbox::kernel::Extension>(ext)); + server->activate_extensions(); + pump(*server, 60); // load the document + run the data-for loop + + if (!ext->has_surface() || server->ui_frame_count() == 0) { + unsetenv("UNBOX_UI_SUBSTRATE_FORCE_SHM"); // no GL path on this box: skip + return; + } + + // (1) Three rows in the backing vector => three rendered rows. Each + // rendered row carries a <span> (the data-for template <p> keeps no span + // child — its inner RML is extracted — so counting <span> counts exactly + // the rendered rows). + CHECK(server->ui_element_count("span") == 3); + + // (2) Grow then shrink the list + dirty(list): the rendered row count tracks + // count() on the next tick. + ext->set_rows({"one", "two", "three", "four", "five"}); + pump(*server, 5); + CHECK(server->ui_element_count("span") == 5); + + ext->set_rows({"solo"}); + pump(*server, 5); + CHECK(server->ui_element_count("span") == 1); + + // (3) Restore three rows and click the middle one: the per-row callback + // fires with the right index (data-event-click="restore(it_index)"). The + // generated rows occupy <p> indices 0..N-1 (the hidden template <p> is last). + ext->set_rows({"r0", "r1", "r2"}); + pump(*server, 5); + CHECK(server->ui_element_count("span") == 3); + const int before = ext->restore_calls; + REQUIRE(server->ui_click_element("p", 1)); + CHECK(ext->restore_calls == before + 1); + CHECK(ext->last_restored == 1); + + // Click row 0 and row 2 to prove the index is the real row, not a constant. + REQUIRE(server->ui_click_element("p", 0)); + CHECK(ext->last_restored == 0); + REQUIRE(server->ui_click_element("p", 2)); + CHECK(ext->last_restored == 2); + + unsetenv("UNBOX_UI_SUBSTRATE_FORCE_SHM"); +} + +// ============================================================================ // VT-switch escape hatch — PURE CORE (no wlroots): keysym -> VT number. The // glue (input.cpp) calls wlr_session_change_vt on a hit and consumes; this // helper decides the hit. Ctrl+Alt+Fn arrives as XF86Switch_VT_1..12. |
