diff options
| author | Adam Malczewski <[email protected]> | 2026-06-14 00:26:07 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-14 00:26:07 +0900 |
| commit | 151fbb589f99aa25f40c2ddb2562c992ef5d3adf (patch) | |
| tree | b6016ed63a86fa1f131ac393a9c1e504269ee483 | |
| parent | f7df36c8f656652e8245d781b35cb3252f17bad2 (diff) | |
| download | unbox-151fbb589f99aa25f40c2ddb2562c992ef5d3adf.tar.gz unbox-151fbb589f99aa25f40c2ddb2562c992ef5d3adf.zip | |
ext-keybindings + ext-stage-dock: config-driven dock-toggle-visible (Super+d)
- **ext-stage-dock**: exports a `Service` interface with `toggle_visible()`.
The extension inherits from it and registers via `provide_service` in
`activate()`. The method slides the dock in/out (using the existing RCSS
transition) regardless of slot count; showing an empty dock is valid.
- **ext-keybindings**: new `Action::dock_toggle_visible` action, mapped from
`"dock-toggle-visible"` in `unbox.toml [[keybind]]`, dispatched to the
stage-dock Service. Default binding: `Super+d`.
- **Manifest**: ext-keybindings now `depends_on {"xdg-shell", "stage-dock"}`.
- **Build**: subdir order swapped so ext-stage-dock builds before
ext-keybindings; `ext_stage_dock_dep` is a link-time dep of the
ext-keybindings library and transitively exposed via `ext_keybindings_dep`.
- **Tests**: glue tests install ext-stage-dock alongside ext-xdg-shell;
policy test expects 6 default bindings. All 10/10 green on build +
build-asan.
Configure in unbox.toml:
[[keybind]]
keys = "Super+d"
action = "dock-toggle-visible"
| -rw-r--r-- | meson.build | 2 | ||||
| -rw-r--r-- | packages/ext-keybindings/meson.build | 12 | ||||
| -rw-r--r-- | packages/ext-keybindings/src/config.hpp | 1 | ||||
| -rw-r--r-- | packages/ext-keybindings/src/extension.cpp | 22 | ||||
| -rw-r--r-- | packages/ext-keybindings/src/policy.hpp | 15 | ||||
| -rw-r--r-- | packages/ext-keybindings/tests/test_glue.cpp | 14 | ||||
| -rw-r--r-- | packages/ext-keybindings/tests/test_policy.cpp | 8 | ||||
| -rw-r--r-- | packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp | 22 | ||||
| -rw-r--r-- | packages/ext-stage-dock/src/extension.cpp | 34 |
9 files changed, 105 insertions, 25 deletions
diff --git a/meson.build b/meson.build index 6237ad7..17668f5 100644 --- a/meson.build +++ b/meson.build @@ -60,6 +60,6 @@ add_test_setup( subdir('packages/kernel') subdir('packages/ext-xdg-shell') subdir('packages/ext-layer-shell') -subdir('packages/ext-keybindings') subdir('packages/ext-stage-dock') +subdir('packages/ext-keybindings') subdir('packages/host-bin') diff --git a/packages/ext-keybindings/meson.build b/packages/ext-keybindings/meson.build index 4629fed..4573810 100644 --- a/packages/ext-keybindings/meson.build +++ b/packages/ext-keybindings/meson.build @@ -16,23 +16,25 @@ ext_keybindings_inc = include_directories('include') tomlplusplus_dep = dependency('tomlplusplus') # Glue library. Needs the kernel ABI, ext-xdg-shell's public contract (the glue -# includes its header to consume the Service + Toplevel), xkbcommon (the combo +# includes its header to consume the Service + Toplevel), ext-stage-dock's public +# contract (the dock Service for dock-toggle-visible), xkbcommon (the combo # parser resolves keysym names), and toml++ (the config loader). ext_keybindings_lib = static_library( 'unbox-ext-keybindings', 'src/extension.cpp', 'src/config.cpp', include_directories: ext_keybindings_inc, - dependencies: [kernel_dep, ext_xdg_shell_dep, xkbcommon_dep, tomlplusplus_dep], + dependencies: [kernel_dep, ext_xdg_shell_dep, ext_stage_dock_dep, + xkbcommon_dep, tomlplusplus_dep], ) # What host-bin links against: the factory. kernel_dep rides through for the -# Extension ABI the factory returns. ext-xdg-shell stays a build-time dep of OUR -# lib only (consumers of the factory do not need it). +# Extension ABI the factory returns. ext_stage_dock_dep rides so consumers +# (including host-bin and the glue test) can resolve the dock Service type. ext_keybindings_dep = declare_dependency( link_with: ext_keybindings_lib, include_directories: ext_keybindings_inc, - dependencies: [kernel_dep], + dependencies: [kernel_dep, ext_stage_dock_dep], ) # Tests, asymmetric: the pure decision cores doctest-hard (combo parser, toml diff --git a/packages/ext-keybindings/src/config.hpp b/packages/ext-keybindings/src/config.hpp index 967a997..072c357 100644 --- a/packages/ext-keybindings/src/config.hpp +++ b/packages/ext-keybindings/src/config.hpp @@ -34,6 +34,7 @@ struct LoadResult { // USER-APPROVED schema: // keys = "Mod+...+Key" | "Mod" (required, string) // action = "spawn" | "focus-next" | "focus-prev" | "close-active" | "quit" +// | "dock-toggle-visible" // command = "..." (required for action="spawn") // Each entry is validated independently: a malformed combo, unknown action, // missing/empty keys, missing command for spawn, or wrong value types skip that diff --git a/packages/ext-keybindings/src/extension.cpp b/packages/ext-keybindings/src/extension.cpp index 273fd47..a133ff5 100644 --- a/packages/ext-keybindings/src/extension.cpp +++ b/packages/ext-keybindings/src/extension.cpp @@ -4,6 +4,7 @@ #include "focus_ring.hpp" #include "policy.hpp" +#include <unbox/ext-stage-dock/ext_stage_dock.hpp> #include <unbox/ext-xdg-shell/ext_xdg_shell.hpp> #include <unbox/kernel/host.hpp> #include <unbox/kernel/wlr.hpp> @@ -155,15 +156,22 @@ public: void activate(Host& host) override { host_ = &host; - // The ONLY fatal: a missing ext-xdg-shell Service (our focus ring + the - // window-targeting actions are meaningless without it; depends_on - // guarantees it activated first, so absence is a broken core session). + // The ONLY fatals: missing ext-xdg-shell Service (actions that target + // windows — focus, close — are meaningless without it), and missing + // ext-stage-dock Service (dock-toggle-visible is a no-op without docks). + // depends_on guarantees both activate first, so absence is a broken core. shell_ = host.service<ext_xdg_shell::Service>(); if (shell_ == nullptr) { throw std::runtime_error( "ext-keybindings: ext-xdg-shell Service unavailable (depends_on " "\"xdg-shell\" not satisfied)"); } + dock_ = host.service<ext_stage_dock::Service>(); + if (dock_ == nullptr) { + throw std::runtime_error( + "ext-keybindings: ext-stage-dock Service unavailable (depends_on " + "\"stage-dock\" not satisfied)"); + } // Input path: thread every key through the Matcher. Non-matching keys // pass through untouched; a matched chord is consumed, a tap fires its @@ -278,6 +286,9 @@ private: case policy::Action::quit: wl_display_terminate(host_->display()); return; + case policy::Action::dock_toggle_visible: + dock_->toggle_visible(); + return; } } @@ -308,7 +319,7 @@ private: const kernel::Manifest manifest_{ .id = "keybindings", .tier = kernel::Tier::core, - .depends_on = {"xdg-shell"}, + .depends_on = {"xdg-shell", "stage-dock"}, }; std::optional<std::string> config_path_; @@ -327,7 +338,8 @@ private: policy::FocusRing<Toplevel*> ring_; Host* host_ = nullptr; - ext_xdg_shell::Service* shell_ = nullptr; // borrow; fetched in activate() + ext_xdg_shell::Service* shell_ = nullptr; // borrow; fetched in activate() + ext_stage_dock::Service* dock_ = nullptr; // borrow; fetched in activate() // RAII subscriptions + the file watch — destruction unsubscribes / stops the // watch (listener-lifetime). Last members so they release FIRST at teardown, diff --git a/packages/ext-keybindings/src/policy.hpp b/packages/ext-keybindings/src/policy.hpp index fbb2b3e..c5f99d4 100644 --- a/packages/ext-keybindings/src/policy.hpp +++ b/packages/ext-keybindings/src/policy.hpp @@ -45,11 +45,12 @@ inline constexpr std::uint32_t keysym_super_r = 0xffec; // XKB_KEY_Super_R // ---- Action vocabulary ------------------------------------------------------ enum class Action { - spawn, // run `command` via `sh -c` - focus_next, // rotate focus forward across mapped windows (wrapping) - focus_prev, // rotate focus backward (wrapping) - close_active, // close the focused toplevel (no-op if none) - quit, // wl_display_terminate + spawn, // run `command` via `sh -c` + focus_next, // rotate focus forward across mapped windows (wrapping) + focus_prev, // rotate focus backward (wrapping) + close_active, // close the focused toplevel (no-op if none) + quit, // wl_display_terminate + dock_toggle_visible, // show/hide the stage dock sidebar }; // Map an action token (lowercased) to the enum; nullopt = unknown action. @@ -69,6 +70,9 @@ enum class Action { if (s == "quit") { return Action::quit; } + if (s == "dock-toggle-visible") { + return Action::dock_toggle_visible; + } return std::nullopt; } @@ -338,6 +342,7 @@ private: add("Alt+Shift+Tab", Action::focus_prev, {}); add("Alt+F1", Action::focus_next, {}); add("Ctrl+Alt+BackSpace", Action::quit, {}); + add("Super+d", Action::dock_toggle_visible, {}); return out; } diff --git a/packages/ext-keybindings/tests/test_glue.cpp b/packages/ext-keybindings/tests/test_glue.cpp index e5b6fb8..2141961 100644 --- a/packages/ext-keybindings/tests/test_glue.cpp +++ b/packages/ext-keybindings/tests/test_glue.cpp @@ -2,6 +2,7 @@ #include <doctest/doctest.h> #include <unbox/ext-keybindings/ext_keybindings.hpp> +#include <unbox/ext-stage-dock/ext_stage_dock.hpp> #include <unbox/ext-xdg-shell/ext_xdg_shell.hpp> #include <unbox/kernel/server.hpp> @@ -25,12 +26,13 @@ auto make_headless_server() -> std::unique_ptr<unbox::kernel::Server> { } // namespace -TEST_CASE("ext-keybindings installs and activates atop ext-xdg-shell") { +TEST_CASE("ext-keybindings installs and activates atop ext-xdg-shell + ext-stage-dock") { auto server = make_headless_server(); server->install(unbox::ext_xdg_shell::create()); + server->install(unbox::ext_stage_dock::create()); server->install(unbox::ext_keybindings::create()); - // Topological activation runs xdg-shell first (keybindings depends_on it), - // so keybindings finds the Service. A missing-Service throw would propagate. + // Topological activation runs xdg-shell first, then stage-dock, then + // keybindings (depends_on both). A missing-Service throw would propagate. server->activate_extensions(); CHECK(!server->socket_name().empty()); } @@ -38,18 +40,20 @@ TEST_CASE("ext-keybindings installs and activates atop ext-xdg-shell") { TEST_CASE("ext-keybindings dispatches and shuts down cleanly") { auto server = make_headless_server(); server->install(unbox::ext_xdg_shell::create()); + server->install(unbox::ext_stage_dock::create()); server->install(unbox::ext_keybindings::create()); server->activate_extensions(); for (int i = 0; i < 5; ++i) { CHECK(server->dispatch(10)); } - // Destruction tears down the key_filter link + the three xdg-shell event - // subscriptions in reverse declaration order with no leaked listeners. + // Destruction tears down the key_filter link + the xdg-shell event + // subscriptions + the stage-dock Service in reverse declaration order. } TEST_CASE("ext-keybindings degrades to defaults for a bad explicit config path") { auto server = make_headless_server(); server->install(unbox::ext_xdg_shell::create()); + server->install(unbox::ext_stage_dock::create()); // A non-existent --config path must NOT throw out of activate(); the // extension logs and uses compiled defaults. server->install(unbox::ext_keybindings::create( diff --git a/packages/ext-keybindings/tests/test_policy.cpp b/packages/ext-keybindings/tests/test_policy.cpp index 8b6b91b..d7fd32e 100644 --- a/packages/ext-keybindings/tests/test_policy.cpp +++ b/packages/ext-keybindings/tests/test_policy.cpp @@ -165,7 +165,7 @@ TEST_CASE("empty / keybind-less document yields zero bindings, not an error") { TEST_CASE("compiled defaults match the documented out-of-the-box set") { auto d = pol::default_bindings(); - REQUIRE(d.size() == 5); + REQUIRE(d.size() == 6); CHECK(d[0].combo.is_tap); CHECK(d[0].combo.modifiers == pol::mod_logo); CHECK(d[0].action == Action::spawn); @@ -178,6 +178,8 @@ TEST_CASE("compiled defaults match the documented out-of-the-box set") { CHECK(d[3].action == Action::focus_next); CHECK(d[4].combo == parse_combo("Ctrl+Alt+BackSpace").value()); CHECK(d[4].action == Action::quit); + CHECK(d[5].combo == parse_combo("Super+d").value()); + CHECK(d[5].action == Action::dock_toggle_visible); } // ============================================================================ @@ -236,11 +238,11 @@ TEST_CASE("reload: a valid-but-empty doc keeps the old table (never drops workin // mid-edit with the [[keybind]] block deleted, or every entry malformed) // must NOT swap — the user's live keys survive. std::vector<Binding> live = pol::default_bindings(); - REQUIRE(live.size() == 5); + REQUIRE(live.size() == 6); auto empty = cfg::reload_bindings(live, "title = \"unrelated\"\n"); CHECK_FALSE(empty.swapped); - REQUIRE(empty.bindings.size() == 5); + REQUIRE(empty.bindings.size() == 6); CHECK(empty.bindings == live); // kept // Same for a doc where every entry is individually skipped (all malformed). diff --git a/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp b/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp index f56ed8c..6cbdbdb 100644 --- a/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp +++ b/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp @@ -22,6 +22,28 @@ namespace unbox::ext_stage_dock { +// ---- Exported Service (cross-extension typed coupling) ----------------------- +// +// Other extensions (ext-keybindings) fetch this via Host::service<T>() to drive +// dock policy (hiding/showing the dock, minimizing windows, etc.). Registered +// by this extension in activate(). See <unbox/kernel/host.hpp> for the service +// registry contract — single-responder, type-keyed, no strings. +class Service { +public: + virtual ~Service() = default; + + // Toggle the dock's visibility: if the dock is currently shown (slid into + // the visible area), hide it with the slide-out transition; if hidden, show + // it with the slide-in. Works regardless of whether the dock has slots + // (minimized windows) — showing an empty dock is valid. The auto-reveal on + // minimize (refresh_slots) is independent: minimizing a window also shows + // the dock. + virtual void toggle_visible() = 0; + +protected: + Service() = default; +}; + // Construct the extension (ownership transfer to the caller; host-bin installs // it via Server::install at c2). Construction is cheap and side-effect free per // the Extension contract; ALL wiring — once it exists — happens in activate(). diff --git a/packages/ext-stage-dock/src/extension.cpp b/packages/ext-stage-dock/src/extension.cpp index 08e3800..685f6e4 100644 --- a/packages/ext-stage-dock/src/extension.cpp +++ b/packages/ext-stage-dock/src/extension.cpp @@ -96,7 +96,8 @@ struct Slot { // -2dp thumb overscan clipped by the rounded overflow; d1 slot-enter animation; // transform-origin 0% 0%). -class StageDockExtension final : public kernel::Extension, public TestProbe { +class StageDockExtension final : public kernel::Extension, public TestProbe, + public ext_stage_dock::Service { public: auto manifest() const -> const kernel::Manifest& override { return manifest_; } @@ -163,6 +164,11 @@ public: // gracefully (model still tracked, hide/show still works, no visual). create_dock_surface(); + // Register the Service so ext-keybindings (or any extension) can drive + // dock policy — toggle_visible, and future minimize/restore — through + // the typed cross-extension coupling (no strings, link-time safety). + host.provide_service<ext_stage_dock::Service>(this); + activated_ = true; } @@ -243,6 +249,32 @@ private: refresh_slots(); } + // ---- Service: toggle_visible ------------------------------------------- + void toggle_visible() override { do_toggle_visible(); } + + void do_toggle_visible() { + if (dock_surface_ == nullptr) { + return; + } + if (open_) { + // Close: slide out. Keep the surface compositing so the animation + // plays; on_dock_settled won't hide it (closing_ is false), so a + // subsequent toggle re-opens instantly without delay. + open_ = false; + closing_ = false; + dock_surface_->dirty("open"); + } else { + // Open: make the surface visible (may already be from slots), then + // slide in. If the dock was fully hidden (surface invisible), the + // set_visible(true) begins compositing; the dirty("open") + // transitions the body to translateX(0). + open_ = true; + closing_ = false; + dock_surface_->set_visible(true); + dock_surface_->dirty("open"); + } + } + // ---- helpers ------------------------------------------------------------ // The first mapped window that is neither `except` nor currently minimized |
