mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(panel): added support for clicking outside the panel to close
This commit is contained in:
@@ -265,6 +265,8 @@ foreach _p : [
|
||||
'protocols/wlr-foreign-toplevel-management-unstable-v1.xml'],
|
||||
['wlr-data-control-unstable-v1',
|
||||
'protocols/wlr-data-control-unstable-v1.xml'],
|
||||
['hyprland-focus-grab-v1',
|
||||
'protocols/hyprland-focus-grab-v1.xml'],
|
||||
]
|
||||
_name = _p[0]
|
||||
_xml = _p[1]
|
||||
@@ -428,6 +430,8 @@ _noctalia_sources = files(
|
||||
'src/shell/osd/audio_osd.cpp',
|
||||
'src/shell/osd/brightness_osd.cpp',
|
||||
'src/shell/osd/osd_overlay.cpp',
|
||||
'src/shell/panel/panel_click_shield.cpp',
|
||||
'src/shell/panel/panel_focus_grab.cpp',
|
||||
'src/shell/panel/panel_manager.cpp',
|
||||
'src/shell/polkit/polkit_panel.cpp',
|
||||
'src/shell/session/session_panel.cpp',
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="hyprland_focus_grab_v1">
|
||||
<copyright>
|
||||
Copyright © 2024 outfoxxed
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</copyright>
|
||||
|
||||
<description summary="limit input focus to a set of surfaces">
|
||||
This protocol allows clients to limit input focus to a specific set
|
||||
of surfaces and receive a notification when the limiter is removed as
|
||||
detailed below.
|
||||
</description>
|
||||
|
||||
<interface name="hyprland_focus_grab_manager_v1" version="1">
|
||||
<description summary="manager for focus grab objects">
|
||||
This interface allows a client to create surface grab objects.
|
||||
</description>
|
||||
|
||||
<request name="create_grab">
|
||||
<description summary="create a focus grab object">
|
||||
Create a surface grab object.
|
||||
</description>
|
||||
|
||||
<arg name="grab" type="new_id" interface="hyprland_focus_grab_v1"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the focus grab manager">
|
||||
Destroy the focus grab manager.
|
||||
This doesn't destroy existing focus grab objects.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="hyprland_focus_grab_v1" version="1">
|
||||
<description summary="input focus limiter">
|
||||
This interface restricts input focus to a specified whitelist of
|
||||
surfaces as long as the focus grab object exists and has at least
|
||||
one comitted surface.
|
||||
|
||||
Mouse and touch events inside a whitelisted surface will be passed
|
||||
to the surface normally, while events outside of a whitelisted surface
|
||||
will clear the grab object. Keyboard events will be passed to the client
|
||||
and a compositor-picked surface in the whitelist will receive a
|
||||
wl_keyboard::enter event if a whitelisted surface is not already entered.
|
||||
|
||||
Upon meeting implementation-defined criteria usually meaning a mouse or
|
||||
touch input outside of any whitelisted surfaces, the compositor will
|
||||
clear the whitelist, rendering the grab inert and sending the cleared
|
||||
event. The same will happen if another focus grab or similar action
|
||||
is started at the compositor's discretion.
|
||||
</description>
|
||||
|
||||
<request name="add_surface">
|
||||
<description summary="add a surface to the focus whitelist">
|
||||
Add a surface to the whitelist. Destroying the surface is treated the
|
||||
same as an explicit call to remove_surface and duplicate additions are
|
||||
ignored.
|
||||
|
||||
Does not take effect until commit is called.
|
||||
</description>
|
||||
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
</request>
|
||||
|
||||
<request name="remove_surface">
|
||||
<description summary="remove a surface from the focus whitelist">
|
||||
Remove a surface from the whitelist. Destroying the surface is treated
|
||||
the same as an explicit call to this function.
|
||||
|
||||
If the grab was active and the removed surface was entered by the
|
||||
keyboard, another surface will be entered on commit.
|
||||
|
||||
Does not take effect until commit is called.
|
||||
</description>
|
||||
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
</request>
|
||||
|
||||
<request name="commit">
|
||||
<description summary="commit the focus whitelist">
|
||||
Commit pending changes to the surface whitelist.
|
||||
|
||||
If the list previously had no entries and now has at least one, the grab
|
||||
will start. If it previously had entries and now has none, the grab will
|
||||
become inert.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the focus grab">
|
||||
Destroy the grab object and remove the grab if active.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="cleared">
|
||||
<description summary="the focus grab was cleared">
|
||||
Sent when an active grab is cancelled by the compositor,
|
||||
regardless of cause.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -833,6 +833,9 @@ void Application::initUi() {
|
||||
[this](wl_output* output, std::optional<AttachedPanelGeometry> geometry) {
|
||||
m_bar.setAttachedPanelGeometry(output, geometry);
|
||||
});
|
||||
m_panelManager.setClickShieldExcludeRectsProvider(
|
||||
[this](wl_output* output) { return m_bar.surfaceRectsForOutput(output); });
|
||||
m_panelManager.setFocusGrabBarSurfacesProvider([this]() { return m_bar.allBarSurfaces(); });
|
||||
m_bar.setAutoHideSuppressionCallback([this]() { return m_trayMenu.isOpen() || m_panelManager.isAttachedOpen(); });
|
||||
// When config reloads, refresh any open panel: bar-driven attached decoration restyle and
|
||||
// shell-driven compositor blur.
|
||||
|
||||
@@ -571,6 +571,87 @@ std::optional<LayerPopupParentContext> Bar::preferredPopupParentContext(wl_outpu
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<InputRect> Bar::surfaceRectsForOutput(wl_output* output) const {
|
||||
std::vector<InputRect> rects;
|
||||
if (m_wayland == nullptr || output == nullptr) {
|
||||
return rects;
|
||||
}
|
||||
|
||||
const WaylandOutput* wlOutput = m_wayland->findOutputByWl(output);
|
||||
if (wlOutput == nullptr) {
|
||||
return rects;
|
||||
}
|
||||
// logicalWidth/Height become valid only after xdg_output.done; before that
|
||||
// we cannot accurately place a bottom/right anchored bar.
|
||||
if (wlOutput->logicalWidth <= 0 || wlOutput->logicalHeight <= 0) {
|
||||
return rects;
|
||||
}
|
||||
const std::int32_t outputW = wlOutput->logicalWidth;
|
||||
const std::int32_t outputH = wlOutput->logicalHeight;
|
||||
|
||||
for (const auto& instance : m_instances) {
|
||||
if (instance == nullptr || instance->output != output || instance->surface == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const auto* surface = instance->surface.get();
|
||||
const std::uint32_t anchor = surface->anchor();
|
||||
const bool aTop = (anchor & LayerShellAnchor::Top) != 0;
|
||||
const bool aBottom = (anchor & LayerShellAnchor::Bottom) != 0;
|
||||
const bool aLeft = (anchor & LayerShellAnchor::Left) != 0;
|
||||
const bool aRight = (anchor & LayerShellAnchor::Right) != 0;
|
||||
const std::int32_t mTop = surface->marginTop();
|
||||
const std::int32_t mRight = surface->marginRight();
|
||||
const std::int32_t mBottom = surface->marginBottom();
|
||||
const std::int32_t mLeft = surface->marginLeft();
|
||||
// surface->width()/height() may be 0 before configure; fall back to BarConfig
|
||||
// thickness so we still publish a sensible exclusion for fresh surfaces.
|
||||
const std::int32_t surfW = static_cast<std::int32_t>(surface->width());
|
||||
const std::int32_t surfH = static_cast<std::int32_t>(surface->height());
|
||||
|
||||
std::int32_t rectW = surfW;
|
||||
std::int32_t rectH = surfH;
|
||||
std::int32_t rectX = 0;
|
||||
std::int32_t rectY = 0;
|
||||
|
||||
if (aLeft && aRight) {
|
||||
rectW = std::max(0, outputW - mLeft - mRight);
|
||||
rectX = mLeft;
|
||||
} else if (aRight) {
|
||||
rectX = std::max(0, outputW - mRight - rectW);
|
||||
} else {
|
||||
rectX = mLeft;
|
||||
}
|
||||
|
||||
if (aTop && aBottom) {
|
||||
rectH = std::max(0, outputH - mTop - mBottom);
|
||||
rectY = mTop;
|
||||
} else if (aBottom) {
|
||||
rectY = std::max(0, outputH - mBottom - rectH);
|
||||
} else {
|
||||
rectY = mTop;
|
||||
}
|
||||
|
||||
if (rectW > 0 && rectH > 0) {
|
||||
rects.push_back(InputRect{rectX, rectY, rectW, rectH});
|
||||
}
|
||||
}
|
||||
|
||||
return rects;
|
||||
}
|
||||
|
||||
std::vector<wl_surface*> Bar::allBarSurfaces() const {
|
||||
std::vector<wl_surface*> surfaces;
|
||||
surfaces.reserve(m_instances.size());
|
||||
for (const auto& instance : m_instances) {
|
||||
if (instance != nullptr && instance->surface != nullptr) {
|
||||
if (wl_surface* s = instance->surface->wlSurface(); s != nullptr) {
|
||||
surfaces.push_back(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return surfaces;
|
||||
}
|
||||
|
||||
void Bar::setAttachedPanelGeometry(wl_output* output, std::optional<AttachedPanelGeometry> geometry) {
|
||||
BarInstance* instance = instanceForOutput(output);
|
||||
if (instance == nullptr) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "shell/bar/widget_factory.h"
|
||||
#include "shell/panel/attached_panel_context.h"
|
||||
#include "ui/dialogs/layer_popup_host.h"
|
||||
#include "wayland/surface.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -68,6 +69,13 @@ public:
|
||||
[[nodiscard]] bool isRunning() const noexcept;
|
||||
[[nodiscard]] std::optional<LayerPopupParentContext> popupParentContextForSurface(wl_surface* surface) const noexcept;
|
||||
[[nodiscard]] std::optional<LayerPopupParentContext> preferredPopupParentContext(wl_output* output) const noexcept;
|
||||
// Returns the bar surface rects on the given output in output-local logical
|
||||
// coordinates. Used by the panel click shield to keep clicks on bar widgets
|
||||
// flowing to the bar instead of dismissing the active panel.
|
||||
[[nodiscard]] std::vector<InputRect> surfaceRectsForOutput(wl_output* output) const;
|
||||
// Returns every bar wl_surface across all outputs. Used as the focus-grab
|
||||
// whitelist on Hyprland so bar widgets keep receiving clicks.
|
||||
[[nodiscard]] std::vector<wl_surface*> allBarSurfaces() const;
|
||||
void setAttachedPanelGeometry(wl_output* output, std::optional<AttachedPanelGeometry> geometry);
|
||||
void beginAttachedPopup(wl_surface* surface);
|
||||
void endAttachedPopup(wl_surface* surface);
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
#include "shell/panel/panel_click_shield.h"
|
||||
|
||||
#include "compositors/compositor_detect.h"
|
||||
#include "core/log.h"
|
||||
#include "viewporter-client-protocol.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#include <wayland-client.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("panel-click-shield");
|
||||
|
||||
// Anonymous file backing for a tiny SHM pool. We use memfd_create so the fd
|
||||
// is never visible on the filesystem.
|
||||
int createAnonFd(std::size_t size) {
|
||||
int fd = memfd_create("noctalia-click-shield", MFD_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (ftruncate(fd, static_cast<off_t>(size)) != 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
const zwlr_layer_surface_v1_listener kLayerSurfaceListener = {
|
||||
.configure = &PanelClickShield::handleConfigure,
|
||||
.closed = &PanelClickShield::handleClosed,
|
||||
};
|
||||
|
||||
// Hyprland refuses to deliver pointer events to layer-shell surfaces with
|
||||
// keyboard_interactivity == None. Exclusive is what unlocks pointer delivery
|
||||
// there. Bar exclusion via input region doesn't actually work on Hyprland
|
||||
// (clicks on bars still hit the shield); on Hyprland we prefer the
|
||||
// hyprland_focus_grab_v1 protocol when available and only fall back to the
|
||||
// shield path here.
|
||||
LayerShellKeyboard shieldKeyboardMode() {
|
||||
return compositors::isHyprland() ? LayerShellKeyboard::Exclusive : LayerShellKeyboard::None;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PanelClickShield::~PanelClickShield() {
|
||||
deactivate();
|
||||
if (m_buffer != nullptr) {
|
||||
wl_buffer_destroy(m_buffer);
|
||||
m_buffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PanelClickShield::initialize(WaylandConnection& wayland) { m_wayland = &wayland; }
|
||||
|
||||
bool PanelClickShield::ensureSharedBuffer() {
|
||||
if (m_buffer != nullptr) {
|
||||
return true;
|
||||
}
|
||||
if (m_wayland == nullptr || m_wayland->shm() == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1×1 ARGB8888 — 4 bytes total. Stretched to fullscreen via wp_viewport.
|
||||
constexpr std::int32_t kWidth = 1;
|
||||
constexpr std::int32_t kHeight = 1;
|
||||
constexpr std::int32_t kStride = kWidth * 4;
|
||||
constexpr std::size_t kSize = static_cast<std::size_t>(kStride * kHeight);
|
||||
|
||||
int fd = createAnonFd(kSize);
|
||||
if (fd < 0) {
|
||||
kLog.warn("failed to create shm fd: {}", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
// ftruncate already zero-fills (transparent ARGB8888).
|
||||
|
||||
wl_shm_pool* pool = wl_shm_create_pool(m_wayland->shm(), fd, static_cast<std::int32_t>(kSize));
|
||||
close(fd);
|
||||
if (pool == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_buffer = wl_shm_pool_create_buffer(pool, 0, kWidth, kHeight, kStride, WL_SHM_FORMAT_ARGB8888);
|
||||
wl_shm_pool_destroy(pool);
|
||||
|
||||
return m_buffer != nullptr;
|
||||
}
|
||||
|
||||
void PanelClickShield::activate(const std::vector<wl_output*>& outputs, LayerShellLayer layer,
|
||||
ExcludeProvider excludeProvider) {
|
||||
if (m_wayland == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tear down any previous shields and recreate. Cheaper to refresh than to
|
||||
// diff outputs/exclude rects, and keeps the dispatch order deterministic.
|
||||
deactivate();
|
||||
|
||||
if (!ensureSharedBuffer()) {
|
||||
kLog.warn("disabled: shared shm buffer unavailable");
|
||||
return;
|
||||
}
|
||||
if (m_wayland->layerShell() == nullptr || m_wayland->compositor() == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (m_wayland->viewporter() == nullptr) {
|
||||
// Without viewporter we'd have to allocate a fullscreen-sized buffer to
|
||||
// make the surface logically fullscreen. Skip the shield rather than burn
|
||||
// tens of MB of SHM. Most modern compositors support viewporter.
|
||||
kLog.warn("disabled: wp_viewporter not available");
|
||||
return;
|
||||
}
|
||||
|
||||
for (wl_output* output : outputs) {
|
||||
if (output == nullptr || m_shields.find(output) != m_shields.end()) {
|
||||
continue;
|
||||
}
|
||||
std::vector<InputRect> excludeRects;
|
||||
if (excludeProvider) {
|
||||
excludeRects = excludeProvider(output);
|
||||
}
|
||||
auto shield = createShield(output, layer, std::move(excludeRects));
|
||||
if (shield) {
|
||||
m_shields.emplace(output, std::move(shield));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<PanelClickShield::Shield> PanelClickShield::createShield(wl_output* output, LayerShellLayer layer,
|
||||
std::vector<InputRect> excludeRects) {
|
||||
auto shield = std::make_unique<Shield>();
|
||||
shield->owner = this;
|
||||
shield->output = output;
|
||||
shield->excludeRects = std::move(excludeRects);
|
||||
|
||||
shield->surface = wl_compositor_create_surface(m_wayland->compositor());
|
||||
if (shield->surface == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
shield->viewport = wp_viewporter_get_viewport(m_wayland->viewporter(), shield->surface);
|
||||
|
||||
shield->layerSurface =
|
||||
zwlr_layer_shell_v1_get_layer_surface(m_wayland->layerShell(), shield->surface, output,
|
||||
static_cast<std::uint32_t>(layer), "noctalia-panel-click-shield");
|
||||
if (shield->layerSurface == nullptr) {
|
||||
if (shield->viewport != nullptr) {
|
||||
wp_viewport_destroy(shield->viewport);
|
||||
}
|
||||
wl_surface_destroy(shield->surface);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
zwlr_layer_surface_v1_add_listener(shield->layerSurface, &kLayerSurfaceListener, shield.get());
|
||||
|
||||
zwlr_layer_surface_v1_set_anchor(shield->layerSurface, LayerShellAnchor::Top | LayerShellAnchor::Bottom |
|
||||
LayerShellAnchor::Left | LayerShellAnchor::Right);
|
||||
zwlr_layer_surface_v1_set_size(shield->layerSurface, 0, 0);
|
||||
zwlr_layer_surface_v1_set_exclusive_zone(shield->layerSurface, -1);
|
||||
zwlr_layer_surface_v1_set_keyboard_interactivity(shield->layerSurface,
|
||||
static_cast<std::uint32_t>(shieldKeyboardMode()));
|
||||
|
||||
// Empty input region until we receive a configure with the actual surface
|
||||
// size. Until then any click would land on the 1×1 buffer at (0,0) before
|
||||
// the viewport applies, which we don't want.
|
||||
if (wl_region* emptyRegion = wl_compositor_create_region(m_wayland->compositor()); emptyRegion != nullptr) {
|
||||
wl_surface_set_input_region(shield->surface, emptyRegion);
|
||||
wl_region_destroy(emptyRegion);
|
||||
}
|
||||
|
||||
// Initial commit (no buffer) — required by layer-shell to enter the
|
||||
// configure round-trip. The buffer is attached on the first configure.
|
||||
wl_surface_commit(shield->surface);
|
||||
|
||||
return shield;
|
||||
}
|
||||
|
||||
void PanelClickShield::deactivate() {
|
||||
for (auto& [output, shield] : m_shields) {
|
||||
if (shield) {
|
||||
destroyShield(*shield);
|
||||
}
|
||||
}
|
||||
m_shields.clear();
|
||||
}
|
||||
|
||||
void PanelClickShield::destroyShield(Shield& shield) {
|
||||
if (shield.viewport != nullptr) {
|
||||
wp_viewport_destroy(shield.viewport);
|
||||
shield.viewport = nullptr;
|
||||
}
|
||||
if (shield.layerSurface != nullptr) {
|
||||
zwlr_layer_surface_v1_destroy(shield.layerSurface);
|
||||
shield.layerSurface = nullptr;
|
||||
}
|
||||
if (shield.surface != nullptr) {
|
||||
wl_surface_destroy(shield.surface);
|
||||
shield.surface = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool PanelClickShield::ownsSurface(wl_surface* surface) const noexcept {
|
||||
if (surface == nullptr) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& [output, shield] : m_shields) {
|
||||
if (shield && shield->surface == surface) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PanelClickShield::handleConfigure(void* data, zwlr_layer_surface_v1* layerSurface, std::uint32_t serial,
|
||||
std::uint32_t width, std::uint32_t height) {
|
||||
zwlr_layer_surface_v1_ack_configure(layerSurface, serial);
|
||||
auto* shield = static_cast<Shield*>(data);
|
||||
if (shield == nullptr || shield->owner == nullptr) {
|
||||
return;
|
||||
}
|
||||
shield->owner->applyConfigured(*shield, width, height);
|
||||
}
|
||||
|
||||
void PanelClickShield::handleClosed(void* data, zwlr_layer_surface_v1* /*layerSurface*/) {
|
||||
auto* shield = static_cast<Shield*>(data);
|
||||
if (shield == nullptr || shield->owner == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Compositor closed our surface. Drop just this shield; the others (and
|
||||
// the next activate()) keep working.
|
||||
PanelClickShield* owner = shield->owner;
|
||||
wl_output* output = shield->output;
|
||||
auto it = owner->m_shields.find(output);
|
||||
if (it != owner->m_shields.end() && it->second.get() == shield) {
|
||||
owner->destroyShield(*it->second);
|
||||
owner->m_shields.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void PanelClickShield::applyConfigured(Shield& shield, std::uint32_t width, std::uint32_t height) {
|
||||
if (shield.surface == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
shield.width = static_cast<std::int32_t>(width);
|
||||
shield.height = static_cast<std::int32_t>(height);
|
||||
|
||||
if (!shield.bufferAttached && m_buffer != nullptr) {
|
||||
wl_surface_attach(shield.surface, m_buffer, 0, 0);
|
||||
wl_surface_set_buffer_scale(shield.surface, 1);
|
||||
wl_surface_damage_buffer(shield.surface, 0, 0, 1, 1);
|
||||
shield.bufferAttached = true;
|
||||
}
|
||||
|
||||
if (shield.viewport != nullptr && width > 0 && height > 0) {
|
||||
wp_viewport_set_destination(shield.viewport, static_cast<std::int32_t>(width), static_cast<std::int32_t>(height));
|
||||
}
|
||||
|
||||
shield.configured = true;
|
||||
applyInputRegion(shield);
|
||||
wl_surface_commit(shield.surface);
|
||||
}
|
||||
|
||||
void PanelClickShield::applyInputRegion(Shield& shield) {
|
||||
if (m_wayland == nullptr || shield.surface == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (shield.width <= 0 || shield.height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
wl_region* region = wl_compositor_create_region(m_wayland->compositor());
|
||||
if (region == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
wl_region_add(region, 0, 0, shield.width, shield.height);
|
||||
for (const auto& r : shield.excludeRects) {
|
||||
wl_region_subtract(region, r.x, r.y, r.width, r.height);
|
||||
}
|
||||
|
||||
wl_surface_set_input_region(shield.surface, region);
|
||||
wl_region_destroy(region);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "wayland/layer_surface.h"
|
||||
#include "wayland/surface.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class WaylandConnection;
|
||||
struct PointerEvent;
|
||||
struct wl_buffer;
|
||||
struct wl_output;
|
||||
struct wl_surface;
|
||||
struct wp_viewport;
|
||||
struct zwlr_layer_surface_v1;
|
||||
|
||||
// Transparent fullscreen layer-shell surfaces (one per output) that catch
|
||||
// clicks landing outside the active panel and outside the bars. Used to
|
||||
// dismiss panels when the user clicks on an xdg_toplevel app window — Wayland
|
||||
// routes that click to the app's surface, so without a catcher the panel
|
||||
// never sees it.
|
||||
//
|
||||
// Ordering: shields are mapped on the same layer as the panel; activate()
|
||||
// must be called BEFORE the panel surface is committed so that the panel
|
||||
// ends up on top of its co-output shield within the layer (wlroots stacks
|
||||
// within-layer surfaces in mapping order).
|
||||
//
|
||||
// Each shield's input region is fullscreen MINUS the rects returned by the
|
||||
// exclude provider for that output, so clicks on bar widgets keep flowing
|
||||
// to the bar.
|
||||
//
|
||||
// Keyboard interactivity:
|
||||
// - Hyprland gates pointer delivery on keyboard_interactivity: layer-shell
|
||||
// surfaces declared as None never receive pointer events, so the shield
|
||||
// uses Exclusive there. The panel is also Exclusive and is mapped after
|
||||
// the shield, so per the layer-shell spec the panel still wins keyboard
|
||||
// focus.
|
||||
// - On every other compositor we tested (niri, wlroots vanilla, sway), None
|
||||
// works fine and avoids touching keyboard focus at all, so we keep that
|
||||
// as the default.
|
||||
//
|
||||
// Buffer strategy: a single shared 1×1 fully-transparent SHM buffer is
|
||||
// stretched to the per-shield surface size via wp_viewport. Cost is ~4 bytes
|
||||
// regardless of resolution or output count.
|
||||
class PanelClickShield {
|
||||
public:
|
||||
using ExcludeProvider = std::function<std::vector<InputRect>(wl_output*)>;
|
||||
|
||||
PanelClickShield() = default;
|
||||
~PanelClickShield();
|
||||
|
||||
PanelClickShield(const PanelClickShield&) = delete;
|
||||
PanelClickShield& operator=(const PanelClickShield&) = delete;
|
||||
|
||||
void initialize(WaylandConnection& wayland);
|
||||
|
||||
// Map a fullscreen shield on each of the given outputs.
|
||||
void activate(const std::vector<wl_output*>& outputs, LayerShellLayer layer, ExcludeProvider excludeProvider);
|
||||
|
||||
// Tear down all shields. Idempotent.
|
||||
void deactivate();
|
||||
|
||||
[[nodiscard]] bool isActive() const noexcept { return !m_shields.empty(); }
|
||||
[[nodiscard]] bool ownsSurface(wl_surface* surface) const noexcept;
|
||||
|
||||
// Public so the C-callback bridge in panel_click_shield.cpp can dispatch.
|
||||
static void handleConfigure(void* data, zwlr_layer_surface_v1* layerSurface, std::uint32_t serial,
|
||||
std::uint32_t width, std::uint32_t height);
|
||||
static void handleClosed(void* data, zwlr_layer_surface_v1* layerSurface);
|
||||
|
||||
private:
|
||||
struct Shield {
|
||||
PanelClickShield* owner = nullptr;
|
||||
wl_output* output = nullptr;
|
||||
wl_surface* surface = nullptr;
|
||||
zwlr_layer_surface_v1* layerSurface = nullptr;
|
||||
wp_viewport* viewport = nullptr;
|
||||
std::int32_t width = 0;
|
||||
std::int32_t height = 0;
|
||||
bool configured = false;
|
||||
bool bufferAttached = false;
|
||||
std::vector<InputRect> excludeRects;
|
||||
};
|
||||
|
||||
bool ensureSharedBuffer();
|
||||
std::unique_ptr<Shield> createShield(wl_output* output, LayerShellLayer layer, std::vector<InputRect> excludeRects);
|
||||
void destroyShield(Shield& shield);
|
||||
void applyConfigured(Shield& shield, std::uint32_t width, std::uint32_t height);
|
||||
void applyInputRegion(Shield& shield);
|
||||
|
||||
WaylandConnection* m_wayland = nullptr;
|
||||
wl_buffer* m_buffer = nullptr;
|
||||
std::unordered_map<wl_output*, std::unique_ptr<Shield>> m_shields;
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "shell/panel/panel_focus_grab.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "hyprland-focus-grab-v1-client-protocol.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("panel-focus-grab");
|
||||
|
||||
const hyprland_focus_grab_v1_listener kFocusGrabListener = {
|
||||
.cleared = &PanelFocusGrab::handleCleared,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PanelFocusGrab::~PanelFocusGrab() { deactivate(); }
|
||||
|
||||
void PanelFocusGrab::initialize(WaylandConnection& wayland) { m_wayland = &wayland; }
|
||||
|
||||
void PanelFocusGrab::setOnCleared(ClearedCallback callback) { m_onCleared = std::move(callback); }
|
||||
|
||||
bool PanelFocusGrab::available() const noexcept {
|
||||
return m_wayland != nullptr && m_wayland->hyprlandFocusGrabManager() != nullptr;
|
||||
}
|
||||
|
||||
void PanelFocusGrab::activate(const std::vector<wl_surface*>& surfaces) {
|
||||
if (!available()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace any existing grab. We don't try to diff: panels open infrequently
|
||||
// enough that recreating the grab keeps the lifecycle obvious.
|
||||
deactivate();
|
||||
|
||||
auto* manager = m_wayland->hyprlandFocusGrabManager();
|
||||
m_grab = hyprland_focus_grab_manager_v1_create_grab(manager);
|
||||
if (m_grab == nullptr) {
|
||||
kLog.warn("create_grab returned null");
|
||||
return;
|
||||
}
|
||||
|
||||
hyprland_focus_grab_v1_add_listener(m_grab, &kFocusGrabListener, this);
|
||||
|
||||
std::size_t added = 0;
|
||||
for (wl_surface* surface : surfaces) {
|
||||
if (surface == nullptr) {
|
||||
continue;
|
||||
}
|
||||
hyprland_focus_grab_v1_add_surface(m_grab, surface);
|
||||
++added;
|
||||
}
|
||||
|
||||
if (added == 0) {
|
||||
// Per protocol, committing an empty whitelist is inert. Tear down so we
|
||||
// don't leak the proxy, and so available()/isActive() stay consistent.
|
||||
hyprland_focus_grab_v1_destroy(m_grab);
|
||||
m_grab = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
hyprland_focus_grab_v1_commit(m_grab);
|
||||
}
|
||||
|
||||
void PanelFocusGrab::deactivate() {
|
||||
if (m_grab == nullptr) {
|
||||
return;
|
||||
}
|
||||
hyprland_focus_grab_v1_destroy(m_grab);
|
||||
m_grab = nullptr;
|
||||
}
|
||||
|
||||
void PanelFocusGrab::handleCleared(void* data, hyprland_focus_grab_v1* /*grab*/) {
|
||||
auto* self = static_cast<PanelFocusGrab*>(data);
|
||||
if (self == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (self->m_onCleared) {
|
||||
self->m_onCleared();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
class WaylandConnection;
|
||||
struct hyprland_focus_grab_v1;
|
||||
struct wl_surface;
|
||||
|
||||
// Thin wrapper around hyprland_focus_grab_v1. While active, the compositor
|
||||
// dispatches pointer/touch events landing inside any whitelisted surface to
|
||||
// that surface as usual; a click that lands outside the whitelist clears the
|
||||
// grab and fires `cleared`. We use this on Hyprland in lieu of the click
|
||||
// shield: same outcome (panel dismisses on outside click) without fighting
|
||||
// Hyprland's layer-shell input quirks.
|
||||
//
|
||||
// Lifecycle:
|
||||
// - activate(...) creates a fresh grab, adds the given surfaces, commits.
|
||||
// - deactivate() destroys the grab.
|
||||
// - On `cleared` the wrapper invokes onCleared (if set). The wrapper does
|
||||
// NOT auto-deactivate; the owner (PanelManager) decides what to do.
|
||||
//
|
||||
// `available()` is true only when the compositor advertises the protocol —
|
||||
// i.e. on Hyprland builds that support it. Callers should check this and
|
||||
// fall back to PanelClickShield otherwise.
|
||||
class PanelFocusGrab {
|
||||
public:
|
||||
using ClearedCallback = std::function<void()>;
|
||||
|
||||
PanelFocusGrab() = default;
|
||||
~PanelFocusGrab();
|
||||
|
||||
PanelFocusGrab(const PanelFocusGrab&) = delete;
|
||||
PanelFocusGrab& operator=(const PanelFocusGrab&) = delete;
|
||||
|
||||
void initialize(WaylandConnection& wayland);
|
||||
void setOnCleared(ClearedCallback callback);
|
||||
|
||||
[[nodiscard]] bool available() const noexcept;
|
||||
[[nodiscard]] bool isActive() const noexcept { return m_grab != nullptr; }
|
||||
|
||||
// Create a grab and seed its whitelist with `surfaces`. Any surface that
|
||||
// is null is skipped. Replaces a previous grab if one is already active.
|
||||
void activate(const std::vector<wl_surface*>& surfaces);
|
||||
|
||||
// Destroy the grab if active. Idempotent.
|
||||
void deactivate();
|
||||
|
||||
// Public so the C-callback bridge in panel_focus_grab.cpp can dispatch.
|
||||
static void handleCleared(void* data, hyprland_focus_grab_v1* grab);
|
||||
|
||||
private:
|
||||
WaylandConnection* m_wayland = nullptr;
|
||||
hyprland_focus_grab_v1* m_grab = nullptr;
|
||||
ClearedCallback m_onCleared;
|
||||
};
|
||||
@@ -68,6 +68,13 @@ void PanelManager::initialize(WaylandConnection& wayland, ConfigService* config,
|
||||
m_wayland = &wayland;
|
||||
m_config = config;
|
||||
m_renderContext = renderContext;
|
||||
m_clickShield.initialize(wayland);
|
||||
m_focusGrab.initialize(wayland);
|
||||
m_focusGrab.setOnCleared([this]() {
|
||||
if (isOpen() && !m_closing) {
|
||||
closePanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void PanelManager::setOpenSettingsWindowCallback(std::function<void()> callback) {
|
||||
@@ -88,6 +95,14 @@ void PanelManager::setAttachedPanelGeometryCallback(
|
||||
m_attachedPanelGeometryCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void PanelManager::setClickShieldExcludeRectsProvider(std::function<std::vector<InputRect>(wl_output*)> provider) {
|
||||
m_clickShieldExcludeRectsProvider = std::move(provider);
|
||||
}
|
||||
|
||||
void PanelManager::setFocusGrabBarSurfacesProvider(std::function<std::vector<wl_surface*>()> provider) {
|
||||
m_focusGrabBarSurfacesProvider = std::move(provider);
|
||||
}
|
||||
|
||||
void PanelManager::registerPanel(const std::string& id, std::unique_ptr<Panel> content) {
|
||||
m_panels[id] = std::move(content);
|
||||
}
|
||||
@@ -117,6 +132,11 @@ void PanelManager::openPanel(const std::string& panelId, wl_output* output, floa
|
||||
m_activePanel->setContentScale(resolvePanelContentScale(m_config));
|
||||
m_pendingOpenContext = std::string(context);
|
||||
|
||||
// Map shields BEFORE the panel surface is created/committed. Within a
|
||||
// single layer, wlroots stacks surfaces by mapping order — the shields
|
||||
// need to be mapped first so the panel ends up on top of them.
|
||||
activateClickShield();
|
||||
|
||||
const auto panelWidth = static_cast<std::uint32_t>(m_activePanel->preferredWidth());
|
||||
const auto panelHeight = static_cast<std::uint32_t>(m_activePanel->preferredHeight());
|
||||
const auto barConfig = resolvePanelBarConfig(m_config, m_wayland, output);
|
||||
@@ -200,6 +220,7 @@ void PanelManager::openPanel(const std::string& panelId, wl_output* output, floa
|
||||
};
|
||||
|
||||
const auto resetPanelOpenState = [this]() {
|
||||
deactivateOutsideClickHandlers();
|
||||
m_surface.reset();
|
||||
m_layerSurface = nullptr;
|
||||
m_output = nullptr;
|
||||
@@ -353,10 +374,13 @@ void PanelManager::openPanel(const std::string& panelId, wl_output* output, floa
|
||||
.marginRight = 0,
|
||||
.marginBottom = 0,
|
||||
.marginLeft = surfaceX,
|
||||
// Force exclusive keyboard so panels with text inputs (search field, etc.) work without
|
||||
// a prior click — matches the previous subsurface behavior where the bar borrowed
|
||||
// exclusive focus on the panel's behalf.
|
||||
.keyboard = LayerShellKeyboard::Exclusive,
|
||||
// Default: force exclusive keyboard so panels with text inputs (search field, etc.) work
|
||||
// without a prior click — matches the previous subsurface behavior where the bar
|
||||
// borrowed exclusive focus on the panel's behalf. On Hyprland, Exclusive on a layer
|
||||
// surface also grabs the pointer (any click anywhere reports on this surface), which
|
||||
// breaks outside-click dismissal. When the focus_grab protocol is available we drop to
|
||||
// OnDemand and let the grab grant keyboard focus to the panel per the spec.
|
||||
.keyboard = m_focusGrab.available() ? LayerShellKeyboard::OnDemand : LayerShellKeyboard::Exclusive,
|
||||
.defaultWidth = surfaceWidth,
|
||||
.defaultHeight = surfaceHeight,
|
||||
};
|
||||
@@ -378,6 +402,15 @@ void PanelManager::openPanel(const std::string& panelId, wl_output* output, floa
|
||||
applyPanelCompositorBlur();
|
||||
publishAttachedPanelGeometry(m_attachedRevealProgress);
|
||||
m_surface->requestRedraw();
|
||||
// Defer the focus grab to the next tick: Hyprland's focus_grab seems to
|
||||
// need the whitelisted surfaces to actually be mapped, which only
|
||||
// happens after the configure round-trip completes.
|
||||
const std::uint64_t gen = m_destroyGeneration;
|
||||
DeferredCall::callLater([this, gen]() {
|
||||
if (m_destroyGeneration == gen) {
|
||||
activateFocusGrab();
|
||||
}
|
||||
});
|
||||
kLog.debug("panel manager: opened \"{}\" as attached layer-shell", panelId);
|
||||
return;
|
||||
}
|
||||
@@ -429,9 +462,61 @@ void PanelManager::openPanel(const std::string& panelId, wl_output* output, floa
|
||||
m_output = output;
|
||||
m_wlSurface = m_surface->wlSurface();
|
||||
applyPanelCompositorBlur();
|
||||
// Defer the focus grab to the next tick — see attached-path comment above.
|
||||
const std::uint64_t gen = m_destroyGeneration;
|
||||
DeferredCall::callLater([this, gen]() {
|
||||
if (m_destroyGeneration == gen) {
|
||||
activateFocusGrab();
|
||||
}
|
||||
});
|
||||
kLog.debug("panel manager: opened \"{}\"", panelId);
|
||||
}
|
||||
|
||||
void PanelManager::activateClickShield() {
|
||||
if (m_activePanel == nullptr || m_wayland == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Hyprland: prefer the native focus-grab path; the shield can't reliably
|
||||
// exclude bar surfaces there (input region exclusion isn't honored when
|
||||
// keyboard_interactivity is Exclusive, which is what unlocks pointer
|
||||
// delivery). Skip the shield and let activateFocusGrab() handle it later.
|
||||
if (m_focusGrab.available()) {
|
||||
return;
|
||||
}
|
||||
std::vector<wl_output*> outputs;
|
||||
outputs.reserve(m_wayland->outputs().size());
|
||||
for (const auto& wlOutput : m_wayland->outputs()) {
|
||||
if (wlOutput.output != nullptr) {
|
||||
outputs.push_back(wlOutput.output);
|
||||
}
|
||||
}
|
||||
m_clickShield.activate(outputs, m_activePanel->layer(), m_clickShieldExcludeRectsProvider);
|
||||
}
|
||||
|
||||
void PanelManager::activateFocusGrab() {
|
||||
if (!m_focusGrab.available() || m_wlSurface == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Whitelist the panel + every bar surface. Clicks on whitelisted surfaces
|
||||
// pass through normally so bar widgets can toggle the next panel; clicks
|
||||
// anywhere else clear the grab and we close the panel via the `cleared`
|
||||
// event handler. The panel uses OnDemand keyboard mode on Hyprland (the
|
||||
// focus_grab grants keyboard focus to the panel on its own) so the panel
|
||||
// surface no longer grabs the pointer the way Exclusive does.
|
||||
std::vector<wl_surface*> whitelist;
|
||||
whitelist.push_back(m_wlSurface);
|
||||
if (m_focusGrabBarSurfacesProvider) {
|
||||
auto bars = m_focusGrabBarSurfacesProvider();
|
||||
whitelist.insert(whitelist.end(), bars.begin(), bars.end());
|
||||
}
|
||||
m_focusGrab.activate(whitelist);
|
||||
}
|
||||
|
||||
void PanelManager::deactivateOutsideClickHandlers() {
|
||||
m_clickShield.deactivate();
|
||||
m_focusGrab.deactivate();
|
||||
}
|
||||
|
||||
void PanelManager::closePanel() {
|
||||
if (!isOpen() || m_inTransition || m_closing) {
|
||||
return;
|
||||
@@ -439,6 +524,10 @@ void PanelManager::closePanel() {
|
||||
|
||||
kLog.debug("panel manager: closing \"{}\"", m_activePanelId);
|
||||
|
||||
// Drop the outside-click handlers as soon as close starts. During the close
|
||||
// animation we want clicks on apps to behave normally, not re-trigger close.
|
||||
deactivateOutsideClickHandlers();
|
||||
|
||||
// Disable input during close animation
|
||||
m_inputDispatcher.setSceneRoot(nullptr);
|
||||
m_closing = true;
|
||||
@@ -486,6 +575,9 @@ void PanelManager::destroyPanel() {
|
||||
if (m_attachedToBar && m_attachedPanelGeometryCallback && m_output != nullptr) {
|
||||
m_attachedPanelGeometryCallback(m_output, std::nullopt);
|
||||
}
|
||||
// Defensive: closePanel deactivates first, but destroyPanel can also be
|
||||
// reached directly (e.g. when openPanel preempts an open panel).
|
||||
deactivateOutsideClickHandlers();
|
||||
m_animations.cancelAll();
|
||||
m_closing = false;
|
||||
m_pointerInside = false;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "render/scene/node.h"
|
||||
#include "shell/panel/attached_panel_context.h"
|
||||
#include "shell/panel/panel.h"
|
||||
#include "shell/panel/panel_click_shield.h"
|
||||
#include "shell/panel/panel_focus_grab.h"
|
||||
#include "ui/dialogs/layer_popup_host.h"
|
||||
#include "wayland/layer_surface.h"
|
||||
#include "wayland/surface.h"
|
||||
@@ -44,6 +46,13 @@ public:
|
||||
void setOpenSettingsWindowCallback(std::function<void()> callback);
|
||||
void openSettingsWindow();
|
||||
void setAttachedPanelGeometryCallback(std::function<void(wl_output*, std::optional<AttachedPanelGeometry>)> callback);
|
||||
// Callback to query the bar surface rects on a given output, in output-local
|
||||
// coordinates. The click shield's input region excludes these rects so
|
||||
// clicks on bar widgets keep flowing to the bar while a panel is open.
|
||||
void setClickShieldExcludeRectsProvider(std::function<std::vector<InputRect>(wl_output*)> provider);
|
||||
// Callback returning every bar wl_surface. Used to seed the Hyprland focus
|
||||
// grab whitelist so bar widgets keep receiving clicks while a panel is open.
|
||||
void setFocusGrabBarSurfacesProvider(std::function<std::vector<wl_surface*>()> provider);
|
||||
|
||||
void registerPanel(const std::string& id, std::unique_ptr<Panel> content);
|
||||
|
||||
@@ -88,6 +97,13 @@ private:
|
||||
void buildScene(std::uint32_t width, std::uint32_t height);
|
||||
void prepareFrame(bool needsUpdate, bool needsLayout);
|
||||
void destroyPanel();
|
||||
// Called BEFORE the panel surface commits so shields sit below the panel
|
||||
// within the layer-shell layer. No-op when the focus-grab path is in use.
|
||||
void activateClickShield();
|
||||
// Called AFTER the panel surface is mapped so the panel wl_surface is
|
||||
// available for the whitelist. No-op when focus-grab is unavailable.
|
||||
void activateFocusGrab();
|
||||
void deactivateOutsideClickHandlers();
|
||||
void applyAttachedReveal(float progress);
|
||||
void publishAttachedPanelGeometry(float revealProgress);
|
||||
// Restyle the attached-panel decoration nodes (bg fill, drop shadow, contact shadow)
|
||||
@@ -104,6 +120,10 @@ private:
|
||||
RenderContext* m_renderContext = nullptr;
|
||||
std::function<void()> m_openSettingsWindow;
|
||||
std::function<void(wl_output*, std::optional<AttachedPanelGeometry>)> m_attachedPanelGeometryCallback;
|
||||
std::function<std::vector<InputRect>(wl_output*)> m_clickShieldExcludeRectsProvider;
|
||||
std::function<std::vector<wl_surface*>()> m_focusGrabBarSurfacesProvider;
|
||||
PanelClickShield m_clickShield;
|
||||
PanelFocusGrab m_focusGrab;
|
||||
|
||||
std::unique_ptr<Surface> m_surface;
|
||||
LayerSurface* m_layerSurface = nullptr;
|
||||
|
||||
@@ -60,6 +60,11 @@ public:
|
||||
void setClickThrough(bool clickThrough);
|
||||
void setKeyboardInteractivity(LayerShellKeyboard mode);
|
||||
[[nodiscard]] LayerShellKeyboard keyboardInteractivity() const noexcept { return m_config.keyboard; }
|
||||
[[nodiscard]] std::uint32_t anchor() const noexcept { return m_config.anchor; }
|
||||
[[nodiscard]] std::int32_t marginTop() const noexcept { return m_config.marginTop; }
|
||||
[[nodiscard]] std::int32_t marginRight() const noexcept { return m_config.marginRight; }
|
||||
[[nodiscard]] std::int32_t marginBottom() const noexcept { return m_config.marginBottom; }
|
||||
[[nodiscard]] std::int32_t marginLeft() const noexcept { return m_config.marginLeft; }
|
||||
|
||||
static void handleConfigure(void* data, zwlr_layer_surface_v1* layerSurface, std::uint32_t serial,
|
||||
std::uint32_t width, std::uint32_t height);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "ext-session-lock-v1-client-protocol.h"
|
||||
#include "ext-workspace-v1-client-protocol.h"
|
||||
#include "fractional-scale-v1-client-protocol.h"
|
||||
#include "hyprland-focus-grab-v1-client-protocol.h"
|
||||
#include "idle-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "viewporter-client-protocol.h"
|
||||
#include "virtual-keyboard-unstable-v1-client-protocol.h"
|
||||
@@ -46,6 +47,7 @@ namespace {
|
||||
constexpr std::uint32_t kIdleInhibitManagerVersion = 1;
|
||||
constexpr std::uint32_t kExtBackgroundEffectManagerVersion = 1;
|
||||
constexpr std::uint32_t kFractionalScaleManagerVersion = 1;
|
||||
constexpr std::uint32_t kHyprlandFocusGrabManagerVersion = 1;
|
||||
constexpr std::uint32_t kViewporterVersion = 1;
|
||||
constexpr std::uint32_t kOutputVersion = 4;
|
||||
constexpr std::uint32_t kVirtualKeyboardManagerVersion = 1;
|
||||
@@ -535,6 +537,10 @@ ext_background_effect_manager_v1* WaylandConnection::backgroundEffectManager() c
|
||||
wp_fractional_scale_manager_v1* WaylandConnection::fractionalScaleManager() const noexcept {
|
||||
return m_fractionalScaleManager;
|
||||
}
|
||||
|
||||
hyprland_focus_grab_manager_v1* WaylandConnection::hyprlandFocusGrabManager() const noexcept {
|
||||
return m_hyprlandFocusGrabManager;
|
||||
}
|
||||
wp_viewporter* WaylandConnection::viewporter() const noexcept { return m_viewporter; }
|
||||
|
||||
void WaylandConnection::onBackgroundEffectCapabilities(std::uint32_t capabilities) noexcept {
|
||||
@@ -725,6 +731,13 @@ void WaylandConnection::bindGlobal(wl_registry* registry, std::uint32_t name, co
|
||||
return;
|
||||
}
|
||||
|
||||
if (interfaceName == hyprland_focus_grab_manager_v1_interface.name) {
|
||||
const auto bindVersion = std::min(version, kHyprlandFocusGrabManagerVersion);
|
||||
m_hyprlandFocusGrabManager = static_cast<hyprland_focus_grab_manager_v1*>(
|
||||
wl_registry_bind(registry, name, &hyprland_focus_grab_manager_v1_interface, bindVersion));
|
||||
return;
|
||||
}
|
||||
|
||||
if (interfaceName == ext_data_control_manager_v1_interface.name) {
|
||||
if (m_dataControlManager != nullptr && m_dataControlOps != extDataControlOps()) {
|
||||
m_dataControlOps->destroyManager(m_dataControlManager);
|
||||
@@ -861,6 +874,11 @@ void WaylandConnection::cleanup() {
|
||||
m_fractionalScaleManager = nullptr;
|
||||
}
|
||||
|
||||
if (m_hyprlandFocusGrabManager != nullptr) {
|
||||
hyprland_focus_grab_manager_v1_destroy(m_hyprlandFocusGrabManager);
|
||||
m_hyprlandFocusGrabManager = nullptr;
|
||||
}
|
||||
|
||||
if (m_viewporter != nullptr) {
|
||||
wp_viewporter_destroy(m_viewporter);
|
||||
m_viewporter = nullptr;
|
||||
|
||||
@@ -36,6 +36,7 @@ struct zwlr_foreign_toplevel_manager_v1;
|
||||
struct zwlr_foreign_toplevel_handle_v1;
|
||||
struct zdwl_ipc_manager_v2;
|
||||
struct zwp_virtual_keyboard_manager_v1;
|
||||
struct hyprland_focus_grab_manager_v1;
|
||||
struct wp_fractional_scale_manager_v1;
|
||||
struct wp_viewporter;
|
||||
class ClipboardService;
|
||||
@@ -109,6 +110,7 @@ public:
|
||||
[[nodiscard]] bool hasBackgroundEffectBlur() const noexcept;
|
||||
[[nodiscard]] ext_background_effect_manager_v1* backgroundEffectManager() const noexcept;
|
||||
[[nodiscard]] wp_fractional_scale_manager_v1* fractionalScaleManager() const noexcept;
|
||||
[[nodiscard]] hyprland_focus_grab_manager_v1* hyprlandFocusGrabManager() const noexcept;
|
||||
[[nodiscard]] wp_viewporter* viewporter() const noexcept;
|
||||
[[nodiscard]] wl_display* display() const noexcept;
|
||||
[[nodiscard]] wl_compositor* compositor() const noexcept;
|
||||
@@ -190,6 +192,7 @@ private:
|
||||
zwp_idle_inhibit_manager_v1* m_idleInhibitManager = nullptr;
|
||||
ext_background_effect_manager_v1* m_backgroundEffectManager = nullptr;
|
||||
wp_fractional_scale_manager_v1* m_fractionalScaleManager = nullptr;
|
||||
hyprland_focus_grab_manager_v1* m_hyprlandFocusGrabManager = nullptr;
|
||||
wp_viewporter* m_viewporter = nullptr;
|
||||
bool m_backgroundEffectBlurSupported = false;
|
||||
void* m_dataControlManager = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user