wm: add WindowManager module with ext-workspace support

This commit is contained in:
outfoxxed 2025-06-21 12:57:15 -07:00
parent 9e8eecf2b8
commit 6705e2da77
No known key found for this signature in database
GPG key ID: 4C88A185FB89301E
22 changed files with 1337 additions and 0 deletions

View file

@ -123,6 +123,8 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify)
add_subdirectory(shortcuts_inhibit)
list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor)
add_subdirectory(windowmanager)
# widgets for qmenu
target_link_libraries(quickshell-wayland PRIVATE
Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate

View file

@ -0,0 +1,19 @@
qt_add_library(quickshell-wayland-windowsystem STATIC
windowmanager.cpp
windowset.cpp
ext_workspace.cpp
)
add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp)
target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick)
wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace")
target_link_libraries(quickshell-wayland-windowsystem PRIVATE
Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
wlp-ext-workspace
)
qs_pch(quickshell-wayland-windowsystem SET large)
target_link_libraries(quickshell PRIVATE quickshell-wayland-windowsystem quickshell-wayland-windowsystem-init)

View file

@ -0,0 +1,176 @@
#include "ext_workspace.hpp"
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwayland-ext-workspace-v1.h>
#include <qwaylandclientextension.h>
#include <wayland-ext-workspace-v1-client-protocol.h>
#include <wayland-util.h>
#include "../../core/logcat.hpp"
namespace qs::wayland::workspace {
QS_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace", QtWarningMsg);
WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); }
WorkspaceManager* WorkspaceManager::instance() {
static auto* instance = new WorkspaceManager();
return instance;
}
void WorkspaceManager::ext_workspace_manager_v1_workspace_group(
::ext_workspace_group_handle_v1* handle
) {
auto* group = new WorkspaceGroup(handle);
qCDebug(logWorkspace) << "Created group" << group;
this->mGroups.insert(handle, group);
emit this->groupCreated(group);
}
void WorkspaceManager::ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) {
auto* workspace = new Workspace(handle);
qCDebug(logWorkspace) << "Created workspace" << workspace;
this->mWorkspaces.insert(handle, workspace);
emit this->workspaceCreated(workspace);
};
void WorkspaceManager::destroyWorkspace(Workspace* workspace) {
this->mWorkspaces.remove(workspace->object());
this->destroyedWorkspaces.append(workspace);
emit this->workspaceDestroyed(workspace);
}
void WorkspaceManager::destroyGroup(WorkspaceGroup* group) {
this->mGroups.remove(group->object());
this->destroyedGroups.append(group);
emit this->groupDestroyed(group);
}
void WorkspaceManager::ext_workspace_manager_v1_done() {
qCDebug(logWorkspace) << "Workspace changes done";
emit this->serverCommit();
for (auto* workspace: this->destroyedWorkspaces) delete workspace;
for (auto* group: this->destroyedGroups) delete group;
this->destroyedWorkspaces.clear();
this->destroyedGroups.clear();
}
void WorkspaceManager::ext_workspace_manager_v1_finished() {
qCWarning(logWorkspace) << "ext_workspace_manager_v1.finished() was received";
}
Workspace::~Workspace() {
if (this->isInitialized()) this->destroy();
}
void Workspace::ext_workspace_handle_v1_id(const QString& id) {
qCDebug(logWorkspace) << "Updated id for workspace" << this << "to" << id;
this->id = id;
}
void Workspace::ext_workspace_handle_v1_name(const QString& name) {
qCDebug(logWorkspace) << "Updated name for workspace" << this << "to" << name;
this->name = name;
}
void Workspace::ext_workspace_handle_v1_coordinates(wl_array* coordinates) {
this->coordinates.clear();
auto* data = static_cast<qint32*>(coordinates->data);
auto size = static_cast<qsizetype>(coordinates->size / sizeof(qint32));
for (auto i = 0; i != size; ++i) {
this->coordinates.append(data[i]); // NOLINT
}
qCDebug(logWorkspace) << "Updated coordinates for workspace" << this << "to" << this->coordinates;
}
void Workspace::ext_workspace_handle_v1_state(quint32 state) {
this->active = state & ext_workspace_handle_v1::state_active;
this->urgent = state & ext_workspace_handle_v1::state_urgent;
this->hidden = state & ext_workspace_handle_v1::state_hidden;
qCDebug(logWorkspace).nospace() << "Updated state for workspace " << this
<< " to [active: " << this->active << ", urgent: " << this->urgent
<< ", hidden: " << this->hidden << ']';
}
void Workspace::ext_workspace_handle_v1_capabilities(quint32 capabilities) {
this->canActivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_activate;
this->canDeactivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_deactivate;
this->canRemove = capabilities & ext_workspace_handle_v1::workspace_capabilities_remove;
this->canAssign = capabilities & ext_workspace_handle_v1::workspace_capabilities_assign;
qCDebug(logWorkspace).nospace() << "Updated capabilities for workspace " << this
<< " to [activate: " << this->canActivate
<< ", deactivate: " << this->canDeactivate
<< ", remove: " << this->canRemove
<< ", assign: " << this->canAssign << ']';
}
void Workspace::ext_workspace_handle_v1_removed() {
qCDebug(logWorkspace) << "Destroyed workspace" << this;
WorkspaceManager::instance()->destroyWorkspace(this);
this->destroy();
}
void Workspace::enterGroup(WorkspaceGroup* group) { this->group = group; }
void Workspace::leaveGroup(WorkspaceGroup* group) {
if (this->group == group) this->group = nullptr;
}
WorkspaceGroup::~WorkspaceGroup() {
if (this->isInitialized()) this->destroy();
}
void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(quint32 capabilities) {
this->canCreateWorkspace =
capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace;
qCDebug(logWorkspace).nospace() << "Updated capabilities for group " << this
<< " to [create_workspace: " << this->canCreateWorkspace << ']';
}
void WorkspaceGroup::ext_workspace_group_handle_v1_output_enter(::wl_output* output) {
qCDebug(logWorkspace) << "Output" << output << "added to group" << this;
this->screens.addOutput(output);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_output_leave(::wl_output* output) {
qCDebug(logWorkspace) << "Output" << output << "removed from group" << this;
this->screens.removeOutput(output);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter(
::ext_workspace_handle_v1* handle
) {
auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle);
qCDebug(logWorkspace) << "Workspace" << workspace << "added to group" << this;
if (workspace) workspace->enterGroup(this);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_leave(
::ext_workspace_handle_v1* handle
) {
auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle);
qCDebug(logWorkspace) << "Workspace" << workspace << "removed from group" << this;
if (workspace) workspace->leaveGroup(this);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_removed() {
qCDebug(logWorkspace) << "Destroyed group" << this;
WorkspaceManager::instance()->destroyGroup(this);
this->destroy();
}
} // namespace qs::wayland::workspace

View file

@ -0,0 +1,117 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qscreen.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwayland-ext-workspace-v1.h>
#include <qwaylandclientextension.h>
#include <wayland-ext-workspace-v1-client-protocol.h>
#include "../../core/logcat.hpp"
#include "../output_tracking.hpp"
namespace qs::wayland::workspace {
QS_DECLARE_LOGGING_CATEGORY(logWorkspace);
class WorkspaceGroup;
class Workspace;
class WorkspaceManager
: public QWaylandClientExtensionTemplate<WorkspaceManager>
, public QtWayland::ext_workspace_manager_v1 {
Q_OBJECT;
public:
static WorkspaceManager* instance();
[[nodiscard]] QList<Workspace*> workspaces() { return this->mWorkspaces.values(); }
signals:
void serverCommit();
void workspaceCreated(Workspace* workspace);
void workspaceDestroyed(Workspace* workspace);
void groupCreated(WorkspaceGroup* group);
void groupDestroyed(WorkspaceGroup* group);
protected:
void ext_workspace_manager_v1_workspace_group(::ext_workspace_group_handle_v1* handle) override;
void ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) override;
void ext_workspace_manager_v1_done() override;
void ext_workspace_manager_v1_finished() override;
private:
WorkspaceManager();
void destroyGroup(WorkspaceGroup* group);
void destroyWorkspace(Workspace* workspace);
QHash<::ext_workspace_handle_v1*, Workspace*> mWorkspaces;
QHash<::ext_workspace_group_handle_v1*, WorkspaceGroup*> mGroups;
QList<WorkspaceGroup*> destroyedGroups;
QList<Workspace*> destroyedWorkspaces;
friend class Workspace;
friend class WorkspaceGroup;
};
class Workspace: public QtWayland::ext_workspace_handle_v1 {
public:
Workspace(::ext_workspace_handle_v1* handle): QtWayland::ext_workspace_handle_v1(handle) {}
~Workspace() override;
Q_DISABLE_COPY_MOVE(Workspace);
QString id;
QString name;
QList<qint32> coordinates;
WorkspaceGroup* group = nullptr;
bool active : 1 = false;
bool urgent : 1 = false;
bool hidden : 1 = false;
bool canActivate : 1 = false;
bool canDeactivate : 1 = false;
bool canRemove : 1 = false;
bool canAssign : 1 = false;
protected:
void ext_workspace_handle_v1_id(const QString& id) override;
void ext_workspace_handle_v1_name(const QString& name) override;
void ext_workspace_handle_v1_coordinates(wl_array* coordinates) override;
void ext_workspace_handle_v1_state(quint32 state) override;
void ext_workspace_handle_v1_capabilities(quint32 capabilities) override;
void ext_workspace_handle_v1_removed() override;
private:
void enterGroup(WorkspaceGroup* group);
void leaveGroup(WorkspaceGroup* group);
friend class WorkspaceGroup;
};
class WorkspaceGroup: public QtWayland::ext_workspace_group_handle_v1 {
public:
WorkspaceGroup(::ext_workspace_group_handle_v1* handle)
: QtWayland::ext_workspace_group_handle_v1(handle) {}
~WorkspaceGroup() override;
Q_DISABLE_COPY_MOVE(WorkspaceGroup);
WlOutputTracker screens;
bool canCreateWorkspace : 1 = false;
protected:
void ext_workspace_group_handle_v1_capabilities(quint32 capabilities) override;
void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override;
void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override;
void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override;
void ext_workspace_group_handle_v1_workspace_leave(::ext_workspace_handle_v1* handle) override;
void ext_workspace_group_handle_v1_removed() override;
};
} // namespace qs::wayland::workspace

