Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,7 @@ if build_tests
'math_provider',
'notification_filter',
'notification_history_utf8',
'niri_workspace_backend',
'path_browse',
'plugin_bindings',
'plugin_catalog',
Expand Down
68 changes: 68 additions & 0 deletions src/compositors/compositor_platform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ namespace {

constexpr Logger kLog("compositor_platform");

[[nodiscard]] std::unordered_set<std::string>
windowIdsFromAssignments(const std::vector<WorkspaceWindowAssignment>& assignments) {
std::unordered_set<std::string> windowIds;
windowIds.reserve(assignments.size());
for (const auto& assignment : assignments) {
if (!assignment.windowId.empty()) {
windowIds.insert(assignment.windowId);
}
}
return windowIds;
}

void
retainToplevelsWithWindowIds(std::vector<ToplevelInfo>& windows, const std::unordered_set<std::string>& windowIds) {
std::erase_if(windows, [&](const ToplevelInfo& window) { return !windowIds.contains(window.identifier); });
}

[[nodiscard]] const char* valueOrUnset(const char* value) {
return value != nullptr && value[0] != '\0' ? value : "<unset>";
}
Expand Down Expand Up @@ -875,11 +892,56 @@ std::vector<ToplevelInfo> CompositorPlatform::windowsWithoutAppId(wl_output* out
return windows;
}

std::vector<ToplevelInfo> CompositorPlatform::taskbarWindowsForApp(
const std::string& idLower, const std::string& wmClassLower, wl_output* outputFilter,
const std::unordered_set<std::string>* allowedNiriWindowIds
) const {
if (compositors::isNiri() && m_wayland.hasExtForeignToplevelList()) {
// Niri exposes its numeric IPC window ID as the ext-foreign-toplevel
// identifier, providing an exact association even for duplicate titles.
auto windows = m_wayland.extWindowsForApp(idLower, wmClassLower);
if (!windows.empty()) {
if (allowedNiriWindowIds != nullptr) {
retainToplevelsWithWindowIds(windows, *allowedNiriWindowIds);
} else {
retainToplevelsWithWindowIds(windows, windowIdsFromAssignments(workspaceWindowAssignments(outputFilter)));
}
}
// Do not fall back to title/app-id matching while one side of the exact
// Niri-id join is still pending. The ext `done` or IPC update will retry.
return windows;
}
return windowsForApp(idLower, wmClassLower, outputFilter);
}

std::vector<ToplevelInfo> CompositorPlatform::taskbarWindowsWithoutAppId(
wl_output* outputFilter, const std::unordered_set<std::string>* allowedNiriWindowIds
) const {
if (compositors::isNiri() && m_wayland.hasExtForeignToplevelList()) {
auto windows = m_wayland.extWindowsWithoutAppId();
if (!windows.empty()) {
if (allowedNiriWindowIds != nullptr) {
retainToplevelsWithWindowIds(windows, *allowedNiriWindowIds);
} else {
retainToplevelsWithWindowIds(windows, windowIdsFromAssignments(workspaceWindowAssignments(outputFilter)));
}
}
return windows;
}
return windowsWithoutAppId(outputFilter);
}

void CompositorPlatform::activateToplevel(zwlr_foreign_toplevel_handle_v1* handle) {
m_wayland.activateToplevel(handle);
}

void CompositorPlatform::activateToplevelInfo(const ToplevelInfo& window) {
// Niri's ext-foreign-toplevel identifier is its exact IPC window ID, and
// foreign-toplevel activation does not reliably scroll to the target.
if (compositors::isNiri() && window.extHandle != nullptr && !window.identifier.empty()) {
focusCompositorWindow(window.identifier);
return;
}
if (window.handle != nullptr) {
activateToplevel(window.handle);
return;
Expand All @@ -900,6 +962,12 @@ void CompositorPlatform::closeToplevelInfo(const ToplevelInfo& window) {
closeToplevel(window.handle);
return;
}
if (compositors::isNiri() && !window.identifier.empty()) {
if (m_workspaceMetadataBackend != nullptr) {
(void)m_workspaceMetadataBackend->closeWindowById(window.identifier);
}
return;
}
if (compositors::isKde() && m_kwinActiveWindow != nullptr && m_kwinActiveWindow->isAvailable()) {
m_kwinActiveWindow->closeWindow(window.title, window.appId, window.identifier);
}
Expand Down
8 changes: 8 additions & 0 deletions src/compositors/compositor_platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>

struct wl_output;
Expand Down Expand Up @@ -104,6 +105,13 @@ class CompositorPlatform {
[[nodiscard]] std::vector<ToplevelInfo>
windowsForApp(const std::string& idLower, const std::string& wmClassLower, wl_output* outputFilter = nullptr) const;
[[nodiscard]] std::vector<ToplevelInfo> windowsWithoutAppId(wl_output* outputFilter = nullptr) const;
[[nodiscard]] std::vector<ToplevelInfo> taskbarWindowsForApp(
const std::string& idLower, const std::string& wmClassLower, wl_output* outputFilter = nullptr,
const std::unordered_set<std::string>* allowedNiriWindowIds = nullptr
) const;
[[nodiscard]] std::vector<ToplevelInfo> taskbarWindowsWithoutAppId(
wl_output* outputFilter = nullptr, const std::unordered_set<std::string>* allowedNiriWindowIds = nullptr
) const;
[[nodiscard]] bool containsWlrToplevelHandle(zwlr_foreign_toplevel_handle_v1* handle) const;
void activateToplevel(zwlr_foreign_toplevel_handle_v1* handle);
void activateToplevelInfo(const ToplevelInfo& window);
Expand Down
19 changes: 18 additions & 1 deletion src/compositors/niri/niri_workspace_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,18 @@ std::optional<std::string> NiriWorkspaceBackend::focusedWindowId() const {
return std::to_string(*m_focusedWindowId);
}

bool NiriWorkspaceBackend::closeWindowById(const std::string& windowId) {
const auto id = parseUnsigned(windowId);
if (!id.has_value()) {
return false;
}
return m_runtime.requestAction(
nlohmann::json{
{"CloseWindow", nlohmann::json{{"id", *id}}},
}
);
}

void NiriWorkspaceBackend::cleanup() { m_runtime.cleanup(); }

void NiriWorkspaceBackend::handleStreamReset() {
Expand Down Expand Up @@ -481,12 +493,13 @@ bool NiriWorkspaceBackend::handleWindowOpenedOrChanged(const nlohmann::json& pay

const bool membershipChanged = existing == m_windows.end() ? (state.workspaceId.has_value() || !state.appId.empty())
: !sameWindowMembership(existing->second, state);
const bool layoutChanged = existing != m_windows.end() && !sameWindowLayout(existing->second, state);
m_windows[*id] = state;
if (membershipChanged) {
recomputeOccupancy();
}

return membershipChanged || focusUpdated;
return membershipChanged || layoutChanged || focusUpdated;
}

bool NiriWorkspaceBackend::handleWindowLayoutsChanged(const nlohmann::json& payload) {
Expand Down Expand Up @@ -694,6 +707,10 @@ bool NiriWorkspaceBackend::sameWindowMembership(
return true;
}

bool NiriWorkspaceBackend::sameWindowLayout(const WindowState& lhs, const WindowState& rhs) noexcept {
return lhs.x == rhs.x && lhs.y == rhs.y;
}

std::optional<std::uint64_t> NiriWorkspaceBackend::parseUnsigned(const std::string& value) {
if (value.empty()) {
return std::nullopt;
Expand Down
4 changes: 3 additions & 1 deletion src/compositors/niri/niri_workspace_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ class NiriWorkspaceBackend final : public compositors::WorkspaceMetadataBackend,
[[nodiscard]] std::unordered_map<std::string, std::vector<std::string>>
appIdsByWorkspace(const std::string& outputName = {}) const override;
[[nodiscard]] std::vector<WorkspaceWindow> workspaceWindows(const std::string& outputName = {}) const override;
bool focusWindowById(const std::string& windowId) override;
[[nodiscard]] std::optional<std::string> focusedWindowId() const override;
bool focusWindowById(const std::string& windowId) override;
bool closeWindowById(const std::string& windowId) override;
void cleanup() override;

private:
Expand Down Expand Up @@ -81,6 +82,7 @@ class NiriWorkspaceBackend final : public compositors::WorkspaceMetadataBackend,
const std::unordered_map<std::uint64_t, WindowState>& lhs,
const std::unordered_map<std::uint64_t, WindowState>& rhs
) noexcept;
[[nodiscard]] static bool sameWindowLayout(const WindowState& lhs, const WindowState& rhs) noexcept;
[[nodiscard]] static std::optional<std::uint64_t> parseUnsigned(const std::string& value);
[[nodiscard]] static std::optional<std::size_t> parseLeadingNumber(const std::string& value);
[[nodiscard]] static std::string workspaceKey(const WorkspaceState& workspace);
Expand Down
4 changes: 2 additions & 2 deletions src/compositors/workspace_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ namespace compositors {
[[nodiscard]] virtual std::vector<WorkspaceWindow> workspaceWindows(const std::string& /*outputName*/ = {}) const {
return {};
}
[[nodiscard]] virtual std::optional<std::string> focusedWindowId() const { return std::nullopt; }
// Focus a window by its compositor-specific id. Returns true if the backend
// handled the request (so the caller can skip other focus paths). Named
// distinctly from WorkspaceBackend::focusWindow so backends that implement
// both interfaces don't hit a conflicting-return-type override.
virtual bool focusWindowById(const std::string& /*windowId*/) { return false; }
// Currently focused compositor window id, when the backend tracks focus.
[[nodiscard]] virtual std::optional<std::string> focusedWindowId() const { return std::nullopt; }
virtual bool closeWindowById(const std::string& /*windowId*/) { return false; }
[[nodiscard]] virtual bool canTrackOverviewState() const noexcept { return false; }
[[nodiscard]] virtual bool hasOverviewState() const noexcept { return false; }
[[nodiscard]] virtual bool isOverviewOpen() const noexcept { return true; }
Expand Down
Loading