feat(active-window): add focused toplevel widget with icon/title

This commit is contained in:
Lysec
2026-04-07 13:47:02 +02:00
parent 2782a895ca
commit e7c87c7d17
11 changed files with 703 additions and 1 deletions
+25
View File
@@ -66,6 +66,8 @@ set(XDG_ACTIVATION_XML
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/xdg-activation/xdg-activation-v1.xml")
set(EXT_SESSION_LOCK_XML
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/ext-session-lock/ext-session-lock-v1.xml")
set(WLR_FOREIGN_TOPLEVEL_XML
"${CMAKE_CURRENT_SOURCE_DIR}/protocols/wlr-foreign-toplevel-management-unstable-v1.xml")
set(XDG_OUTPUT_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/xdg-output-unstable-v1-client-protocol.c")
@@ -93,6 +95,10 @@ set(EXT_SESSION_LOCK_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/ext-session-lock-v1-client-protocol.c")
set(EXT_SESSION_LOCK_PROTOCOL_H
"${GENERATED_PROTOCOL_DIR}/ext-session-lock-v1-client-protocol.h")
set(WLR_FOREIGN_TOPLEVEL_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/wlr-foreign-toplevel-management-unstable-v1-client-protocol.c")
set(WLR_FOREIGN_TOPLEVEL_PROTOCOL_H
"${GENERATED_PROTOCOL_DIR}/wlr-foreign-toplevel-management-unstable-v1-client-protocol.h")
set(WLR_LAYER_SHELL_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/wlr-layer-shell-unstable-v1-client-protocol.c")
@@ -209,6 +215,20 @@ add_custom_command(
VERBATIM
)
add_custom_command(
OUTPUT "${WLR_FOREIGN_TOPLEVEL_PROTOCOL_C}"
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" private-code "${WLR_FOREIGN_TOPLEVEL_XML}" "${WLR_FOREIGN_TOPLEVEL_PROTOCOL_C}"
DEPENDS "${WLR_FOREIGN_TOPLEVEL_XML}"
VERBATIM
)
add_custom_command(
OUTPUT "${WLR_FOREIGN_TOPLEVEL_PROTOCOL_H}"
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" client-header "${WLR_FOREIGN_TOPLEVEL_XML}" "${WLR_FOREIGN_TOPLEVEL_PROTOCOL_H}"
DEPENDS "${WLR_FOREIGN_TOPLEVEL_XML}"
VERBATIM
)
add_custom_target(noctalia_wayland_protocols
DEPENDS
"${WLR_LAYER_SHELL_PROTOCOL_C}"
@@ -225,6 +245,8 @@ add_custom_target(noctalia_wayland_protocols
"${XDG_ACTIVATION_PROTOCOL_H}"
"${EXT_SESSION_LOCK_PROTOCOL_C}"
"${EXT_SESSION_LOCK_PROTOCOL_H}"
"${WLR_FOREIGN_TOPLEVEL_PROTOCOL_C}"
"${WLR_FOREIGN_TOPLEVEL_PROTOCOL_H}"
)
# --- Target ---
@@ -318,6 +340,7 @@ add_executable(noctalia
src/shell/launcher/launcher_panel.cpp
src/shell/panels/test_panel.cpp
src/shell/widgets/launcher_widget.cpp
src/shell/widgets/active_window_widget.cpp
src/shell/widgets/clock_widget.cpp
src/shell/widgets/notification_widget.cpp
src/shell/widgets/spacer_widget.cpp
@@ -333,6 +356,7 @@ add_executable(noctalia
src/wayland/layer_surface.cpp
src/wayland/wayland_connection.cpp
src/wayland/wayland_seat.cpp
src/wayland/wayland_toplevels.cpp
src/wayland/wayland_workspaces.cpp
third_party/tinyexpr/tinyexpr.c
"${WLR_LAYER_SHELL_PROTOCOL_C}"
@@ -342,6 +366,7 @@ add_executable(noctalia
"${CURSOR_SHAPE_PROTOCOL_C}"
"${XDG_ACTIVATION_PROTOCOL_C}"
"${EXT_SESSION_LOCK_PROTOCOL_C}"
"${WLR_FOREIGN_TOPLEVEL_PROTOCOL_C}"
)
target_compile_definitions(noctalia PRIVATE
NOCTALIA_ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets"
+2 -1
View File
@@ -16,6 +16,7 @@ A lightweight Wayland shell and bar with no Qt or GTK dependency.
| Wayland core | `libwayland-client`, `wayland-scanner`, `wayland-protocols` |
| Surfaces | `zwlr-layer-shell-v1` |
| Multi-monitor | `zxdg-output-unstable-v1` |
| Active window metadata | `zwlr-foreign-toplevel-management-unstable-v1` |
| Lockscreen | `ext-session-lock-v1` |
| Cursor | `wp-cursor-shape-v1` |
| Rendering | `EGL`, `OpenGL ES 3`, `wayland-egl` |
@@ -229,7 +230,7 @@ gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug
- [ ] Microphone
- [ ] Power profile
- [ ] System monitor
- [ ] Active window
- [x] Active window
- [ ] Dock
- [ ] Keyboard layout
- [ ] Lock keys (Caps/Num)
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_foreign_toplevel_management_unstable_v1">
<copyright>
Copyright © 2018 Ilia Bozhinov
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="zwlr_foreign_toplevel_manager_v1" version="3">
<description summary="list and control opened apps">
The purpose of this protocol is to enable the creation of taskbars
and docks by providing them with a list of opened applications and
letting them request certain actions on them, like maximizing.
</description>
<event name="toplevel">
<arg name="toplevel" type="new_id" interface="zwlr_foreign_toplevel_handle_v1"/>
</event>
<request name="stop"/>
<event name="finished"/>
</interface>
<interface name="zwlr_foreign_toplevel_handle_v1" version="3">
<description summary="an opened toplevel"/>
<event name="title">
<arg name="title" type="string"/>
</event>
<event name="app_id">
<arg name="app_id" type="string"/>
</event>
<event name="output_enter">
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="output_leave">
<arg name="output" type="object" interface="wl_output"/>
</event>
<request name="set_maximized"/>
<request name="unset_maximized"/>
<request name="set_minimized"/>
<request name="unset_minimized"/>
<request name="activate">
<arg name="seat" type="object" interface="wl_seat"/>
</request>
<enum name="state">
<entry name="maximized" value="0"/>
<entry name="minimized" value="1"/>
<entry name="activated" value="2"/>
<entry name="fullscreen" value="3" since="2"/>
</enum>
<event name="state">
<arg name="state" type="array"/>
</event>
<event name="done"/>
<request name="close"/>
<request name="set_rectangle">
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<event name="closed"/>
<request name="destroy" type="destructor"/>
<request name="set_fullscreen" since="2">
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
</request>
<request name="unset_fullscreen" since="2"/>
<event name="parent" since="3">
<arg name="parent" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="true"/>
</event>
</interface>
</protocol>
+1
View File
@@ -105,6 +105,7 @@ void Application::initServices() {
m_lockScreen.onOutputChange();
});
m_wayland.setWorkspaceChangeCallback([this]() { m_bar.onWorkspaceChange(); });
m_wayland.setToplevelChangeCallback([this]() { m_bar.onWorkspaceChange(); });
m_wallpaper.initialize(m_wayland, &m_configService, &m_stateService);
+7
View File
@@ -5,6 +5,7 @@
#include "dbus/tray/tray_service.h"
#include "notification/notification_manager.h"
#include "shell/widgets/battery_widget.h"
#include "shell/widgets/active_window_widget.h"
#include "shell/widgets/launcher_widget.h"
#include "shell/widgets/clock_widget.h"
#include "shell/widgets/notification_widget.h"
@@ -53,6 +54,12 @@ std::unique_ptr<Widget> WidgetFactory::create(const std::string& name, wl_output
return std::make_unique<WorkspacesWidget>(m_wayland, output);
}
if (type == "active_window") {
const float maxTitleWidth = static_cast<float>(wc != nullptr ? wc->getDouble("max_width", 260.0) : 260.0);
const float iconSize = static_cast<float>(wc != nullptr ? wc->getDouble("icon_size", 16.0) : 16.0);
return std::make_unique<ActiveWindowWidget>(m_wayland, maxTitleWidth, iconSize);
}
if (type == "notifications") {
std::int32_t scale = 1;
const auto* wlOutput = m_wayland.findOutputByWl(output);
+218
View File
@@ -0,0 +1,218 @@
#include "shell/widgets/active_window_widget.h"
#include "launcher/desktop_entry.h"
#include "render/core/renderer.h"
#include "render/scene/node.h"
#include "ui/controls/image.h"
#include "ui/controls/label.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "wayland/wayland_connection.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <string_view>
namespace {
std::string toLower(std::string_view value) {
std::string out(value);
std::transform(out.begin(), out.end(), out.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return out;
}
std::string fitTextToWidth(Renderer& renderer, const std::string& text, float fontSize, bool bold, float maxWidth) {
if (text.empty() || maxWidth <= 0.0f) {
return {};
}
if (renderer.measureText(text, fontSize, bold).width <= maxWidth) {
return text;
}
static constexpr const char* kEllipsis = "...";
const float ellipsisWidth = renderer.measureText(kEllipsis, fontSize, bold).width;
if (ellipsisWidth >= maxWidth) {
return {};
}
std::size_t lo = 0;
std::size_t hi = text.size();
std::size_t best = 0;
while (lo <= hi) {
const std::size_t mid = lo + ((hi - lo) / 2);
std::string candidate = text.substr(0, mid) + kEllipsis;
if (renderer.measureText(candidate, fontSize, bold).width <= maxWidth) {
best = mid;
lo = mid + 1;
} else {
if (mid == 0) {
break;
}
hi = mid - 1;
}
}
return text.substr(0, best) + kEllipsis;
}
} // namespace
ActiveWindowWidget::ActiveWindowWidget(WaylandConnection& connection, float maxTitleWidth, float iconSize)
: m_connection(connection), m_maxTitleWidth(maxTitleWidth), m_iconSize(iconSize) {
buildDesktopIconIndex();
}
void ActiveWindowWidget::create(Renderer& renderer) {
auto rootNode = std::make_unique<Node>();
auto icon = std::make_unique<Image>();
icon->setCornerRadius(Style::radiusSm);
icon->setBackground(Color{palette.surfaceVariant.r, palette.surfaceVariant.g, palette.surfaceVariant.b, 0.75f});
icon->setFit(ImageFit::Contain);
icon->setSize(m_iconSize * m_contentScale, m_iconSize * m_contentScale);
m_icon = static_cast<Image*>(rootNode->addChild(std::move(icon)));
auto title = std::make_unique<Label>();
title->setBold(true);
title->setFontSize(Style::fontSizeBody * m_contentScale);
title->setColor(palette.onSurface);
title->setMaxWidth(m_maxTitleWidth * m_contentScale);
m_title = static_cast<Label*>(rootNode->addChild(std::move(title)));
m_root = std::move(rootNode);
syncState(renderer);
}
void ActiveWindowWidget::layout(Renderer& renderer, float /*containerWidth*/, float /*containerHeight*/) {
auto* rootNode = root();
if (rootNode == nullptr || m_icon == nullptr || m_title == nullptr) {
return;
}
const float iconSize = m_iconSize * m_contentScale;
m_icon->setSize(iconSize, iconSize);
m_title->setMaxWidth(m_maxTitleWidth * m_contentScale);
m_title->measure(renderer);
const float contentHeight = std::max(iconSize, m_title->height());
const float iconY = std::round((contentHeight - iconSize) * 0.5f);
const float labelY = std::round((contentHeight - m_title->height()) * 0.5f);
m_icon->setPosition(0.0f, iconY);
m_title->setPosition(iconSize + Style::spaceXs, labelY);
rootNode->setSize(m_title->x() + m_title->width(), contentHeight);
}
void ActiveWindowWidget::update(Renderer& renderer) {
syncState(renderer);
Widget::update(renderer);
}
void ActiveWindowWidget::syncState(Renderer& renderer) {
if (m_icon == nullptr || m_title == nullptr) {
return;
}
const auto current = m_connection.activeToplevel();
std::string identifier;
std::string title;
std::string appId;
if (current.has_value()) {
identifier = current->identifier;
title = current->title;
appId = current->appId;
}
if (title.empty()) {
title = !appId.empty() ? appId : "Desktop";
}
if (identifier == m_lastIdentifier && title == m_lastTitle && appId == m_lastAppId) {
return;
}
m_lastIdentifier = std::move(identifier);
m_lastTitle = title;
m_lastAppId = appId;
std::string iconPath = resolveIconPath(appId);
if (iconPath.empty()) {
iconPath = m_iconResolver.resolve("application-x-executable");
}
const float titleMaxWidth = m_maxTitleWidth * m_contentScale;
m_title->setText(fitTextToWidth(renderer, m_lastTitle, Style::fontSizeBody * m_contentScale, true, titleMaxWidth));
m_title->measure(renderer);
if (iconPath != m_lastIconPath) {
m_lastIconPath = iconPath;
if (!m_lastIconPath.empty()) {
m_icon->setSourceFile(renderer, m_lastIconPath, static_cast<int>(std::round(48.0f * m_contentScale)));
} else {
m_icon->clear(renderer);
}
}
requestRedraw();
}
std::string ActiveWindowWidget::resolveIconPath(const std::string& appId) {
if (appId.empty()) {
return {};
}
auto resolveByName = [this](const std::string& name) -> std::string {
if (name.empty()) {
return {};
}
return m_iconResolver.resolve(name);
};
if (auto it = m_appIcons.find(appId); it != m_appIcons.end()) {
const auto path = resolveByName(it->second);
if (!path.empty()) {
return path;
}
}
const std::string appIdLower = toLower(appId);
if (auto it = m_appIcons.find(appIdLower); it != m_appIcons.end()) {
const auto path = resolveByName(it->second);
if (!path.empty()) {
return path;
}
}
if (const auto slash = appId.find_last_of('/'); slash != std::string::npos && slash + 1 < appId.size()) {
const std::string tail = appId.substr(slash + 1);
if (auto it = m_appIcons.find(tail); it != m_appIcons.end()) {
const auto path = resolveByName(it->second);
if (!path.empty()) {
return path;
}
}
}
return resolveByName(appId);
}
void ActiveWindowWidget::buildDesktopIconIndex() {
const auto entries = scanDesktopEntries();
for (const auto& entry : entries) {
if (entry.id.empty() || entry.icon.empty()) {
continue;
}
m_appIcons.try_emplace(entry.id, entry.icon);
m_appIcons.try_emplace(toLower(entry.id), entry.icon);
if (const auto dot = entry.id.rfind('.'); dot != std::string::npos && dot + 1 < entry.id.size()) {
m_appIcons.try_emplace(entry.id.substr(dot + 1), entry.icon);
m_appIcons.try_emplace(toLower(entry.id.substr(dot + 1)), entry.icon);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include "shell/widget/widget.h"
#include "system/icon_resolver.h"
#include <string>
#include <unordered_map>
class Image;
class Label;
class Renderer;
class WaylandConnection;
class ActiveWindowWidget : public Widget {
public:
ActiveWindowWidget(WaylandConnection& connection, float maxTitleWidth, float iconSize);
void create(Renderer& renderer) override;
void layout(Renderer& renderer, float containerWidth, float containerHeight) override;
void update(Renderer& renderer) override;
private:
void syncState(Renderer& renderer);
[[nodiscard]] std::string resolveIconPath(const std::string& appId);
void buildDesktopIconIndex();
WaylandConnection& m_connection;
float m_maxTitleWidth = 240.0f;
float m_iconSize = 16.0f;
Image* m_icon = nullptr;
Label* m_title = nullptr;
IconResolver m_iconResolver;
std::unordered_map<std::string, std::string> m_appIcons;
std::string m_lastIdentifier;
std::string m_lastTitle;
std::string m_lastAppId;
std::string m_lastIconPath;
};
+19
View File
@@ -10,6 +10,7 @@
#include "cursor-shape-v1-client-protocol.h"
#include "ext-session-lock-v1-client-protocol.h"
#include "ext-workspace-v1-client-protocol.h"
#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h"
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
#include "xdg-activation-v1-client-protocol.h"
#include "xdg-output-unstable-v1-client-protocol.h"
@@ -22,6 +23,7 @@ constexpr std::uint32_t kShmVersion = 1;
constexpr std::uint32_t kLayerShellVersion = 4;
constexpr std::uint32_t kXdgOutputManagerVersion = 3;
constexpr std::uint32_t kExtWorkspaceManagerVersion = 1;
constexpr std::uint32_t kWlrForeignToplevelManagerVersion = 3;
constexpr std::uint32_t kCursorShapeManagerVersion = 1;
constexpr std::uint32_t kXdgActivationVersion = 1;
constexpr std::uint32_t kExtSessionLockManagerVersion = 1;
@@ -159,6 +161,10 @@ void WaylandConnection::setWorkspaceChangeCallback(ChangeCallback callback) {
m_workspacesHandler.setChangeCallback(std::move(callback));
}
void WaylandConnection::setToplevelChangeCallback(ChangeCallback callback) {
m_toplevelsHandler.setChangeCallback(std::move(callback));
}
void WaylandConnection::setPointerEventCallback(WaylandSeat::PointerEventCallback callback) {
m_seatHandler.setPointerEventCallback(std::move(callback));
}
@@ -193,6 +199,8 @@ std::vector<Workspace> WaylandConnection::workspaces(wl_output* output) const {
return m_workspacesHandler.forOutput(output);
}
std::optional<ActiveToplevel> WaylandConnection::activeToplevel() const { return m_toplevelsHandler.current(); }
bool WaylandConnection::isConnected() const noexcept { return m_display != nullptr; }
bool WaylandConnection::hasRequiredGlobals() const noexcept {
@@ -204,6 +212,7 @@ bool WaylandConnection::hasLayerShell() const noexcept { return m_hasLayerShellG
bool WaylandConnection::hasXdgOutputManager() const noexcept { return m_xdgOutputManager != nullptr; }
bool WaylandConnection::hasExtWorkspaceManager() const noexcept { return m_hasExtWorkspaceGlobal; }
bool WaylandConnection::hasForeignToplevelManager() const noexcept { return m_hasForeignToplevelManagerGlobal; }
bool WaylandConnection::hasSessionLockManager() const noexcept { return m_sessionLockManager != nullptr; }
bool WaylandConnection::hasXdgActivation() const noexcept { return m_xdgActivation != nullptr; }
@@ -342,6 +351,15 @@ void WaylandConnection::bindGlobal(wl_registry* registry, std::uint32_t name, co
return;
}
if (interfaceName == zwlr_foreign_toplevel_manager_v1_interface.name) {
m_hasForeignToplevelManagerGlobal = true;
const auto bindVersion = std::min(version, kWlrForeignToplevelManagerVersion);
auto* manager = static_cast<zwlr_foreign_toplevel_manager_v1*>(
wl_registry_bind(registry, name, &zwlr_foreign_toplevel_manager_v1_interface, bindVersion));
m_toplevelsHandler.bind(manager);
return;
}
if (interfaceName == wp_cursor_shape_manager_v1_interface.name) {
const auto bindVersion = std::min(version, kCursorShapeManagerVersion);
m_cursorShapeManager = static_cast<wp_cursor_shape_manager_v1*>(
@@ -388,6 +406,7 @@ void WaylandConnection::bindGlobal(wl_registry* registry, std::uint32_t name, co
}
void WaylandConnection::cleanup() {
m_toplevelsHandler.cleanup();
m_workspacesHandler.cleanup();
for (auto& out : m_outputs) {
+8
View File
@@ -1,10 +1,12 @@
#pragma once
#include "wayland/wayland_seat.h"
#include "wayland/wayland_toplevels.h"
#include "wayland/wayland_workspaces.h"
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <vector>
@@ -20,6 +22,7 @@ struct zxdg_output_v1;
struct wp_cursor_shape_manager_v1;
struct xdg_activation_v1;
struct ext_session_lock_manager_v1;
struct zwlr_foreign_toplevel_manager_v1;
struct WaylandOutput {
std::uint32_t name = 0;
@@ -52,6 +55,7 @@ public:
// Delegate setters
void setOutputChangeCallback(ChangeCallback callback);
void setWorkspaceChangeCallback(ChangeCallback callback);
void setToplevelChangeCallback(ChangeCallback callback);
void setPointerEventCallback(WaylandSeat::PointerEventCallback callback);
void setKeyboardEventCallback(WaylandSeat::KeyboardEventCallback callback);
void setCursorShape(std::uint32_t serial, std::uint32_t shape);
@@ -69,6 +73,7 @@ public:
[[nodiscard]] bool hasLayerShell() const noexcept;
[[nodiscard]] bool hasXdgOutputManager() const noexcept;
[[nodiscard]] bool hasExtWorkspaceManager() const noexcept;
[[nodiscard]] bool hasForeignToplevelManager() const noexcept;
[[nodiscard]] bool hasSessionLockManager() const noexcept;
[[nodiscard]] wl_display* display() const noexcept;
[[nodiscard]] wl_compositor* compositor() const noexcept;
@@ -84,6 +89,7 @@ public:
[[nodiscard]] std::vector<Workspace> workspaces() const;
[[nodiscard]] std::vector<Workspace> workspaces(wl_output* output) const;
[[nodiscard]] std::optional<ActiveToplevel> activeToplevel() const;
// Registry listener entrypoints
static void handleGlobal(void* data, wl_registry* registry, std::uint32_t name, const char* interface,
@@ -107,9 +113,11 @@ private:
ext_session_lock_manager_v1* m_sessionLockManager = nullptr;
bool m_hasLayerShellGlobal = false;
bool m_hasExtWorkspaceGlobal = false;
bool m_hasForeignToplevelManagerGlobal = false;
std::vector<WaylandOutput> m_outputs;
ChangeCallback m_outputChangeCallback;
WaylandSeat m_seatHandler;
WaylandWorkspaces m_workspacesHandler;
WaylandToplevels m_toplevelsHandler;
};
+218
View File
@@ -0,0 +1,218 @@
#include "wayland/wayland_toplevels.h"
#include <algorithm>
#include <wayland-client.h>
#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h"
namespace {
void managerToplevel(void* data, zwlr_foreign_toplevel_manager_v1* /*manager*/,
zwlr_foreign_toplevel_handle_v1* handle) {
static_cast<WaylandToplevels*>(data)->onToplevelCreated(handle);
}
void managerFinished(void* data, zwlr_foreign_toplevel_manager_v1* /*manager*/) {
static_cast<WaylandToplevels*>(data)->onManagerFinished();
}
const zwlr_foreign_toplevel_manager_v1_listener kManagerListener = {
.toplevel = managerToplevel,
.finished = managerFinished,
};
void handleClosed(void* data, zwlr_foreign_toplevel_handle_v1* handle) {
static_cast<WaylandToplevels*>(data)->onHandleClosed(handle);
}
void handleDone(void* data, zwlr_foreign_toplevel_handle_v1* handle) {
static_cast<WaylandToplevels*>(data)->onHandleDone(handle);
}
void handleTitle(void* data, zwlr_foreign_toplevel_handle_v1* handle, const char* title) {
static_cast<WaylandToplevels*>(data)->onHandleTitle(handle, title);
}
void handleAppId(void* data, zwlr_foreign_toplevel_handle_v1* handle, const char* appId) {
static_cast<WaylandToplevels*>(data)->onHandleAppId(handle, appId);
}
void handleState(void* data, zwlr_foreign_toplevel_handle_v1* handle, wl_array* state) {
static_cast<WaylandToplevels*>(data)->onHandleState(handle, state);
}
void handleOutputEnter(void* /*data*/, zwlr_foreign_toplevel_handle_v1* /*handle*/, wl_output* /*output*/) {}
void handleOutputLeave(void* /*data*/, zwlr_foreign_toplevel_handle_v1* /*handle*/, wl_output* /*output*/) {}
void handleParent(void* /*data*/, zwlr_foreign_toplevel_handle_v1* /*handle*/,
zwlr_foreign_toplevel_handle_v1* /*parent*/) {}
const zwlr_foreign_toplevel_handle_v1_listener kHandleListener = {
.title = handleTitle,
.app_id = handleAppId,
.output_enter = handleOutputEnter,
.output_leave = handleOutputLeave,
.state = handleState,
.done = handleDone,
.closed = handleClosed,
.parent = handleParent,
};
} // namespace
void WaylandToplevels::bind(zwlr_foreign_toplevel_manager_v1* manager) {
m_manager = manager;
zwlr_foreign_toplevel_manager_v1_add_listener(m_manager, &kManagerListener, this);
}
void WaylandToplevels::setChangeCallback(ChangeCallback callback) { m_changeCallback = std::move(callback); }
void WaylandToplevels::cleanup() {
for (auto& [handle, _] : m_handles) {
if (handle != nullptr) {
zwlr_foreign_toplevel_handle_v1_destroy(handle);
}
}
m_handles.clear();
m_currentHandle = nullptr;
if (m_manager != nullptr) {
zwlr_foreign_toplevel_manager_v1_stop(m_manager);
zwlr_foreign_toplevel_manager_v1_destroy(m_manager);
m_manager = nullptr;
}
}
std::optional<ActiveToplevel> WaylandToplevels::current() const {
if (m_currentHandle == nullptr) {
return std::nullopt;
}
const auto it = m_handles.find(m_currentHandle);
if (it == m_handles.end()) {
return std::nullopt;
}
return ActiveToplevel{
.title = it->second.title,
.appId = it->second.appId,
.identifier = it->second.appId + ":" + it->second.title,
};
}
void WaylandToplevels::onToplevelCreated(zwlr_foreign_toplevel_handle_v1* handle) {
if (handle == nullptr) {
return;
}
m_handles.try_emplace(handle, ToplevelState{});
zwlr_foreign_toplevel_handle_v1_add_listener(handle, &kHandleListener, this);
}
void WaylandToplevels::onManagerFinished() {
if (m_manager != nullptr) {
zwlr_foreign_toplevel_manager_v1_destroy(m_manager);
m_manager = nullptr;
}
}
void WaylandToplevels::onHandleClosed(zwlr_foreign_toplevel_handle_v1* handle) {
const auto before = current();
if (handle != nullptr) {
zwlr_foreign_toplevel_handle_v1_destroy(handle);
m_handles.erase(handle);
}
if (m_currentHandle == handle) {
m_currentHandle = nullptr;
selectFallbackCurrent();
}
notifyIfChanged(before);
}
void WaylandToplevels::onHandleDone(zwlr_foreign_toplevel_handle_v1* handle) {
auto it = m_handles.find(handle);
if (it == m_handles.end()) {
return;
}
const auto before = current();
if (it->second.activated) {
m_currentHandle = handle;
} else if (m_currentHandle == nullptr || it->second.dirty) {
m_currentHandle = handle;
}
it->second.dirty = false;
notifyIfChanged(before);
}
void WaylandToplevels::onHandleTitle(zwlr_foreign_toplevel_handle_v1* handle, const char* title) {
auto it = m_handles.find(handle);
if (it == m_handles.end()) {
return;
}
it->second.title = title != nullptr ? title : "";
it->second.dirty = true;
it->second.generation = ++m_generation;
}
void WaylandToplevels::onHandleAppId(zwlr_foreign_toplevel_handle_v1* handle, const char* appId) {
auto it = m_handles.find(handle);
if (it == m_handles.end()) {
return;
}
it->second.appId = appId != nullptr ? appId : "";
it->second.dirty = true;
it->second.generation = ++m_generation;
}
void WaylandToplevels::onHandleState(zwlr_foreign_toplevel_handle_v1* handle, wl_array* state) {
auto it = m_handles.find(handle);
if (it == m_handles.end()) {
return;
}
bool activated = false;
if (state != nullptr) {
auto* value = static_cast<const std::uint32_t*>(state->data);
const auto count = state->size / sizeof(std::uint32_t);
for (std::size_t i = 0; i < count; ++i) {
if (value[i] == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) {
activated = true;
break;
}
}
}
it->second.activated = activated;
it->second.dirty = true;
it->second.generation = ++m_generation;
}
void WaylandToplevels::notifyIfChanged(const std::optional<ActiveToplevel>& before) {
const auto now = current();
if (before.has_value() != now.has_value()) {
if (m_changeCallback) {
m_changeCallback();
}
return;
}
if (!before.has_value() || !now.has_value()) {
return;
}
if (before->title != now->title || before->appId != now->appId || before->identifier != now->identifier) {
if (m_changeCallback) {
m_changeCallback();
}
}
}
void WaylandToplevels::selectFallbackCurrent() {
if (m_handles.empty()) {
return;
}
auto best = std::max_element(m_handles.begin(), m_handles.end(),
[](const auto& a, const auto& b) { return a.second.generation < b.second.generation; });
if (best != m_handles.end()) {
m_currentHandle = best->first;
}
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
struct zwlr_foreign_toplevel_handle_v1;
struct zwlr_foreign_toplevel_manager_v1;
struct wl_array;
struct ActiveToplevel {
std::string title;
std::string appId;
std::string identifier;
};
class WaylandToplevels {
public:
using ChangeCallback = std::function<void()>;
void bind(zwlr_foreign_toplevel_manager_v1* manager);
void setChangeCallback(ChangeCallback callback);
void cleanup();
[[nodiscard]] std::optional<ActiveToplevel> current() const;
// Listener entrypoints called by C callbacks
void onToplevelCreated(zwlr_foreign_toplevel_handle_v1* handle);
void onManagerFinished();
void onHandleClosed(zwlr_foreign_toplevel_handle_v1* handle);
void onHandleDone(zwlr_foreign_toplevel_handle_v1* handle);
void onHandleTitle(zwlr_foreign_toplevel_handle_v1* handle, const char* title);
void onHandleAppId(zwlr_foreign_toplevel_handle_v1* handle, const char* appId);
void onHandleState(zwlr_foreign_toplevel_handle_v1* handle, wl_array* state);
private:
struct ToplevelState {
std::string title;
std::string appId;
bool activated = false;
bool dirty = false;
std::uint64_t generation = 0;
};
void notifyIfChanged(const std::optional<ActiveToplevel>& before);
void selectFallbackCurrent();
zwlr_foreign_toplevel_manager_v1* m_manager = nullptr;
std::unordered_map<zwlr_foreign_toplevel_handle_v1*, ToplevelState> m_handles;
zwlr_foreign_toplevel_handle_v1* m_currentHandle = nullptr;
std::uint64_t m_generation = 0;
ChangeCallback m_changeCallback;
};