summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/kernel/include/unbox/kernel/server.hpp8
-rw-r--r--packages/kernel/include/unbox/kernel/ui.hpp34
-rw-r--r--packages/kernel/src/server.cpp4
-rw-r--r--packages/kernel/src/ui_substrate.cpp102
-rw-r--r--packages/kernel/src/ui_substrate.hpp10
-rw-r--r--packages/kernel/tests/test_kernel.cpp126
6 files changed, 282 insertions, 2 deletions
diff --git a/packages/kernel/include/unbox/kernel/server.hpp b/packages/kernel/include/unbox/kernel/server.hpp
index 25c2aef..9aeb143 100644
--- a/packages/kernel/include/unbox/kernel/server.hpp
+++ b/packages/kernel/include/unbox/kernel/server.hpp
@@ -129,6 +129,14 @@ public:
// Test instrumentation; single-thread only.
auto ui_click_element(const char* tag, int index) -> bool;
+ // Synthesize a real RmlUi drag on the `index`-th element with tag `tag` in
+ // the first ui surface (press at its centre, move past RmlUi's drag
+ // threshold by (dx,dy), release), so a UiSurface::bind_drag callback receives
+ // start/move/end with surface-local coordinates — the same path a real
+ // captured pointer/touch drag takes, no input device. False if no such
+ // element / no GL document. Test instrumentation; single-thread only.
+ auto ui_drag_element(const char* tag, int index, double dx, double dy) -> bool;
+
// Synchronously reload the first ui surface's document from its rml_path file
// — the same reload the dev hot-reload (UNBOX_DEV) inotify watcher drives,
// exposed so tests trigger it deterministically without racing real
diff --git a/packages/kernel/include/unbox/kernel/ui.hpp b/packages/kernel/include/unbox/kernel/ui.hpp
index e272b3b..0ab48b3 100644
--- a/packages/kernel/include/unbox/kernel/ui.hpp
+++ b/packages/kernel/include/unbox/kernel/ui.hpp
@@ -102,6 +102,40 @@ public:
// extension (its surfaces + subscriptions dropped) — never the session.
virtual void bind_event(std::string_view name, std::function<void()> callback) = 0;
+ // Drag phase for bind_drag (a single captured pointer/touch drag stream).
+ enum class DragPhase { start, move, end };
+
+ // Bind a named RML drag interaction to a C++ callback. The document opts an
+ // element into dragging with the RCSS `drag` property and authors the event on
+ // it (e.g. data-event-dragstart / data-event-drag / data-event-dragend all
+ // naming <name>); the substrate routes RMLUi's Dragstart/Drag/Dragend for that
+ // element to ONE callback, tagged with the phase. `x`/`y` are the pointer
+ // position in THIS surface's LOCAL document coordinates (px, origin = surface
+ // top-left), so an extension can map travel to a fraction directly. Invoked on
+ // the event-loop thread; a throwing callback is caught at the substrate boundary
+ // and disables YOUR extension only — never the session. Call before the first
+ // frame (same rule as bind_event); re-binding the same name replaces it.
+ // A tap (press-release with no drag past RmlUi's threshold) does NOT fire this —
+ // it still fires data-event-click — so tap-to-restore and drag-to-close coexist.
+ //
+ // RML AUTHORING SHAPE (the substrate hooks NONE of these implicitly — author
+ // all three you want; binding one name to several phases is the whole point):
+ //
+ // <... style="drag: drag;"
+ // data-event-dragstart="<name>"
+ // data-event-drag="<name>"
+ // data-event-dragend="<name>">
+ //
+ // The RCSS `drag: drag;` on the element is REQUIRED — without it RMLUi emits no
+ // drag events at all (the captured pointer stream stays a plain click/tap). All
+ // three data-event-drag{start,,end} attributes name the SAME <name>; the
+ // substrate delivers each to your one callback with phase start/move/end
+ // respectively. Omit `data-event-dragstart`/`-dragend` if you only need the
+ // phase(s) you author — but the dock close needs all three (start = arm,
+ // move = live slide percent, end = snap), so author all three.
+ virtual void bind_drag(std::string_view name,
+ std::function<void(DragPhase phase, double x, double y)> callback) = 0;
+
// React to a touch-mode flip on THIS surface. `callback(touch)` is invoked
// (event-loop thread) when the substrate's touch-mode changes — touch ==
// true for finger mode. The substrate itself does NOTHING visual on a flip;
diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp
index cbd23c3..66f2cc3 100644
--- a/packages/kernel/src/server.cpp
+++ b/packages/kernel/src/server.cpp
@@ -105,6 +105,10 @@ auto Server::ui_click_element(const char* tag, int index) -> bool {
return impl_->substrate != nullptr && impl_->substrate->click_element(tag, index);
}
+auto Server::ui_drag_element(const char* tag, int index, double dx, double dy) -> bool {
+ return impl_->substrate != nullptr && impl_->substrate->drag_element(tag, index, dx, dy);
+}
+
auto Server::ui_reload_surface() -> bool {
return impl_->substrate != nullptr && impl_->substrate->reload_first_surface();
}
diff --git a/packages/kernel/src/ui_substrate.cpp b/packages/kernel/src/ui_substrate.cpp
index 1f289bd..4c9efe8 100644
--- a/packages/kernel/src/ui_substrate.cpp
+++ b/packages/kernel/src/ui_substrate.cpp
@@ -9,7 +9,9 @@
#include <RmlUi/Core/DataVariable.h>
#include <RmlUi/Core/Element.h>
#include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/Event.h>
#include <RmlUi/Core/Factory.h>
+#include <RmlUi/Core/ID.h>
#include <RmlUi/Core/SystemInterface.h>
#include <RmlUi/Core/Variant.h>
@@ -403,6 +405,18 @@ struct Surface {
Substrate::Impl* owner;
};
std::list<EventBinding> event_bindings;
+ // Drag bindings: one callback fed by RMLUi's Dragstart/Drag/Dragend for the
+ // element(s) authoring data-event-drag{start,,end}=<name>. The phase is read
+ // off the live Rml::Event id; x/y are the event's mouse_x/mouse_y which the
+ // substrate already feeds the context in surface-LOCAL coords (ctx_motion
+ // subtracts s.x/s.y), so they ARE surface-local px (origin top-left). Same
+ // error-isolation boundary as EventBinding.
+ struct DragBinding {
+ std::function<void(UiSurface::DragPhase, double, double)> cb;
+ ExtensionId who;
+ Substrate::Impl* owner;
+ };
+ std::list<DragBinding> drag_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
@@ -857,8 +871,10 @@ void Substrate::Impl::unwatch_surface(Surface* s) {
bool Substrate::Impl::reload_surface(Surface& s) {
// Only file-backed, already-loaded surfaces reload. The data model + the
- // extension's registered bind_*/bind_list*/bind_event getters are CONTEXT-
- // and substrate-owned, so they survive — we never touch s.ctor/s.*_bindings.
+ // extension's registered bind_*/bind_list*/bind_event/bind_drag getters are
+ // CONTEXT- and substrate-owned, so they survive — we never touch
+ // s.ctor/s.*_bindings (the reloaded document re-binds data-event-drag* to
+ // the still-present model callback by name).
if (s.context == nullptr || s.resolved_path.empty() || !s.doc_loaded) {
return false;
}
@@ -1762,6 +1778,36 @@ auto Substrate::click_element(const char* tag, int index) -> bool {
return false;
}
+auto Substrate::drag_element(const char* tag, int index, double dx, double dy) -> bool {
+ for (Surface& s : impl_->surfaces) {
+ if (s.document == nullptr || s.context == nullptr) {
+ continue;
+ }
+ Rml::ElementList elements;
+ s.document->GetElementsByTagName(elements, Rml::String(tag));
+ if (index < 0 || index >= static_cast<int>(elements.size())) {
+ return false;
+ }
+ Rml::Element* el = elements[static_cast<std::size_t>(index)];
+ // The element's content-box centre in context (= surface-local) coords:
+ // the same space ctx_motion feeds and the same space mouse_x/mouse_y are
+ // reported in, so the delivered drag coords should match these moves.
+ const Rml::Vector2f origin = el->GetAbsoluteOffset(Rml::BoxArea::Content);
+ const int cx = static_cast<int>(origin.x + el->GetClientWidth() / 2.0F);
+ const int cy = static_cast<int>(origin.y + el->GetClientHeight() / 2.0F);
+ // Press at the centre, then move PAST RmlUi's drag threshold so it emits
+ // Dragstart + Drag; a second move proves move tracks travel; release ends.
+ s.context->ProcessMouseMove(cx, cy, 0);
+ s.context->ProcessMouseButtonDown(0, 0);
+ s.context->ProcessMouseMove(cx + static_cast<int>(dx), cy + static_cast<int>(dy), 0);
+ s.context->ProcessMouseMove(cx + static_cast<int>(dx) * 2,
+ cy + static_cast<int>(dy) * 2, 0);
+ s.context->ProcessMouseButtonUp(0, 0);
+ return true;
+ }
+ return false;
+}
+
auto Substrate::reload_first_surface() -> bool {
for (Surface& s : impl_->surfaces) {
return impl_->reload_surface(s);
@@ -1929,6 +1975,58 @@ void SurfaceHandle::bind_event(std::string_view name, std::function<void()> call
}
namespace {
+// Map a live RMLUi drag event to the public DragPhase. Pure: only the three
+// drag ids are routed here (the binding hooks no other event), so an unknown id
+// is treated as a move (the safe middle phase) rather than dropped. Returns
+// false for an id we should ignore entirely (none currently).
+auto drag_phase_for(Rml::EventId id, UiSurface::DragPhase& out) -> bool {
+ switch (id) {
+ case Rml::EventId::Dragstart: out = UiSurface::DragPhase::start; return true;
+ case Rml::EventId::Dragend: out = UiSurface::DragPhase::end; return true;
+ case Rml::EventId::Drag: out = UiSurface::DragPhase::move; return true;
+ default: out = UiSurface::DragPhase::move; return true;
+ }
+}
+} // namespace
+
+void SurfaceHandle::bind_drag(std::string_view name,
+ std::function<void(UiSurface::DragPhase, double, double)> callback) {
+ Surface& s = *surface_;
+ if (!s.ctor) {
+ return;
+ }
+ s.drag_bindings.push_back({std::move(callback), s.who, s.owner});
+ Surface::DragBinding* binding = &s.drag_bindings.back();
+ // One model callback name carries all three phases; the document authors
+ // data-event-dragstart / data-event-drag / data-event-dragend all naming
+ // <name> on a drag-enabled element (RCSS `drag: drag;`). The phase is read
+ // off the live event id; mouse_x/mouse_y are already surface-local px
+ // (ctx_motion feeds the context coords relative to the surface origin), so
+ // they pass straight through as x/y. Same error-isolation boundary as
+ // bind_event (a throw disables the owning extension only). Survives dev
+ // hot-reload like every other binding: registered once on the open ctor,
+ // re-applied by the substrate against the reloaded document.
+ s.ctor.BindEventCallback(
+ std::string(name),
+ [binding](Rml::DataModelHandle, Rml::Event& ev, const Rml::VariantList&) {
+ try {
+ if (!binding->cb) {
+ return;
+ }
+ UiSurface::DragPhase phase = UiSurface::DragPhase::move;
+ drag_phase_for(ev.GetId(), phase);
+ const double x = static_cast<double>(ev.GetParameter<float>("mouse_x", 0.0F));
+ const double y = static_cast<double>(ev.GetParameter<float>("mouse_y", 0.0F));
+ binding->cb(phase, x, y);
+ } catch (...) {
+ if (binding->owner->disable) {
+ binding->owner->disable(binding->who);
+ }
+ }
+ });
+}
+
+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) {
diff --git a/packages/kernel/src/ui_substrate.hpp b/packages/kernel/src/ui_substrate.hpp
index 32072b4..d08c7de 100644
--- a/packages/kernel/src/ui_substrate.hpp
+++ b/packages/kernel/src/ui_substrate.hpp
@@ -102,6 +102,8 @@ 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_drag(std::string_view name,
+ std::function<void(DragPhase, double, double)> 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;
@@ -212,6 +214,14 @@ public:
// 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;
+ // Test seam: synthesize a real RmlUi drag on the index-th `tag` element of
+ // the first surface (press at the element's content centre, move PAST RmlUi's
+ // drag threshold by (dx,dy), then release), so a bind_drag callback receives
+ // start/move/end with surface-LOCAL coords — the same path a real captured
+ // pointer/touch drag takes, without an input device. False if no such
+ // element / no document. GL-path only (no-op skip when unavailable). Test
+ // instrumentation; single-thread only.
+ auto drag_element(const char* tag, int index, double dx, double dy) -> bool;
// Test seam: synchronously reload the first surface's document from its file
// (the same reload the dev inotify watcher drives), so tests trigger reload
// deterministically without racing real filesystem events. Returns true if a
diff --git a/packages/kernel/tests/test_kernel.cpp b/packages/kernel/tests/test_kernel.cpp
index 49f0ad5..ef50b9d 100644
--- a/packages/kernel/tests/test_kernel.cpp
+++ b/packages/kernel/tests/test_kernel.cpp
@@ -808,6 +808,132 @@ TEST_CASE("substrate: data-for list renders N rows, re-renders on dirty, routes
}
// ============================================================================
+// DRAG-EVENT BINDING (coordinate-carrying). A ui surface element opts into
+// dragging with RCSS `drag: drag;` and authors data-event-dragstart / -drag /
+// -dragend all naming ONE callback bound via UiSurface::bind_drag. The
+// substrate routes RMLUi's Dragstart/Drag/Dragend to that callback tagged with
+// DragPhase {start,move,end}, carrying the pointer position in SURFACE-LOCAL px
+// (origin top-left). The full path needs RmlUi to GENERATE drag events from a
+// real pointer-down→move-past-threshold→up sequence on the GL context, so it is
+// GL-path only (pixman has a null substrate, headless+gles2 exercises it). The
+// Server::ui_drag_element seam drives exactly that sequence (no input device).
+// ============================================================================
+
+namespace {
+
+// One full-bleed, drag-enabled box. `drag: drag;` is what makes RmlUi emit the
+// drag events; without it the same gesture is just a click. The box fills the
+// surface so the drag seam's centre-press always lands on it.
+const char* kDragRml = R"RML(<rml>
+<head>
+<style>
+body { background-color: #1e2230; width: 200px; height: 200px; margin: 0px; }
+#grip { display: block; position: absolute; left: 0px; top: 0px;
+ width: 200px; height: 200px; background-color: #3a4670; drag: drag; }
+</style>
+</head>
+<body data-model="ui">
+<div id="grip"
+ data-event-dragstart="slide"
+ data-event-drag="slide"
+ data-event-dragend="slide"></div>
+</body>
+</rml>)RML";
+
+// Records every drag phase + coordinate the callback receives.
+class DragTestExtension : public unbox::kernel::Extension {
+public:
+ auto manifest() const -> const Manifest& override { return manifest_; }
+
+ void activate(Host& host) override {
+ UiSurfaceSpec spec;
+ spec.rml_inline = kDragRml;
+ spec.x = 40; // a non-zero surface origin: proves coords are surface-LOCAL
+ spec.y = 30; // (NOT layout-space), i.e. the substrate subtracted s.x/s.y.
+ spec.width = 200;
+ spec.height = 200;
+ spec.layer = unbox::kernel::SceneLayer::overlay;
+ spec.visible = true;
+ surface_ = host.ui().create_surface(spec);
+ if (surface_ != nullptr) {
+ surface_->bind_drag("slide",
+ [this](UiSurface::DragPhase phase, double x, double y) {
+ phases.push_back(phase);
+ last_x = x;
+ last_y = y;
+ if (phase == UiSurface::DragPhase::start) {
+ start_x = x;
+ start_y = y;
+ }
+ });
+ }
+ }
+
+ std::vector<UiSurface::DragPhase> phases;
+ double start_x = -1.0;
+ double start_y = -1.0;
+ double last_x = -1.0;
+ double last_y = -1.0;
+ [[nodiscard]] auto has_surface() const -> bool { return surface_ != nullptr; }
+
+private:
+ Manifest manifest_{"drag-test", Tier::standard, {}};
+ std::unique_ptr<UiSurface> surface_;
+};
+
+} // namespace
+
+TEST_CASE("substrate: bind_drag routes Dragstart/Drag/Dragend with surface-local coords") {
+ 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 DragTestExtension();
+ server->install(std::unique_ptr<unbox::kernel::Extension>(ext));
+ server->activate_extensions();
+ pump(*server, 60); // load + lay out the document so the grip has geometry
+
+ if (!ext->has_surface() || server->ui_frame_count() == 0) {
+ unsetenv("UNBOX_UI_SUBSTRATE_FORCE_SHM"); // no GL path on this box: skip
+ return;
+ }
+
+ using DP = UiSurface::DragPhase;
+
+ // Drag the grip by (+30,+20) from its centre. The seam presses at the box
+ // centre (100,100 in surface-local px), then moves past RmlUi's threshold.
+ REQUIRE(server->ui_drag_element("div", 0, 30.0, 20.0));
+
+ // (1) The callback saw a start, then move(s), then an end — in that order.
+ REQUIRE(ext->phases.size() >= 3);
+ CHECK(ext->phases.front() == DP::start);
+ CHECK(ext->phases.back() == DP::end);
+ bool saw_move = false;
+ for (std::size_t i = 1; i + 1 < ext->phases.size(); ++i) {
+ if (ext->phases[i] == DP::move) {
+ saw_move = true;
+ }
+ }
+ CHECK(saw_move);
+
+ // (2) Coordinates are SURFACE-LOCAL px (origin = surface top-left), NOT
+ // layout-space: the surface sits at layout (40,30) but the centre-press
+ // reports ~ (100,100), the grip's local centre — proof the substrate mapped
+ // mouse_x/mouse_y into the surface's own coordinate system.
+ CHECK(ext->start_x == doctest::Approx(100.0).epsilon(0.05));
+ CHECK(ext->start_y == doctest::Approx(100.0).epsilon(0.05));
+
+ // (3) The final (dragend) coordinate followed the travel: centre + 2*delta
+ // (the seam issues two moves of (dx,dy) and 2*(dx,dy)). So last ~ (160,140).
+ CHECK(ext->last_x == doctest::Approx(160.0).epsilon(0.05));
+ CHECK(ext->last_y == doctest::Approx(140.0).epsilon(0.05));
+
+ unsetenv("UNBOX_UI_SUBSTRATE_FORCE_SHM");
+}
+
+// ============================================================================
// slice-10 / ui-surface ALPHA (transparency). A ui surface composites with
// per-pixel alpha: a pixel the document does NOT paint is transparent (the
// scene below shows through), while a painted opaque box stays solid. The