View file

@ -0,0 +1,23 @@
#include <qcontainerfwd.h>
#include <qguiapplication.h>
#include <qlist.h>
#include "../../core/plugin.hpp"
namespace qs::wm::wayland {
void installWmProvider();
}
namespace {
class WaylandWmPlugin: public QsEnginePlugin {
QList<QString> dependencies() override { return {"window"}; }
bool applies() override { return QGuiApplication::platformName() == "wayland"; }
void init() override { qs::wm::wayland::installWmProvider(); }
};
QS_REGISTER_PLUGIN(WaylandWmPlugin);
} // namespace

View file

@ -0,0 +1,21 @@
#include "windowmanager.hpp"
#include "../../windowmanager/windowmanager.hpp"
#include "windowset.hpp"
namespace qs::wm::wayland {
WaylandWindowManager* WaylandWindowManager::instance() {
static auto* instance = []() {
auto* wm = new WaylandWindowManager();
WindowsetManager::instance();
return wm;
}();
return instance;
}
void installWmProvider() { // NOLINT (misc-use-internal-linkage)
qs::wm::WindowManager::setProvider([]() { return WaylandWindowManager::instance(); });
}
} // namespace qs::wm::wayland

View file

@ -0,0 +1,17 @@
#pragma once
#include <qtmetamacros.h>
#include "../../windowmanager/windowmanager.hpp"
#include "windowset.hpp"
namespace qs::wm::wayland {
class WaylandWindowManager: public WindowManager {
Q_OBJECT;
public:
static WaylandWindowManager* instance();
};
} // namespace qs::wm::wayland

