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 <iamcheyan@users.noreply.github.com>
This commit is contained in:
iamcheyan
2026-06-13 11:34:29 +09:00
parent 05945748dc
commit 14e59daae4
3 changed files with 316 additions and 1 deletions
+27
View File
@@ -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<Task>;
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<std::string, std::string> app_ids_replace_map_;
struct zwlr_foreign_toplevel_manager_v1* manager_;
struct ext_workspace_manager_v1* workspace_manager_;
struct wl_seat* seat_;
std::vector<struct ext_workspace_group_handle_v1*> workspace_groups_;
std::vector<std::unique_ptr<WorkspaceState>> 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<std::string>& ignore_list() const;
const std::map<std::string, std::string>& app_ids_replace_map() const;
private:
void set_bar_css_class(const std::string&, bool);
};
} /* namespace waybar::modules::wlr */
+45
View File
@@ -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);
}
```
+243
View File
@@ -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);
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<Taskbar*>(data)->register_manager(registry, name, version);
} else if (std::strcmp(interface, ext_workspace_manager_v1_interface.name) == 0) {
static_cast<Taskbar*>(data)->register_workspace_manager(registry, name, version);
} else if (std::strcmp(interface, wl_seat_interface.name) == 0) {
static_cast<Taskbar*>(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<Taskbar::WorkspaceState*>(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<Taskbar::WorkspaceState*>(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<Taskbar*>(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<Taskbar*>(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<Taskbar*>(data)->handle_workspace_create(workspace);
}
static void workspace_manager_handle_done(void* data, struct ext_workspace_manager_v1*) {
static_cast<Taskbar*>(data)->handle_workspace_done();
}
static void workspace_manager_handle_finished(void* data, struct ext_workspace_manager_v1*) {
static_cast<Taskbar*>(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<uint32_t>(version, ext_workspace_manager_v1_interface.version);
workspace_manager_ = static_cast<struct ext_workspace_manager_v1*>(
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>(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 {