mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(active-window): add focused toplevel widget with icon/title
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user