View file

@ -0,0 +1,252 @@
#include "windowset.hpp"
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qproperty.h>
#include "../../windowmanager/windowmanager.hpp"
#include "../../windowmanager/windowset.hpp"
#include "../../windowmanager/screenprojection.hpp"
#include "ext_workspace.hpp"
namespace qs::wm::wayland {
WindowsetManager::WindowsetManager() {
auto* impl = impl::WorkspaceManager::instance();
QObject::connect(
impl,
&impl::WorkspaceManager::serverCommit,
this,
&WindowsetManager::onServerCommit
);
QObject::connect(
impl,
&impl::WorkspaceManager::workspaceCreated,
this,
&WindowsetManager::onWindowsetCreated
);
QObject::connect(
impl,
&impl::WorkspaceManager::workspaceDestroyed,
this,
&WindowsetManager::onWindowsetDestroyed
);
QObject::connect(
impl,
&impl::WorkspaceManager::groupCreated,
this,
&WindowsetManager::onProjectionCreated
);
QObject::connect(
impl,
&impl::WorkspaceManager::groupDestroyed,
this,
&WindowsetManager::onProjectionDestroyed
);
}
void WindowsetManager::scheduleCommit() {
if (this->commitScheduled) {
qCDebug(impl::logWorkspace) << "Workspace commit already scheduled.";
return;
}
qCDebug(impl::logWorkspace) << "Scheduling workspace commit...";
this->commitScheduled = true;
QMetaObject::invokeMethod(this, &WindowsetManager::doCommit, Qt::QueuedConnection);
}
void WindowsetManager::doCommit() { // NOLINT
qCDebug(impl::logWorkspace) << "Committing workspaces...";
impl::WorkspaceManager::instance()->commit();
this->commitScheduled = false;
}
void WindowsetManager::onServerCommit() {
// Projections are created/destroyed around windowsets to avoid any nulls making it
// to the qml engine.
Qt::beginPropertyUpdateGroup();
auto* wm = WindowManager::instance();
auto windowsets = wm->bWindowsets.value();
auto projections = wm->bWindowsetProjections.value();
for (auto* projImpl: this->pendingProjectionCreations) {
auto* projection = new WlWindowsetProjection(this, projImpl);
this->projectionsByImpl.insert(projImpl, projection);
projections.append(projection);
}
for (auto* wsImpl: this->pendingWindowsetCreations) {
auto* ws = new WlWindowset(this, wsImpl);
this->windowsetByImpl.insert(wsImpl, ws);
windowsets.append(ws);
}
for (auto* wsImpl: this->pendingWindowsetDestructions) {
windowsets.removeOne(this->windowsetByImpl.value(wsImpl));
this->windowsetByImpl.remove(wsImpl);
}
for (auto* projImpl: this->pendingProjectionDestructions) {
projections.removeOne(this->projectionsByImpl.value(projImpl));
this->projectionsByImpl.remove(projImpl);
}
for (auto* ws: windowsets) {
static_cast<WlWindowset*>(ws)->commitImpl(); // NOLINT
}
for (auto* projection: projections) {
static_cast<WlWindowsetProjection*>(projection)->commitImpl(); // NOLINT
}
this->pendingWindowsetCreations.clear();
this->pendingWindowsetDestructions.clear();
this->pendingProjectionCreations.clear();
this->pendingProjectionDestructions.clear();
wm->bWindowsets = windowsets;
wm->bWindowsetProjections = projections;
Qt::endPropertyUpdateGroup();
}
void WindowsetManager::onWindowsetCreated(impl::Workspace* workspace) {
this->pendingWindowsetCreations.append(workspace);
}
void WindowsetManager::onWindowsetDestroyed(impl::Workspace* workspace) {
if (!this->pendingWindowsetCreations.removeOne(workspace)) {
this->pendingWindowsetDestructions.append(workspace);
}
}
void WindowsetManager::onProjectionCreated(impl::WorkspaceGroup* group) {
this->pendingProjectionCreations.append(group);
}
void WindowsetManager::onProjectionDestroyed(impl::WorkspaceGroup* group) {
if (!this->pendingProjectionCreations.removeOne(group)) {
this->pendingProjectionDestructions.append(group);
}
}
WindowsetManager* WindowsetManager::instance() {
static auto* instance = new WindowsetManager();
return instance;
}
WlWindowset::WlWindowset(WindowsetManager* manager, impl::Workspace* impl)
: Windowset(manager)
, impl(impl) {
this->commitImpl();
}
void WlWindowset::commitImpl() {
Qt::beginPropertyUpdateGroup();
this->bId = this->impl->id;
this->bName = this->impl->name;
this->bCoordinates = this->impl->coordinates;
this->bActive = this->impl->active;
this->bShouldDisplay = !this->impl->hidden;
this->bUrgent = this->impl->urgent;
this->bCanActivate = this->impl->canActivate;
this->bCanDeactivate = this->impl->canDeactivate;
this->bCanSetProjection = this->impl->canAssign;
this->bProjection = this->manager()->projectionsByImpl.value(this->impl->group);
Qt::endPropertyUpdateGroup();
}
void WlWindowset::activate() {
if (!this->bCanActivate) {
qCritical(logWorkspace) << this << "cannot be activated";
return;
}
qCDebug(impl::logWorkspace) << "Calling activate() for" << this;
this->impl->activate();
WindowsetManager::instance()->scheduleCommit();
}
void WlWindowset::deactivate() {
if (!this->bCanDeactivate) {
qCritical(logWorkspace) << this << "cannot be deactivated";
return;
}
qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this;
this->impl->deactivate();
WindowsetManager::instance()->scheduleCommit();
}
void WlWindowset::remove() {
if (!this->bCanRemove) {
qCritical(logWorkspace) << this << "cannot be removed";
return;
}
qCDebug(impl::logWorkspace) << "Calling remove() for" << this;
this->impl->remove();
WindowsetManager::instance()->scheduleCommit();
}
void WlWindowset::setProjection(WindowsetProjection* projection) {
if (!this->bCanSetProjection) {
qCritical(logWorkspace) << this << "cannot be assigned to a projection";
return;
}
if (!projection) {
qCritical(logWorkspace) << "Cannot set a windowset's projection to null";
return;
}
WlWindowsetProjection* wlProjection = nullptr;
if (auto* p = dynamic_cast<WlWindowsetProjection*>(projection)) {
wlProjection = p;
} else if (auto* p = dynamic_cast<ScreenProjection*>(projection)) {
// In the 99% case, there will only be a single windowset on a screen.
// In the 1% case, the oldest projection (first in list) is most likely the desired one.
auto* screen = p->screen();
for (const auto& proj: WindowsetManager::instance()->projectionsByImpl.values()) {
if (proj->bQScreens.value().contains(screen)) {
wlProjection = proj;
break;
}
}
}
if (!wlProjection) {
qCritical(logWorkspace) << "Cannot set a windowset's projection to" << projection
<< "as no wayland projection could be derived.";
return;
}
qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << projection;
this->impl->assign(wlProjection->impl->object());
WindowsetManager::instance()->scheduleCommit();
}
WlWindowsetProjection::WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl)
: WindowsetProjection(manager)
, impl(impl) {
this->commitImpl();
}
void WlWindowsetProjection::commitImpl() {
// TODO: will not commit the correct screens if missing qt repr at commit time
this->bQScreens = this->impl->screens.screens();
}
} // namespace qs::wm::wayland

