From e7c87c7d17814a5c51fb4db9802406ef921185e1 Mon Sep 17 00:00:00 2001 From: Lysec Date: Tue, 7 Apr 2026 13:47:02 +0200 Subject: [PATCH] feat(active-window): add focused toplevel widget with icon/title --- CMakeLists.txt | 25 ++ README.md | 3 +- ...oreign-toplevel-management-unstable-v1.xml | 110 +++++++++ src/app/application.cpp | 1 + src/shell/widget/widget_factory.cpp | 7 + src/shell/widgets/active_window_widget.cpp | 218 ++++++++++++++++++ src/shell/widgets/active_window_widget.h | 40 ++++ src/wayland/wayland_connection.cpp | 19 ++ src/wayland/wayland_connection.h | 8 + src/wayland/wayland_toplevels.cpp | 218 ++++++++++++++++++ src/wayland/wayland_toplevels.h | 55 +++++ 11 files changed, 703 insertions(+), 1 deletion(-) create mode 100644 protocols/wlr-foreign-toplevel-management-unstable-v1.xml create mode 100644 src/shell/widgets/active_window_widget.cpp create mode 100644 src/shell/widgets/active_window_widget.h create mode 100644 src/wayland/wayland_toplevels.cpp create mode 100644 src/wayland/wayland_toplevels.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c7aaea6e..a06c0a0a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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" diff --git a/README.md b/README.md index aba64d46d..8e17957e1 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/protocols/wlr-foreign-toplevel-management-unstable-v1.xml b/protocols/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 000000000..36c9c4573 --- /dev/null +++ b/protocols/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,110 @@ + + + + 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. + + + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/application.cpp b/src/app/application.cpp index efe5b0f8d..129c42d75 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -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); diff --git a/src/shell/widget/widget_factory.cpp b/src/shell/widget/widget_factory.cpp index 9f600e55a..2a0c386ce 100644 --- a/src/shell/widget/widget_factory.cpp +++ b/src/shell/widget/widget_factory.cpp @@ -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 WidgetFactory::create(const std::string& name, wl_output return std::make_unique(m_wayland, output); } + if (type == "active_window") { + const float maxTitleWidth = static_cast(wc != nullptr ? wc->getDouble("max_width", 260.0) : 260.0); + const float iconSize = static_cast(wc != nullptr ? wc->getDouble("icon_size", 16.0) : 16.0); + return std::make_unique(m_wayland, maxTitleWidth, iconSize); + } + if (type == "notifications") { std::int32_t scale = 1; const auto* wlOutput = m_wayland.findOutputByWl(output); diff --git a/src/shell/widgets/active_window_widget.cpp b/src/shell/widgets/active_window_widget.cpp new file mode 100644 index 000000000..1ec17170f --- /dev/null +++ b/src/shell/widgets/active_window_widget.cpp @@ -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 +#include +#include +#include + +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(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(); + + auto icon = std::make_unique(); + 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(rootNode->addChild(std::move(icon))); + + auto title = std::make_unique