From 14e59daae4137c88157545397644853f22e4eab6 Mon Sep 17 00:00:00 2001 From: iamcheyan Date: Sat, 13 Jun 2026 11:34:29 +0900 Subject: [PATCH] feat(wlr/taskbar): expose active toplevel state on the bar window Add opt-in bar-css-states option to wlr/taskbar module. When enabled, the module tracks the active workspace and updates CSS classes on window#waybar to reflect the current window state: - toplevel-active: when a window is focused - toplevel-maximized: when any non-minimized window on active workspace is maximized - toplevel-minimized: when the active window is minimized - toplevel-fullscreen: when any non-minimized window on active workspace is fullscreen This feature requires compositor support for ext-workspace-v1 protocol to correctly track workspace membership. Without it, classes fall back to the active application state only. Also includes active-only option implementation. Changes: - Add ext-workspace-v1 protocol support for workspace tracking - Implement WorkspaceState tracking in Taskbar class - Add workspace management lifecycle (create/remove/done) - Add bar CSS class update logic with aggregation - Add visible() getter to Task class - Implement active-only task visibility control - Update man page with documentation and CSS examples Signed-off-by: iamcheyan --- include/modules/wlr/taskbar.hpp | 27 ++++ man/waybar-wlr-taskbar.5.scd | 45 ++++++ src/modules/wlr/taskbar.cpp | 245 +++++++++++++++++++++++++++++++- 3 files changed, 316 insertions(+), 1 deletion(-) diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 413eae85f7..3ad41c6ea8 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -18,6 +18,7 @@ #include "AModule.hpp" #include "bar.hpp" #include "client.hpp" +#include "ext-workspace-v1-client-protocol.h" #include "giomm/desktopappinfo.h" #include "util/icon_loader.hpp" #include "util/json.hpp" @@ -80,6 +81,7 @@ class Task { std::string title_; std::string app_id_; uint32_t state_ = 0; + struct ext_workspace_handle_v1* workspace_ = nullptr; int32_t drag_start_x; int32_t drag_start_y; @@ -102,6 +104,9 @@ class Task { bool minimized() const { return state_ & MINIMIZED; } bool active() const { return state_ & ACTIVE; } bool fullscreen() const { return state_ & FULLSCREEN; } + bool visible() const { return button_visible_; } + struct ext_workspace_handle_v1* workspace() const { return workspace_; } + void set_workspace(struct ext_workspace_handle_v1* workspace) { workspace_ = workspace; } public: /* Callbacks for the wlr protocol */ @@ -142,6 +147,12 @@ using TaskPtr = std::unique_ptr; class Taskbar : public waybar::AModule { public: + struct WorkspaceState { + Taskbar* taskbar; + struct ext_workspace_handle_v1* handle; + uint32_t state = 0; + }; + Taskbar(const std::string&, const waybar::Bar&, const Json::Value&); ~Taskbar(); void update(); @@ -156,22 +167,35 @@ class Taskbar : public waybar::AModule { std::map app_ids_replace_map_; struct zwlr_foreign_toplevel_manager_v1* manager_; + struct ext_workspace_manager_v1* workspace_manager_; struct wl_seat* seat_; + std::vector workspace_groups_; + std::vector> workspaces_; + struct ext_workspace_handle_v1* current_workspace_ = nullptr; public: /* Callbacks for global registration */ void register_manager(struct wl_registry*, uint32_t name, uint32_t version); + void register_workspace_manager(struct wl_registry*, uint32_t name, uint32_t version); void register_seat(struct wl_registry*, uint32_t name, uint32_t version); /* Callbacks for the wlr protocol */ void handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1*); void handle_finished(); + void handle_workspace_group_create(struct ext_workspace_group_handle_v1*); + void handle_workspace_group_removed(struct ext_workspace_group_handle_v1*); + void handle_workspace_create(struct ext_workspace_handle_v1*); + void handle_workspace_done(); + void handle_workspace_finished(); + void handle_workspace_removed(struct ext_workspace_handle_v1*); public: void add_button(Gtk::Button&); void move_button(Gtk::Button&, int); void remove_button(Gtk::Button&); void remove_task(uint32_t); + void assign_current_workspace(Task&); + void update_bar_css_classes(); bool show_output(struct wl_output*) const; bool all_outputs() const; @@ -179,6 +203,9 @@ class Taskbar : public waybar::AModule { const IconLoader& icon_loader() const; const std::unordered_set& ignore_list() const; const std::map& app_ids_replace_map() const; + + private: + void set_bar_css_class(const std::string&, bool); }; } /* namespace waybar::modules::wlr */ diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index af1ba97f76..77cf91e5c0 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -18,6 +18,13 @@ Addressed by *wlr/taskbar* default: false ++ If set to false applications on the waybar's current output will be shown. Otherwise, all applications are shown. +*bar-css-states*: ++ + typeof: bool ++ + default: false ++ + If set to true, application state is exposed as CSS classes on the Waybar + window. Maximized and fullscreen state is aggregated across applications + known to belong to the active workspace. See *Bar state style* below. + *format*: ++ typeof: string ++ default: {icon} ++ @@ -52,6 +59,12 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, always reorder the tasks in the taskbar so that the currently active one is first. Otherwise don't reorder. +*active-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only the currently active application button is shown. + Other applications remain tracked and reappear when activated. + *sort-by-app-id*: ++ typeof: bool ++ default: false ++ @@ -156,3 +169,35 @@ Invalid expressions (e.g., mismatched parentheses) are skipped. - *#taskbar button.minimized* - *#taskbar button.active* - *#taskbar button.fullscreen* + +# Bar state style + +When *bar-css-states* is enabled, the following classes are added to +*window#waybar*: + +- *window#waybar.toplevel-active* +- *window#waybar.toplevel-maximized* +- *window#waybar.toplevel-minimized* +- *window#waybar.toplevel-fullscreen* + +The active, minimized classes describe the active application. The maximized +and fullscreen classes are set if any non-minimized application known to belong +to the active workspace has that state. + +Workspace membership is learned when an application is activated and requires +the compositor to support *ext-workspace-v1*. Before an application has been +activated during the current Waybar session, its workspace may be unknown. On +compositors without *ext-workspace-v1*, these classes fall back to the active +application's state. + +For example: + +``` +window#waybar { + background-color: rgba(0, 0, 0, 0.5); +} + +window#waybar.toplevel-maximized { + background-color: rgba(0, 0, 0, 1); +} +``` diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index bde9091060..ab76d89e52 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -293,9 +293,12 @@ void Task::handle_output_enter(struct wl_output* output) { button.signal_size_allocate().connect_notify( sigc::mem_fun(this, &Task::on_button_size_allocated)); tbar_->add_button(button); - button.show(); + if (!config_["active-only"].asBool() || active()) { + button.show(); + } button_visible_ = true; spdlog::debug("{} now visible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); } } @@ -308,6 +311,7 @@ void Task::handle_output_leave(struct wl_output* output) { button.hide(); button_visible_ = false; spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); } } @@ -350,6 +354,20 @@ void Task::handle_done() { button.get_style_context()->remove_class("fullscreen"); } + if (button_visible_ && config_["active-only"].asBool()) { + if (active()) { + button.show(); + } else { + button.hide(); + } + } + + if (active()) { + tbar_->assign_current_workspace(*this); + } + + tbar_->update_bar_css_classes(); + if (config_["active-first"].isBool() && config_["active-first"].asBool() && active()) tbar_->move_button(button, 0); @@ -551,6 +569,8 @@ static void handle_global(void* data, struct wl_registry* registry, uint32_t nam const char* interface, uint32_t version) { if (std::strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { static_cast(data)->register_manager(registry, name, version); + } else if (std::strcmp(interface, ext_workspace_manager_v1_interface.name) == 0) { + static_cast(data)->register_workspace_manager(registry, name, version); } else if (std::strcmp(interface, wl_seat_interface.name) == 0) { static_cast(data)->register_seat(registry, name, version); } @@ -568,6 +588,7 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu bar_(bar), box_{bar.orientation, 0}, manager_{nullptr}, + workspace_manager_{nullptr}, seat_{nullptr} { box_.set_name("taskbar"); if (!id.empty()) { @@ -623,6 +644,27 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu } Taskbar::~Taskbar() { + for (auto& workspace : workspaces_) { + ext_workspace_handle_v1_destroy(workspace->handle); + } + workspaces_.clear(); + for (auto* group : workspace_groups_) { + ext_workspace_group_handle_v1_destroy(group); + } + workspace_groups_.clear(); + + if (workspace_manager_) { + struct wl_display* display = Client::inst()->wl_display; + ext_workspace_manager_v1_stop(workspace_manager_); + wl_display_roundtrip(display); + + if (workspace_manager_) { + spdlog::warn("Workspace manager destroyed before .finished event"); + ext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; + } + } + if (manager_) { struct wl_display* display = Client::inst()->wl_display; /* @@ -639,6 +681,13 @@ Taskbar::~Taskbar() { manager_ = nullptr; } } + + if (config_["bar-css-states"].asBool()) { + set_bar_css_class("toplevel-active", false); + set_bar_css_class("toplevel-maximized", false); + set_bar_css_class("toplevel-minimized", false); + set_bar_css_class("toplevel-fullscreen", false); + } } void Taskbar::update() { @@ -674,6 +723,80 @@ static const struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_i .finished = tm_handle_finished, }; +static void workspace_handle_id(void*, struct ext_workspace_handle_v1*, const char*) {} +static void workspace_handle_name(void*, struct ext_workspace_handle_v1*, const char*) {} +static void workspace_handle_coordinates(void*, struct ext_workspace_handle_v1*, struct wl_array*) { +} + +static void workspace_handle_state(void* data, struct ext_workspace_handle_v1*, uint32_t state) { + static_cast(data)->state = state; +} + +static void workspace_handle_capabilities(void*, struct ext_workspace_handle_v1*, uint32_t) {} + +static void workspace_handle_removed(void* data, struct ext_workspace_handle_v1* handle) { + static_cast(data)->taskbar->handle_workspace_removed(handle); +} + +static const struct ext_workspace_handle_v1_listener workspace_handle_impl = { + .id = workspace_handle_id, + .name = workspace_handle_name, + .coordinates = workspace_handle_coordinates, + .state = workspace_handle_state, + .capabilities = workspace_handle_capabilities, + .removed = workspace_handle_removed, +}; + +static void workspace_group_handle_capabilities(void*, struct ext_workspace_group_handle_v1*, + uint32_t) {} +static void workspace_group_handle_output_enter(void*, struct ext_workspace_group_handle_v1*, + struct wl_output*) {} +static void workspace_group_handle_output_leave(void*, struct ext_workspace_group_handle_v1*, + struct wl_output*) {} +static void workspace_group_handle_workspace_enter(void*, struct ext_workspace_group_handle_v1*, + struct ext_workspace_handle_v1*) {} +static void workspace_group_handle_workspace_leave(void*, struct ext_workspace_group_handle_v1*, + struct ext_workspace_handle_v1*) {} + +static void workspace_group_handle_removed(void* data, + struct ext_workspace_group_handle_v1* group) { + static_cast(data)->handle_workspace_group_removed(group); +} + +static const struct ext_workspace_group_handle_v1_listener workspace_group_impl = { + .capabilities = workspace_group_handle_capabilities, + .output_enter = workspace_group_handle_output_enter, + .output_leave = workspace_group_handle_output_leave, + .workspace_enter = workspace_group_handle_workspace_enter, + .workspace_leave = workspace_group_handle_workspace_leave, + .removed = workspace_group_handle_removed, +}; + +static void workspace_manager_handle_group(void* data, struct ext_workspace_manager_v1*, + struct ext_workspace_group_handle_v1* group) { + static_cast(data)->handle_workspace_group_create(group); +} + +static void workspace_manager_handle_workspace(void* data, struct ext_workspace_manager_v1*, + struct ext_workspace_handle_v1* workspace) { + static_cast(data)->handle_workspace_create(workspace); +} + +static void workspace_manager_handle_done(void* data, struct ext_workspace_manager_v1*) { + static_cast(data)->handle_workspace_done(); +} + +static void workspace_manager_handle_finished(void* data, struct ext_workspace_manager_v1*) { + static_cast(data)->handle_workspace_finished(); +} + +static const struct ext_workspace_manager_v1_listener workspace_manager_impl = { + .workspace_group = workspace_manager_handle_group, + .workspace = workspace_manager_handle_workspace, + .done = workspace_manager_handle_done, + .finished = workspace_manager_handle_finished, +}; + void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint32_t version) { if (manager_) { spdlog::warn("Register foreign toplevel manager again although already existing!"); @@ -698,6 +821,18 @@ void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint spdlog::debug("Failed to register manager"); } +void Taskbar::register_workspace_manager(struct wl_registry* registry, uint32_t name, + uint32_t version) { + if (workspace_manager_) { + return; + } + + version = std::min(version, ext_workspace_manager_v1_interface.version); + workspace_manager_ = static_cast( + wl_registry_bind(registry, name, &ext_workspace_manager_v1_interface, version)); + ext_workspace_manager_v1_add_listener(workspace_manager_, &workspace_manager_impl, this); +} + void Taskbar::register_seat(struct wl_registry* registry, uint32_t name, uint32_t version) { if (seat_) { spdlog::warn("Register seat again although already existing!"); @@ -717,6 +852,64 @@ void Taskbar::handle_finished() { manager_ = nullptr; } +void Taskbar::handle_workspace_group_create(struct ext_workspace_group_handle_v1* handle) { + ext_workspace_group_handle_v1_add_listener(handle, &workspace_group_impl, this); + workspace_groups_.push_back(handle); +} + +void Taskbar::handle_workspace_group_removed(struct ext_workspace_group_handle_v1* handle) { + const auto group = std::find(workspace_groups_.begin(), workspace_groups_.end(), handle); + if (group != workspace_groups_.end()) { + ext_workspace_group_handle_v1_destroy(*group); + workspace_groups_.erase(group); + } +} + +void Taskbar::handle_workspace_create(struct ext_workspace_handle_v1* handle) { + auto workspace = std::make_unique(WorkspaceState{this, handle}); + ext_workspace_handle_v1_add_listener(handle, &workspace_handle_impl, workspace.get()); + workspaces_.push_back(std::move(workspace)); +} + +void Taskbar::handle_workspace_done() { + const auto active_workspace = + std::find_if(workspaces_.begin(), workspaces_.end(), [](const auto& workspace) { + return workspace->state & EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE; + }); + current_workspace_ = + active_workspace == workspaces_.end() ? nullptr : (*active_workspace)->handle; + if (current_workspace_) { + const auto active_task = + std::find_if(tasks_.begin(), tasks_.end(), [](const auto& task) { return task->active(); }); + if (active_task != tasks_.end()) { + (*active_task)->set_workspace(current_workspace_); + } + } + update_bar_css_classes(); +} + +void Taskbar::handle_workspace_finished() { workspace_manager_ = nullptr; } + +void Taskbar::handle_workspace_removed(struct ext_workspace_handle_v1* handle) { + if (current_workspace_ == handle) { + current_workspace_ = nullptr; + } + for (auto& task : tasks_) { + if (task->workspace() == handle) { + task->set_workspace(nullptr); + } + } + + const auto workspace = + std::find_if(workspaces_.begin(), workspaces_.end(), + [handle](const auto& workspace) { return workspace->handle == handle; }); + if (workspace != workspaces_.end()) { + ext_workspace_handle_v1_destroy((*workspace)->handle); + workspaces_.erase(workspace); + } + update_bar_css_classes(); +} + void Taskbar::add_button(Gtk::Button& bt) { box_.pack_start(bt, false, false); box_.get_style_context()->remove_class("empty"); @@ -741,6 +934,56 @@ void Taskbar::remove_task(uint32_t id) { } tasks_.erase(it); + update_bar_css_classes(); +} + +void Taskbar::assign_current_workspace(Task& task) { + if (current_workspace_) { + task.set_workspace(current_workspace_); + } +} + +void Taskbar::update_bar_css_classes() { + if (!config_["bar-css-states"].asBool()) { + return; + } + + const auto active_task = std::find_if(tasks_.begin(), tasks_.end(), [](const TaskPtr& task) { + return task->visible() && task->active(); + }); + + const bool has_active_task = active_task != tasks_.end(); + const auto on_current_workspace = [this](const TaskPtr& task) { + if (!current_workspace_) { + return task->active(); + } + return task->workspace() == current_workspace_; + }; + const bool has_maximized_task = + std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) { + return task->visible() && !task->minimized() && on_current_workspace(task) && + task->maximized(); + }); + const bool has_fullscreen_task = + std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) { + return task->visible() && !task->minimized() && on_current_workspace(task) && + task->fullscreen(); + }); + set_bar_css_class("toplevel-active", has_active_task); + set_bar_css_class("toplevel-maximized", has_maximized_task); + set_bar_css_class("toplevel-minimized", has_active_task && (*active_task)->minimized()); + set_bar_css_class("toplevel-fullscreen", has_fullscreen_task); +} + +void Taskbar::set_bar_css_class(const std::string& class_name, bool enabled) { + const auto style = bar_.window.get_style_context(); + if (enabled && !style->has_class(class_name)) { + spdlog::trace("Adding bar class: {}", class_name); + style->add_class(class_name); + } else if (!enabled && style->has_class(class_name)) { + spdlog::trace("Removing bar class: {}", class_name); + style->remove_class(class_name); + } } bool Taskbar::show_output(struct wl_output* output) const {