View file

@ -0,0 +1,85 @@
#pragma once
#include <qhash.h>
#include <qlist.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include "../../windowmanager/windowset.hpp"
#include "ext_workspace.hpp"
namespace qs::wm::wayland {
namespace impl = qs::wayland::workspace;
class WlWindowset;
class WlWindowsetProjection;
class WindowsetManager: public QObject {
Q_OBJECT;
public:
static WindowsetManager* instance();
void scheduleCommit();
private slots:
void doCommit();
void onServerCommit();
void onWindowsetCreated(impl::Workspace* workspace);
void onWindowsetDestroyed(impl::Workspace* workspace);
void onProjectionCreated(impl::WorkspaceGroup* group);
void onProjectionDestroyed(impl::WorkspaceGroup* group);
private:
WindowsetManager();
bool commitScheduled = false;
QList<impl::Workspace*> pendingWindowsetCreations;
QList<impl::Workspace*> pendingWindowsetDestructions;
QHash<impl::Workspace*, WlWindowset*> windowsetByImpl;
QList<impl::WorkspaceGroup*> pendingProjectionCreations;
QList<impl::WorkspaceGroup*> pendingProjectionDestructions;
QHash<impl::WorkspaceGroup*, WlWindowsetProjection*> projectionsByImpl;
friend class WlWindowset;
};
class WlWindowset: public Windowset {
public:
WlWindowset(WindowsetManager* manager, impl::Workspace* impl);
void commitImpl();
void activate() override;
void deactivate() override;
void remove() override;
void setProjection(WindowsetProjection* projection) override;
[[nodiscard]] WindowsetManager* manager() {
return static_cast<WindowsetManager*>(this->parent()); // NOLINT
}
private:
impl::Workspace* impl = nullptr;
};
class WlWindowsetProjection: public WindowsetProjection {
public:
WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl);
void commitImpl();
[[nodiscard]] WindowsetManager* manager() {
return static_cast<WindowsetManager*>(this->parent()); // NOLINT
}
private:
impl::WorkspaceGroup* impl = nullptr;
friend class WlWindowset;
};
} // namespace qs::wm::wayland