Merge pull request #852 from omacom/omarchy-hyprland-titlebars
Add omarchy-hyprland-titlebars
This commit is contained in:
17 files changed
+2139
No files matched your search
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "local",
|
||||
"rebuild_on": [
|
||||
"hyprland"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "BarPassElement.hpp"
|
||||
#include <hyprland/src/render/OpenGL.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#include "barDeco.hpp"
|
||||
|
||||
using namespace Render::GL;
|
||||
|
||||
COmarchyFloatingBarPassElement::COmarchyFloatingBarPassElement(const COmarchyFloatingBarPassElement::SBarData &data_) : data(data_) { ; }
|
||||
|
||||
std::vector<UP<IPassElement>> COmarchyFloatingBarPassElement::draw() {
|
||||
data.deco->renderPass(g_pHyprRenderer->m_renderData.pMonitor.lock(), data.a);
|
||||
return {};
|
||||
}
|
||||
|
||||
bool COmarchyFloatingBarPassElement::needsLiveBlur() {
|
||||
static auto PENABLEBLURGLOBAL = CConfigValue<Config::BOOL>("decoration:blur:enabled");
|
||||
|
||||
CHyprColor color = data.deco->m_bForcedBarColor.value_or(CHyprColor{static_cast<uint64_t>(g_pOmarchyFloatingGlobalState->config.barColor->value())});
|
||||
color.a *= data.a;
|
||||
const bool SHOULDBLUR = g_pOmarchyFloatingGlobalState->config.barBlur->value() && *PENABLEBLURGLOBAL && color.a < 1.F;
|
||||
|
||||
return SHOULDBLUR;
|
||||
}
|
||||
|
||||
std::optional<CBox> COmarchyFloatingBarPassElement::boundingBox() {
|
||||
// Temporary fix: expand the bar bb a bit, otherwise occlusion gets too aggressive.
|
||||
return data.deco->assignedBoxGlobal().translate(-g_pHyprRenderer->m_renderData.pMonitor->m_position).expand(10);
|
||||
}
|
||||
|
||||
bool COmarchyFloatingBarPassElement::needsPrecomputeBlur() { return false; }
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <hyprland/src/render/pass/PassElement.hpp>
|
||||
|
||||
class COmarchyFloatingHyprBar;
|
||||
|
||||
class COmarchyFloatingBarPassElement : public IPassElement {
|
||||
public:
|
||||
struct SBarData {
|
||||
COmarchyFloatingHyprBar *deco = nullptr;
|
||||
float a = 1.F;
|
||||
};
|
||||
|
||||
COmarchyFloatingBarPassElement(const SBarData &data_);
|
||||
virtual ~COmarchyFloatingBarPassElement() = default;
|
||||
|
||||
virtual std::vector<UP<IPassElement>> draw() override;
|
||||
virtual bool needsLiveBlur() override;
|
||||
virtual bool needsPrecomputeBlur() override;
|
||||
virtual std::optional<CBox> boundingBox() override;
|
||||
|
||||
virtual const char *passName() override { return "COmarchyFloatingBarPassElement"; }
|
||||
|
||||
virtual ePassElementType type() override { return EK_CUSTOM; }
|
||||
|
||||
private:
|
||||
SBarData data;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
cmake_minimum_required(VERSION 3.27)
|
||||
|
||||
project(omarchy-hyprland-titlebars
|
||||
DESCRIPTION "Titlebars and edge snapping for Omarchy's floating workspace mode"
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
|
||||
set(SRC main.cpp barDeco.cpp BarPassElement.cpp edgeSnap.cpp)
|
||||
|
||||
# Built as titlebars.so (no lib prefix). Omarchy loads it from the fixed path
|
||||
# /usr/lib/omarchy-hyprland-titlebars/titlebars.so. The Hyprland-facing plugin
|
||||
# name stays "hyprbars" so the hl.plugin.hyprbars Lua namespace is unchanged.
|
||||
add_library(titlebars SHARED ${SRC})
|
||||
set_target_properties(titlebars PROPERTIES PREFIX "")
|
||||
# Hyprland exposes inline globals through its headers, so whole-library hidden
|
||||
# visibility breaks the host API. Our own types/globals have unique OmarchyFloating names.
|
||||
target_compile_options(titlebars PRIVATE -fno-gnu-unique)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(deps REQUIRED IMPORTED_TARGET
|
||||
hyprland
|
||||
libdrm
|
||||
libinput
|
||||
libudev
|
||||
pixman-1
|
||||
wayland-server
|
||||
xkbcommon
|
||||
)
|
||||
target_link_libraries(titlebars PRIVATE rt PkgConfig::deps)
|
||||
|
||||
install(TARGETS titlebars LIBRARY DESTINATION lib/omarchy-hyprland-titlebars)
|
||||
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, Hypr Development
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Maintainer: Ryan Hughes <ryan@heyoodle.com>
|
||||
|
||||
pkgname=omarchy-hyprland-titlebars
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="Titlebars and edge snapping for Omarchy's floating workspaces"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url='https://github.com/omacom/omarchy'
|
||||
license=('BSD-3-Clause' 'MIT')
|
||||
# A Hyprland plugin only loads into the exact Hyprland build it was compiled
|
||||
# against. rebuild_on in .omarchy/package.json rebuilds it with every Hyprland.
|
||||
depends=('hyprland=0.56.2' 'glibc' 'libgcc' 'libstdc++' 'hyprlang' 'hyprutils')
|
||||
makedepends=('cmake' 'pkgconf')
|
||||
options=('!lto' '!debug')
|
||||
source=('CMakeLists.txt'
|
||||
'main.cpp'
|
||||
'barDeco.cpp'
|
||||
'barDeco.hpp'
|
||||
'BarPassElement.cpp'
|
||||
'BarPassElement.hpp'
|
||||
'edgeSnap.cpp'
|
||||
'edgeSnap.hpp'
|
||||
'globals.hpp'
|
||||
'LICENSE'
|
||||
'LICENSE.MIT')
|
||||
sha256sums=('27a292b1b037210774d785a8db17d0e77aceba529a9946b7b555bd6333b9b8e8'
|
||||
'aa6e0c8cb22dc4d26516fa64aac81d1e493cf7a9b99d7a8bd78b82d61b2f2e89'
|
||||
'2d9ab463efa263881c23110f9550b10694366e508418bfe06700dc9e18b31579'
|
||||
'dbdd56902558b36f7e7d01f7b9884530a93f7dda411a2b360acf69255a42c8f2'
|
||||
'e0074f71d8bbb3ae0a24a91a4647c4d0da6ab8c5effa0dce2b5a1bb8c85d085c'
|
||||
'dffbf8ddc768fd111b2a41697fe6547e954c46edc16750913c789d8daa8690cc'
|
||||
'a2b70dd8fc9974a9fca2f4a7db575d14c2c6802c154e4606a3c9e06c1b25a4e6'
|
||||
'2ebf96f7e7508e4485d706c99c186d6fabf14ba5ceb7349fbc3d72529904424e'
|
||||
'831147435d58b3071ad26fd756f9755eaf461793d664ea3cb049270b38228993'
|
||||
'3a1d080d85ce1d722b3bd84d87b74dd2332ba7b68a40a28a7126a86cfca6502a'
|
||||
'717ba1949502290f8e47688ae2e323acd06c8ca47aec9f7596b15f678c1af4a2')
|
||||
|
||||
build() {
|
||||
cmake -S "$srcdir" -B build \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
package() {
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
install -Dm644 LICENSE.MIT "$pkgdir/usr/share/licenses/$pkgname/LICENSE.MIT"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# omarchy-hyprland-titlebars
|
||||
|
||||
A native Hyprland plugin that gives Omarchy's floating workspace mode its window titlebars and edge snapping. It draws theme-neutral titlebars on floating windows and snaps a dragged window to the left or right half of the monitor, or maximizes it at the top edge.
|
||||
|
||||
Omarchy core has no build step, so the plugin ships as its own package, built here alongside Hyprland. Omarchy loads it from its Lua config only while a workspace floats, from a fixed path:
|
||||
|
||||
```lua
|
||||
hl.plugin.load("/usr/lib/omarchy-hyprland-titlebars/titlebars.so")
|
||||
```
|
||||
|
||||
That path is a contract with Omarchy core. The plugin still registers itself with Hyprland as `hyprbars` and keeps the `hl.plugin.hyprbars.add_button()` Lua interface and the `plugin.hyprbars.*` options, because Omarchy's config is written against those names.
|
||||
|
||||
## Origin and license
|
||||
|
||||
This is a fork of Hypr Development's [BSD-licensed hyprbars](https://github.com/hyprwm/hyprland-plugins/tree/main/hyprbars). The source was adapted from the Omarchy Windows 98 theme's `vendor/hyprbars` fork ([theme project](https://github.com/omacom/omarchy-98-theme)), which supplies Hyprland 0.56 Lua API support and safe coexistence with the earlier Windows XP fork. Colors, font, rounding, and actions are supplied by Omarchy's configuration; no theme styling is embedded in the renderer.
|
||||
|
||||
The original hyprbars license is in `LICENSE` (BSD-3-Clause). Omarchy's changes are under the MIT license in `LICENSE.MIT`. The package installs both under `/usr/share/licenses/omarchy-hyprland-titlebars/`.
|
||||
|
||||
## Added options
|
||||
|
||||
These are in addition to the upstream `plugin.hyprbars` titlebar settings.
|
||||
|
||||
| Option | Default | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `edge_snap` | `false` | Enables edge snapping for moving floating windows. |
|
||||
| `edge_threshold` | `24` | Edge activation distance in logical pixels. |
|
||||
| `snap_gap` | `8` | Space around and between half-screen windows, in logical pixels. |
|
||||
| `bar_color_inactive` | transparent | Inactive titlebar color; transparent falls back to `bar_color`. |
|
||||
| `workspace_tag` | empty | When set, titlebars and edge snapping apply only to floating windows with this tag. Removing the tag or tiling the window immediately releases titlebar space. |
|
||||
|
||||
## Window targeting with `%WINDOW%`
|
||||
|
||||
Button `action` commands and `on_double_click` may contain `%WINDOW%`. Every occurrence is replaced synchronously with the clicked titlebar's window address, such as `0x1234abcd`, before the command is spawned. Use it to keep asynchronous actions targeted at the clicked window when focus changes before the command runs:
|
||||
|
||||
```lua
|
||||
hl.plugin.hyprbars.add_button({
|
||||
bg_color = "rgb(f7768e)", fg_color = "rgb(1a1b26)", size = 18, icon = "×",
|
||||
action = [[hyprctl dispatch 'hl.dsp.window.close({ window = "address:%WINDOW%" })']],
|
||||
})
|
||||
```
|
||||
|
||||
The replacement contains only `0x` and hexadecimal digits; no window title or other application-controlled text enters the command.
|
||||
|
||||
## Edge snapping
|
||||
|
||||
Left/right snapping divides the usable monitor area into equal halves, preserving the configured gap and accounting for the titlebar, window borders, monitor scale, and reserved panels. Windows whose minimum size cannot fit a half stay freely placed. The top edge uses Hyprland's native maximized state so decorations remain reachable and normal fullscreen behavior stays available. Dragging a snapped or maximized titlebar restores its floating size under the pointer.
|
||||
|
||||
## `omarchy_snap_preview` event
|
||||
|
||||
The plugin emits socket2 events only when the snap preview changes:
|
||||
|
||||
```text
|
||||
omarchy_snap_preview>>monitorName,left,x,y,width,height
|
||||
omarchy_snap_preview>>monitorName,right,x,y,width,height
|
||||
omarchy_snap_preview>>monitorName,maximize,x,y,width,height
|
||||
omarchy_snap_preview>>,none,0,0,0,0
|
||||
```
|
||||
|
||||
Geometry is global and logical, including titlebar and borders. Omarchy's shell renders the preview. Leaving an edge, releasing the pointer, closing the dragged window, reloading configuration, or unloading the plugin clears it.
|
||||
|
||||
## Hyprland ABI
|
||||
|
||||
A Hyprland plugin is only compatible with the exact Hyprland build whose headers it was compiled against, and Hyprland refuses to load a mismatched build. `rebuild_on` in `.omarchy/package.json` rebuilds this package whenever `hyprland` changes. A new Hyprland version also needs the `hyprland=` dependency in `PKGBUILD` moved to it.
|
||||
|
||||
Plugin-owned C++ symbols have an `OmarchyFloating` prefix and the library is built with `-fno-gnu-unique`, so it can be unloaded and reloaded cleanly alongside other hyprbars forks such as the Windows XP theme's. Do not use whole-library hidden visibility: Hyprland shares inline state through its headers.
|
||||
|
||||
## Layout
|
||||
|
||||
The plugin source sits beside `PKGBUILD` as local sources, since makepkg finds those by file name. `tools/verify.py` is a nested-compositor verifier, with `pointer.c` and the wlr virtual pointer protocol it uses to drive real pointer input; it is not part of the build.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
bin/repo release --package omarchy-hyprland-titlebars
|
||||
```
|
||||
|
||||
Or locally, with the Hyprland headers from the `hyprland` package installed:
|
||||
|
||||
```bash
|
||||
makepkg -f
|
||||
```
|
||||
|
||||
## Verifying
|
||||
|
||||
```bash
|
||||
python3 tools/verify.py src/build/titlebars.so
|
||||
```
|
||||
|
||||
The verifier needs a running Hyprland session with exactly one Hyprland instance, plus `foot`, `grim`, `wayland-scanner`, and a C compiler. It starts a disposable nested Hyprland with its own runtime directory and never loads the candidate library into the parent compositor. It checks repeated load/reload/unload, interleaved with the Windows XP fork when `/usr/lib/omarchy-windows-xp/hyprbars.so` is installed, then exercises real pointer titlebar dragging, both half-screen snaps, top-edge maximize, drag-down restoration, double-click maximize, and preview events. A two-window regression check deliberately delays button and double-click commands and changes focus before they run, to verify that actions keep the clicked window as their target. A final check confirms that `workspace_tag`-scoped titlebars release their space immediately when a window is tiled or untagged.
|
||||
|
||||
Screenshots and logs go to `/tmp/omarchy-hyprland-titlebars-verification` by default; pass `--artifacts DIR` to change it.
|
||||
@@ -0,0 +1,744 @@
|
||||
#include "barDeco.hpp"
|
||||
|
||||
#include <hyprland/src/Compositor.hpp>
|
||||
#include <hyprland/src/desktop/state/FocusState.hpp>
|
||||
#include <hyprland/src/desktop/state/WindowState.hpp>
|
||||
#include <hyprland/src/desktop/state/LayerState.hpp>
|
||||
#include <hyprland/src/desktop/state/ViewHitTester.hpp>
|
||||
#include <hyprland/src/desktop/view/Window.hpp>
|
||||
#include <hyprland/src/desktop/view/LayerSurface.hpp>
|
||||
#include <hyprland/src/helpers/MiscFunctions.hpp>
|
||||
#include <hyprland/src/managers/SeatManager.hpp>
|
||||
#include <hyprland/src/managers/input/InputManager.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#include <hyprland/src/config/ConfigManager.hpp>
|
||||
#include <hyprland/src/config/shared/animation/AnimationTree.hpp>
|
||||
#include <hyprland/src/config/shared/parserUtils/ParserUtils.hpp>
|
||||
#include <hyprland/src/config/supplementary/executor/Executor.hpp>
|
||||
#include <hyprland/src/config/shared/actions/ConfigActions.hpp>
|
||||
#include <hyprland/src/animation/AnimationManager.hpp>
|
||||
#include <hyprland/src/protocols/LayerShell.hpp>
|
||||
#include <hyprland/src/event/EventBus.hpp>
|
||||
#include <hyprland/src/layout/LayoutManager.hpp>
|
||||
#include <hyprland/src/render/OpenGL.hpp>
|
||||
#include <hyprland/src/state/MonitorState.hpp>
|
||||
|
||||
#include "globals.hpp"
|
||||
#include "BarPassElement.hpp"
|
||||
#include "edgeSnap.hpp"
|
||||
#include <hyprland/src/managers/fullscreen/FullscreenController.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
|
||||
using namespace Render::GL;
|
||||
|
||||
static CHyprColor configColor(Config::INTEGER color) { return CHyprColor{static_cast<uint64_t>(color)}; }
|
||||
|
||||
static std::string windowCommand(std::string command, const PHLWINDOW &window) {
|
||||
// Resolve the decoration owner synchronously. An asynchronously spawned
|
||||
// command must not choose its target from whatever happens to be focused.
|
||||
constexpr std::string_view token = "%WINDOW%";
|
||||
const auto address = std::format("0x{:x}", reinterpret_cast<uintptr_t>(window.get()));
|
||||
size_t offset = 0;
|
||||
while ((offset = command.find(token, offset)) != std::string::npos) {
|
||||
command.replace(offset, token.size(), address);
|
||||
offset += address.size();
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
COmarchyFloatingHyprBar::COmarchyFloatingHyprBar(PHLWINDOW pWindow) : IHyprWindowDecoration(pWindow) {
|
||||
m_pWindow = pWindow;
|
||||
|
||||
const auto PMONITOR = pWindow->m_monitor.lock();
|
||||
PMONITOR->m_scheduledRecalc = true;
|
||||
|
||||
// button events
|
||||
m_pMouseButtonCallback =
|
||||
Event::bus()->m_events.input.mouse.button.listen([&](IPointer::SButtonEvent e, Event::SCallbackInfo &info) { onMouseButton(info, e); });
|
||||
m_pTouchDownCallback = Event::bus()->m_events.input.touch.down.listen([&](ITouch::SDownEvent e, Event::SCallbackInfo &info) { onTouchDown(info, e); });
|
||||
m_pTouchUpCallback = Event::bus()->m_events.input.touch.up.listen([&](ITouch::SUpEvent e, Event::SCallbackInfo &info) { onTouchUp(info, e); });
|
||||
|
||||
// move events
|
||||
m_pTouchMoveCallback = Event::bus()->m_events.input.touch.motion.listen([&](ITouch::SMotionEvent e, Event::SCallbackInfo &info) { onTouchMove(info, e); });
|
||||
m_pMouseMoveCallback = Event::bus()->m_events.input.mouse.move.listen([&](Vector2D c, Event::SCallbackInfo &info) { onMouseMove(c); });
|
||||
|
||||
Animation::mgr()->createAnimation(configColor(g_pOmarchyFloatingGlobalState->config.barColor->value()), m_cRealBarColor,
|
||||
Config::animationTree()->getAnimationPropertyConfig("border"), pWindow, AVARDAMAGE_NONE);
|
||||
m_cRealBarColor->setUpdateCallback([&](auto) { damageEntire(); });
|
||||
}
|
||||
|
||||
COmarchyFloatingHyprBar::~COmarchyFloatingHyprBar() { std::erase(g_pOmarchyFloatingGlobalState->bars, m_self); }
|
||||
|
||||
SDecorationPositioningInfo COmarchyFloatingHyprBar::getPositioningInfo() {
|
||||
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
|
||||
const auto ENABLED = g_pOmarchyFloatingGlobalState->config.enabled->value();
|
||||
const auto PRECEDENCE = g_pOmarchyFloatingGlobalState->config.barPrecedenceOverBorder->value();
|
||||
|
||||
SDecorationPositioningInfo info;
|
||||
info.policy = m_hidden ? DECORATION_POSITION_ABSOLUTE : DECORATION_POSITION_STICKY;
|
||||
info.edges = DECORATION_EDGE_TOP;
|
||||
info.priority = PRECEDENCE ? 10005 : 5000;
|
||||
info.reserved = true;
|
||||
info.desiredExtents = {{0, m_hidden || !ENABLED ? 0 : HEIGHT}, {0, 0}};
|
||||
return info;
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::onPositioningReply(const SDecorationPositioningReply &reply) {
|
||||
if (reply.assignedGeometry.size() != m_bAssignedBox.size())
|
||||
m_bWindowSizeChanged = true;
|
||||
|
||||
m_bAssignedBox = reply.assignedGeometry;
|
||||
}
|
||||
|
||||
std::string COmarchyFloatingHyprBar::getDisplayName() { return "Hyprbar"; }
|
||||
|
||||
bool COmarchyFloatingHyprBar::inputIsValid() {
|
||||
if (m_hidden || !g_pOmarchyFloatingGlobalState->config.enabled->value())
|
||||
return false;
|
||||
|
||||
if (g_pSeatManager->m_seatGrab && !g_pSeatManager->m_seatGrab->accepts(m_pWindow->wlSurface()->resource()))
|
||||
return false;
|
||||
|
||||
const auto MOUSE = g_pInputManager->getMouseCoordsInternal();
|
||||
auto PMONITOR = Desktop::focusState()->monitor();
|
||||
|
||||
if (!PMONITOR)
|
||||
return false;
|
||||
|
||||
Desktop::CViewHitTester hitTester{*Desktop::viewState()};
|
||||
|
||||
const auto WINDOWATCURSOR = hitTester.windowAt(MOUSE, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING);
|
||||
|
||||
auto focusState = Desktop::focusState();
|
||||
auto window = focusState->window();
|
||||
|
||||
if (WINDOWATCURSOR != m_pWindow && m_pWindow != window)
|
||||
return false;
|
||||
|
||||
PHLLS foundSurface = nullptr;
|
||||
Vector2D surfaceCoords;
|
||||
|
||||
// Check Top Layer
|
||||
hitTester.layerSurfaceAt(MOUSE, &PMONITOR->m_layerSurfaceLayers[ZWLR_LAYER_SHELL_V1_LAYER_TOP], &surfaceCoords, &foundSurface);
|
||||
if (foundSurface)
|
||||
return false;
|
||||
|
||||
// Check Overlay Layer
|
||||
hitTester.layerSurfaceAt(MOUSE, &PMONITOR->m_layerSurfaceLayers[ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY], &surfaceCoords, &foundSurface);
|
||||
if (foundSurface)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::onMouseButton(Event::SCallbackInfo &info, IPointer::SButtonEvent e) {
|
||||
if (e.button != BTN_LEFT)
|
||||
return;
|
||||
|
||||
if (e.state != WL_POINTER_BUTTON_STATE_PRESSED) {
|
||||
// A titlebar drag can finish over a panel or another surface.
|
||||
if (m_bDraggingThis || m_bCancelledDown || m_bDragPending)
|
||||
handleUpEvent(info);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inputIsValid())
|
||||
return;
|
||||
|
||||
handleDownEvent(info, std::nullopt);
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::onTouchDown(Event::SCallbackInfo &info, ITouch::SDownEvent e) {
|
||||
// Don't do anything if you're already grabbed a window with another finger
|
||||
if (!inputIsValid() || e.touchID != 0)
|
||||
return;
|
||||
|
||||
handleDownEvent(info, e);
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::onTouchUp(Event::SCallbackInfo &info, ITouch::SUpEvent e) {
|
||||
if (!m_bDragPending || !m_bTouchEv || e.touchID != m_touchId)
|
||||
return;
|
||||
|
||||
handleUpEvent(info);
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::onMouseMove(Vector2D coords) {
|
||||
// ensure proper redraws of button icons on hover when using hardware cursors
|
||||
if (g_pOmarchyFloatingGlobalState->config.iconOnHover->value())
|
||||
damageOnButtonHover();
|
||||
|
||||
if (!m_bDragPending || m_bTouchEv || !validMapped(m_pWindow) || m_touchId != 0)
|
||||
return;
|
||||
|
||||
m_bDragPending = false;
|
||||
handleMovement();
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::onTouchMove(Event::SCallbackInfo &info, ITouch::SMotionEvent e) {
|
||||
if (!m_bDragPending || !m_bTouchEv || !validMapped(m_pWindow) || e.touchID != m_touchId)
|
||||
return;
|
||||
|
||||
auto PMONITOR = m_pWindow->m_monitor.lock();
|
||||
PMONITOR = PMONITOR ? PMONITOR : Desktop::focusState()->monitor();
|
||||
const auto COORDS = Vector2D(PMONITOR->m_position.x + e.pos.x * PMONITOR->m_size.x, PMONITOR->m_position.y + e.pos.y * PMONITOR->m_size.y);
|
||||
|
||||
if (!m_bDraggingThis) {
|
||||
// Initial setup for dragging a window.
|
||||
g_pKeybindManager->m_dispatchers["setfloating"]("activewindow");
|
||||
g_pKeybindManager->m_dispatchers["resizewindowpixel"]("exact 50% 50%,activewindow");
|
||||
// pin it so you can change workspaces while dragging a window
|
||||
g_pKeybindManager->m_dispatchers["pin"]("activewindow");
|
||||
}
|
||||
g_pKeybindManager->m_dispatchers["movewindowpixel"](std::format("exact {} {},activewindow", (int)(COORDS.x - (assignedBoxGlobal().w / 2)), (int)COORDS.y));
|
||||
m_bDraggingThis = true;
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::handleDownEvent(Event::SCallbackInfo &info, std::optional<ITouch::SDownEvent> touchEvent) {
|
||||
m_bTouchEv = touchEvent.has_value();
|
||||
if (m_bTouchEv)
|
||||
m_touchId = touchEvent.value().touchID;
|
||||
|
||||
const auto PWINDOW = m_pWindow.lock();
|
||||
|
||||
auto COORDS = cursorRelativeToBar();
|
||||
if (m_bTouchEv) {
|
||||
ITouch::SDownEvent e = touchEvent.value();
|
||||
PHLMONITOR PMONITOR = nullptr;
|
||||
for (auto &m : State::monitorState()->monitors()) {
|
||||
if (m->m_name == (!e.device->m_boundOutput.empty() ? e.device->m_boundOutput : "")) {
|
||||
PMONITOR = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
PMONITOR = PMONITOR ? PMONITOR : Desktop::focusState()->monitor();
|
||||
COORDS = Vector2D(PMONITOR->m_position.x + e.pos.x * PMONITOR->m_size.x, PMONITOR->m_position.y + e.pos.y * PMONITOR->m_size.y) - assignedBoxGlobal().pos();
|
||||
}
|
||||
|
||||
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
|
||||
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
|
||||
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
|
||||
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
|
||||
const auto ON_DOUBLE_CLICK = g_pOmarchyFloatingGlobalState->config.onDoubleClick->value();
|
||||
|
||||
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
|
||||
|
||||
if (!VECINRECT(COORDS, 0, 0, assignedBoxGlobal().w, HEIGHT - 1)) {
|
||||
|
||||
if (m_bDraggingThis) {
|
||||
if (m_bTouchEv)
|
||||
g_pKeybindManager->m_dispatchers["settiled"]("activewindow");
|
||||
g_pKeybindManager->m_dispatchers["mouse"]("0movewindow");
|
||||
Log::logger->log(Log::DEBUG, "[hyprbars] Dragging ended on {:x}", (uintptr_t)PWINDOW.get());
|
||||
}
|
||||
|
||||
m_bDraggingThis = false;
|
||||
m_bDragPending = false;
|
||||
m_bTouchEv = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Desktop::focusState()->window() != PWINDOW)
|
||||
Desktop::focusState()->fullWindowFocus(PWINDOW, Desktop::FOCUS_REASON_CLICK);
|
||||
|
||||
if (PWINDOW->m_isFloating)
|
||||
Desktop::windowState()->raise(PWINDOW);
|
||||
|
||||
info.cancelled = true;
|
||||
m_bCancelledDown = true;
|
||||
|
||||
if (doButtonPress(BARPADDING, BARBUTTONPADDING, HEIGHT, COORDS, BUTTONSRIGHT))
|
||||
return;
|
||||
|
||||
if (!ON_DOUBLE_CLICK.empty() &&
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(Time::steadyNow() - m_lastMouseDown).count() < 400 /* Arbitrary delay I found suitable */) {
|
||||
Config::Supplementary::executor()->spawn(windowCommand(ON_DOUBLE_CLICK, PWINDOW));
|
||||
m_bDragPending = false;
|
||||
} else {
|
||||
m_lastMouseDown = Time::steadyNow();
|
||||
m_bDragPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::handleUpEvent(Event::SCallbackInfo &info) {
|
||||
if (m_pWindow.lock() != Desktop::focusState()->window())
|
||||
return;
|
||||
|
||||
if (m_bCancelledDown)
|
||||
info.cancelled = true;
|
||||
|
||||
m_bCancelledDown = false;
|
||||
|
||||
if (m_bDraggingThis) {
|
||||
g_pKeybindManager->changeMouseBindMode(MBIND_INVALID);
|
||||
m_bDraggingThis = false;
|
||||
if (m_bTouchEv)
|
||||
(void)Config::Actions::floatWindow(Config::Actions::eTogglableAction::TOGGLE_ACTION_DISABLE);
|
||||
|
||||
Log::logger->log(Log::DEBUG, "[hyprbars] Dragging ended on {:x}", (uintptr_t)m_pWindow.lock().get());
|
||||
}
|
||||
|
||||
m_bDragPending = false;
|
||||
m_bTouchEv = false;
|
||||
m_touchId = 0;
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::handleMovement() {
|
||||
const auto window = m_pWindow.lock();
|
||||
const auto cursor = g_pInputManager->getMouseCoordsInternal();
|
||||
const auto bar = assignedBoxGlobal();
|
||||
const auto anchor = std::clamp((cursor.x - bar.x) / std::max(1.0, bar.w), 0.0, 1.0);
|
||||
const auto offset = std::clamp(cursor.y - bar.y, 0.0, bar.h);
|
||||
const bool maximized = Fullscreen::controller()->isFullscreen(window, Fullscreen::FSMODE_MAXIMIZED);
|
||||
if (maximized)
|
||||
Fullscreen::controller()->setFullscreenMode(window, Fullscreen::FSMODE_NONE);
|
||||
|
||||
const bool restored = omarchyFloatingRestoreSnap(window);
|
||||
if (maximized || restored) {
|
||||
auto box = window->layoutTarget()->position();
|
||||
const auto reserved = window->getFullWindowReservedArea();
|
||||
box.x = cursor.x - box.w * anchor;
|
||||
box.y = cursor.y + reserved.topLeft.y - offset;
|
||||
g_layoutManager->setTargetGeom(box, window->layoutTarget());
|
||||
window->layoutTarget()->warpPositionSize();
|
||||
}
|
||||
g_pKeybindManager->changeMouseBindMode(MBIND_MOVE);
|
||||
m_bDraggingThis = true;
|
||||
Log::logger->log(Log::DEBUG, "[hyprbars] Dragging initiated on {:x}", (uintptr_t)m_pWindow.lock().get());
|
||||
return;
|
||||
}
|
||||
|
||||
bool COmarchyFloatingHyprBar::doButtonPress(Config::INTEGER barPadding, Config::INTEGER barButtonPadding, Config::INTEGER barHeight, Vector2D COORDS,
|
||||
const bool BUTTONSRIGHT) {
|
||||
// check if on a button
|
||||
float offset = barPadding;
|
||||
|
||||
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
|
||||
const auto BARBUF = Vector2D{(int)assignedBoxGlobal().w, barHeight};
|
||||
Vector2D currentPos = Vector2D{(BUTTONSRIGHT ? BARBUF.x - barButtonPadding - b.size - offset : offset), (BARBUF.y - b.size) / 2.0}.floor();
|
||||
|
||||
if (VECINRECT(COORDS, currentPos.x, currentPos.y, currentPos.x + b.size + barButtonPadding, currentPos.y + b.size)) {
|
||||
// hit on close
|
||||
g_pKeybindManager->m_dispatchers["exec"](windowCommand(b.cmd, m_pWindow.lock()));
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += barButtonPadding + b.size;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::renderBarTitle(const Vector2D &bufferSize, const float scale) {
|
||||
const auto COLORVAL = g_pOmarchyFloatingGlobalState->config.textColor->value();
|
||||
const auto SIZE = g_pOmarchyFloatingGlobalState->config.barTextSize->value();
|
||||
const auto WEIGHT = g_pOmarchyFloatingGlobalState->config.barTextWeight->value();
|
||||
const auto FONT = g_pOmarchyFloatingGlobalState->config.barTextFont->value();
|
||||
const auto ALIGN = g_pOmarchyFloatingGlobalState->config.barTextAlign->value();
|
||||
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
|
||||
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
|
||||
|
||||
float buttonSizes = BARBUTTONPADDING;
|
||||
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
|
||||
buttonSizes += b.size + BARBUTTONPADDING;
|
||||
}
|
||||
|
||||
const int scaledSize = std::round(SIZE * scale);
|
||||
const auto scaledButtonsSize = buttonSizes * scale;
|
||||
const auto scaledBarPadding = BARPADDING * scale;
|
||||
const int paddingTotal = scaledBarPadding * 2 + scaledButtonsSize + (ALIGN != "left" ? scaledButtonsSize : 0);
|
||||
const int maxWidth = std::clamp(static_cast<int>(bufferSize.x - paddingTotal), 0, INT_MAX);
|
||||
|
||||
if (m_szLastTitle.empty() || maxWidth < 1) {
|
||||
m_pTextTex = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
const CHyprColor COLOR = m_bForcedTitleColor.value_or(configColor(COLORVAL));
|
||||
m_pTextTex = g_pHyprRenderer->renderText(m_szLastTitle, COLOR, scaledSize, false, FONT, maxWidth, WEIGHT.m_value);
|
||||
}
|
||||
|
||||
size_t COmarchyFloatingHyprBar::getVisibleButtonCount(Config::INTEGER barButtonPadding, Config::INTEGER barPadding, const Vector2D &bufferSize,
|
||||
const float scale) {
|
||||
float availableSpace = bufferSize.x - barPadding * scale * 2;
|
||||
size_t count = 0;
|
||||
|
||||
for (const auto &button : g_pOmarchyFloatingGlobalState->buttons) {
|
||||
const float buttonSpace = (button.size + barButtonPadding) * scale;
|
||||
if (availableSpace >= buttonSpace) {
|
||||
count++;
|
||||
availableSpace -= buttonSpace;
|
||||
} else
|
||||
break;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::renderBarButtons(CBox *barBox, const float scale, const float a) {
|
||||
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
|
||||
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
|
||||
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
|
||||
const auto INACTIVECOLOR = g_pOmarchyFloatingGlobalState->config.inactiveButtonColor->value();
|
||||
const auto BUTTONROUNDING = g_pOmarchyFloatingGlobalState->config.buttonRounding->value();
|
||||
const auto BUTTONBORDERSIZE = g_pOmarchyFloatingGlobalState->config.buttonBorderSize->value();
|
||||
const auto BUTTONBORDERCOLOR = g_pOmarchyFloatingGlobalState->config.buttonBorderColor->value();
|
||||
|
||||
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
|
||||
const auto visibleCount = getVisibleButtonCount(BARBUTTONPADDING, BARPADDING, Vector2D{barBox->w, barBox->h}, scale);
|
||||
const bool INVALIDATEICONS = m_bButtonsDirty || m_bWindowSizeChanged;
|
||||
|
||||
int offset = BARPADDING * scale;
|
||||
for (size_t i = 0; i < visibleCount; ++i) {
|
||||
auto &button = g_pOmarchyFloatingGlobalState->buttons[i];
|
||||
const auto scaledButtonSize = button.size * scale;
|
||||
const auto scaledButtonsPad = BARBUTTONPADDING * scale;
|
||||
|
||||
auto color = button.bgcol;
|
||||
|
||||
if (INACTIVECOLOR > 0) {
|
||||
color = m_bWindowHasFocus ? color : configColor(INACTIVECOLOR);
|
||||
if (INVALIDATEICONS && button.userfg && button.iconTex)
|
||||
button.iconTex = nullptr;
|
||||
}
|
||||
|
||||
color.a *= a;
|
||||
|
||||
CBox buttonBox = {barBox->x + (BUTTONSRIGHT ? barBox->w - offset - scaledButtonSize : offset), barBox->y + (barBox->h - scaledButtonSize) / 2.0,
|
||||
scaledButtonSize, scaledButtonSize};
|
||||
buttonBox.round();
|
||||
|
||||
const int maxRounding = static_cast<int>(std::round(scaledButtonSize / 2.0));
|
||||
const int rounding = BUTTONROUNDING < 0 ? maxRounding : std::clamp(static_cast<int>(std::round(BUTTONROUNDING * scale)), 0, maxRounding);
|
||||
const int borderSize = std::clamp(static_cast<int>(std::round(BUTTONBORDERSIZE * scale)), 0, maxRounding);
|
||||
|
||||
if (borderSize > 0) {
|
||||
auto borderColor = configColor(BUTTONBORDERCOLOR);
|
||||
borderColor.a *= a;
|
||||
g_pHyprOpenGL->renderRect(buttonBox, borderColor, {.round = rounding});
|
||||
auto face = buttonBox.copy().expand(-borderSize);
|
||||
g_pHyprOpenGL->renderRect(face, color, {.round = std::max(0, rounding - borderSize)});
|
||||
} else
|
||||
g_pHyprOpenGL->renderRect(buttonBox, color, {.round = rounding});
|
||||
|
||||
offset += scaledButtonsPad + scaledButtonSize;
|
||||
}
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::renderBarButtonsText(CBox *barBox, const float scale, const float a) {
|
||||
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
|
||||
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
|
||||
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
|
||||
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
|
||||
const auto ICONONHOVER = g_pOmarchyFloatingGlobalState->config.iconOnHover->value();
|
||||
|
||||
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
|
||||
const auto visibleCount = getVisibleButtonCount(BARBUTTONPADDING, BARPADDING, Vector2D{barBox->w, barBox->h}, scale);
|
||||
const auto COORDS = cursorRelativeToBar();
|
||||
|
||||
int offset = BARPADDING * scale;
|
||||
float noScaleOffset = BARPADDING;
|
||||
|
||||
for (size_t i = 0; i < visibleCount; ++i) {
|
||||
auto &button = g_pOmarchyFloatingGlobalState->buttons[i];
|
||||
const auto scaledButtonSize = button.size * scale;
|
||||
const auto scaledButtonsPad = BARBUTTONPADDING * scale;
|
||||
|
||||
// check if hovering here
|
||||
const auto BARBUF = Vector2D{(int)assignedBoxGlobal().w, HEIGHT};
|
||||
Vector2D currentPos =
|
||||
Vector2D{(BUTTONSRIGHT ? BARBUF.x - BARBUTTONPADDING - button.size - noScaleOffset : noScaleOffset), (BARBUF.y - button.size) / 2.0}.floor();
|
||||
bool hovering = VECINRECT(COORDS, currentPos.x, currentPos.y, currentPos.x + button.size + BARBUTTONPADDING, currentPos.y + button.size);
|
||||
noScaleOffset += BARBUTTONPADDING + button.size;
|
||||
|
||||
if ((!button.iconTex || button.iconTex->m_texID == 0) && !button.icon.empty()) {
|
||||
// render icon
|
||||
auto fgcol = button.userfg ? button.fgcol : (button.bgcol.r + button.bgcol.g + button.bgcol.b < 1) ? CHyprColor(0xFFFFFFFF) : CHyprColor(0xFF000000);
|
||||
|
||||
button.iconTex = g_pHyprRenderer->renderText(button.icon, fgcol, std::round(button.size * 0.62 * scale), false, "sans", scaledButtonSize);
|
||||
}
|
||||
|
||||
if (!button.iconTex || button.iconTex->m_texID == 0)
|
||||
continue;
|
||||
|
||||
const auto iconX =
|
||||
barBox->x + (BUTTONSRIGHT ? barBox->width - offset - scaledButtonSize / 2.0 : offset + scaledButtonSize / 2.0) - button.iconTex->m_size.x / 2.0;
|
||||
const auto iconY = barBox->y + barBox->height / 2.0 - button.iconTex->m_size.y / 2.0;
|
||||
CBox pos = {iconX, iconY, button.iconTex->m_size.x, button.iconTex->m_size.y};
|
||||
|
||||
if (!ICONONHOVER || (ICONONHOVER && m_iButtonHoverState > 0))
|
||||
g_pHyprOpenGL->renderTexture(button.iconTex, pos, {.a = a});
|
||||
offset += scaledButtonsPad + scaledButtonSize;
|
||||
|
||||
bool currentBit = (m_iButtonHoverState & (1 << i)) != 0;
|
||||
if (hovering != currentBit) {
|
||||
m_iButtonHoverState ^= (1 << i);
|
||||
// damage to get rid of some artifacts when icons are "hidden"
|
||||
damageEntire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::draw(PHLMONITOR pMonitor, const float &a) {
|
||||
const auto ENABLED = g_pOmarchyFloatingGlobalState->config.enabled->value();
|
||||
|
||||
if (m_bLastEnabledState != ENABLED) {
|
||||
m_bLastEnabledState = ENABLED;
|
||||
g_pDecorationPositioner->repositionDeco(this);
|
||||
}
|
||||
|
||||
if (m_hidden || !validMapped(m_pWindow) || !ENABLED)
|
||||
return;
|
||||
|
||||
const auto PWINDOW = m_pWindow.lock();
|
||||
|
||||
if (!PWINDOW->m_ruleApplicator->decorate().valueOrDefault())
|
||||
return;
|
||||
|
||||
auto data = COmarchyFloatingBarPassElement::SBarData{this, a};
|
||||
g_pHyprRenderer->m_renderPass.add(makeUnique<COmarchyFloatingBarPassElement>(data));
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::renderPass(PHLMONITOR pMonitor, const float &a) {
|
||||
const auto PWINDOW = m_pWindow.lock();
|
||||
|
||||
static auto PENABLEBLURGLOBAL = CConfigValue<Config::BOOL>("decoration:blur:enabled");
|
||||
const auto BARCOLOR = g_pOmarchyFloatingGlobalState->config.barColor->value();
|
||||
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
|
||||
const auto PRECEDENCE = g_pOmarchyFloatingGlobalState->config.barPrecedenceOverBorder->value();
|
||||
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
|
||||
const auto ENABLETITLE = g_pOmarchyFloatingGlobalState->config.barTitleEnabled->value();
|
||||
const auto ENABLEBLUR = g_pOmarchyFloatingGlobalState->config.barBlur->value();
|
||||
const auto INACTIVECOLOR = g_pOmarchyFloatingGlobalState->config.inactiveButtonColor->value();
|
||||
|
||||
if (INACTIVECOLOR > 0) {
|
||||
bool currentWindowFocus = PWINDOW == Desktop::focusState()->window();
|
||||
if (currentWindowFocus != m_bWindowHasFocus) {
|
||||
m_bWindowHasFocus = currentWindowFocus;
|
||||
m_bButtonsDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto INACTIVEBAR = g_pOmarchyFloatingGlobalState->config.inactiveBarColor->value();
|
||||
const auto baseColor = PWINDOW != Desktop::focusState()->window() && INACTIVEBAR != 0 ? INACTIVEBAR : BARCOLOR;
|
||||
const CHyprColor DEST_COLOR = m_bForcedBarColor.value_or(configColor(baseColor));
|
||||
if (DEST_COLOR != m_cRealBarColor->goal())
|
||||
*m_cRealBarColor = DEST_COLOR;
|
||||
|
||||
CHyprColor color = m_cRealBarColor->value();
|
||||
|
||||
color.a *= a;
|
||||
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
|
||||
const bool SHOULDBLUR = ENABLEBLUR && *PENABLEBLURGLOBAL && color.a < 1.F;
|
||||
|
||||
if (HEIGHT < 1) {
|
||||
m_iLastHeight = HEIGHT;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto PWORKSPACE = PWINDOW->m_workspace;
|
||||
const auto WORKSPACEOFFSET = PWORKSPACE && !PWINDOW->m_pinned ? PWORKSPACE->m_renderOffset->value() : Vector2D();
|
||||
|
||||
const auto ROUNDING = PWINDOW->rounding() + (PRECEDENCE ? 0 : PWINDOW->getRealBorderSize());
|
||||
|
||||
const auto scaledRounding = ROUNDING > 0 ? ROUNDING * pMonitor->m_scale - 2 /* idk why but otherwise it looks bad due to the gaps */ : 0;
|
||||
|
||||
m_seExtents = {{0, HEIGHT}, {}};
|
||||
|
||||
const auto DECOBOX = assignedBoxGlobal();
|
||||
|
||||
const auto BARBUF = DECOBOX.size() * pMonitor->m_scale;
|
||||
|
||||
CBox titleBarBox = {DECOBOX.x - pMonitor->m_position.x, DECOBOX.y - pMonitor->m_position.y, DECOBOX.w,
|
||||
DECOBOX.h + ROUNDING * 3 /* to fill the bottom cuz we can't disable rounding there */};
|
||||
|
||||
titleBarBox.translate(PWINDOW->m_floatingOffset).scale(pMonitor->m_scale).round();
|
||||
|
||||
if (titleBarBox.w < 1 || titleBarBox.h < 1)
|
||||
return;
|
||||
|
||||
g_pHyprOpenGL->scissor(titleBarBox);
|
||||
|
||||
if (ROUNDING) {
|
||||
// the +1 is a shit garbage temp fix until renderRect supports an alpha matte
|
||||
CBox windowBox = {PWINDOW->position(Desktop::View::IGeometric::GEOMETRIC_CURRENT).x + PWINDOW->m_floatingOffset.x - pMonitor->m_position.x + 1,
|
||||
PWINDOW->position(Desktop::View::IGeometric::GEOMETRIC_CURRENT).y + PWINDOW->m_floatingOffset.y - pMonitor->m_position.y + 1,
|
||||
PWINDOW->size(Desktop::View::IGeometric::GEOMETRIC_CURRENT).x - 2, PWINDOW->size(Desktop::View::IGeometric::GEOMETRIC_CURRENT).y - 2};
|
||||
|
||||
if (windowBox.w < 1 || windowBox.h < 1)
|
||||
return;
|
||||
|
||||
glClearStencil(0);
|
||||
glClear(GL_STENCIL_BUFFER_BIT);
|
||||
|
||||
g_pHyprOpenGL->setCapStatus(GL_STENCIL_TEST, true);
|
||||
|
||||
glStencilFunc(GL_ALWAYS, 1, -1);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||
|
||||
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
|
||||
|
||||
windowBox.translate(WORKSPACEOFFSET).scale(pMonitor->m_scale).round();
|
||||
g_pHyprOpenGL->renderRect(windowBox, CHyprColor(0, 0, 0, 0), {.round = scaledRounding, .roundingPower = m_pWindow->roundingPower()});
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
glStencilFunc(GL_NOTEQUAL, 1, -1);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||
}
|
||||
|
||||
g_pHyprOpenGL->renderRect(titleBarBox, color, {.round = static_cast<int>(scaledRounding), .roundingPower = PWINDOW->roundingPower()});
|
||||
|
||||
// render title
|
||||
if (ENABLETITLE && (m_szLastTitle != PWINDOW->m_title || m_bWindowSizeChanged || !m_pTextTex || m_pTextTex->m_texID == 0 || m_bTitleColorChanged)) {
|
||||
m_szLastTitle = PWINDOW->m_title;
|
||||
renderBarTitle(BARBUF, pMonitor->m_scale);
|
||||
}
|
||||
|
||||
if (ROUNDING) {
|
||||
// cleanup stencil
|
||||
glClearStencil(0);
|
||||
glClear(GL_STENCIL_BUFFER_BIT);
|
||||
g_pHyprOpenGL->setCapStatus(GL_STENCIL_TEST, false);
|
||||
glStencilMask(-1);
|
||||
glStencilFunc(GL_ALWAYS, 1, 0xFF);
|
||||
}
|
||||
|
||||
CBox textBox = {titleBarBox.x, titleBarBox.y, (int)BARBUF.x, (int)BARBUF.y};
|
||||
if (ENABLETITLE && m_pTextTex) {
|
||||
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
|
||||
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
|
||||
const auto ALIGN = g_pOmarchyFloatingGlobalState->config.barTextAlign->value();
|
||||
|
||||
float buttonSizes = BARBUTTONPADDING;
|
||||
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
|
||||
buttonSizes += b.size + BARBUTTONPADDING;
|
||||
}
|
||||
|
||||
const auto scaledBorderSize = PWINDOW->getRealBorderSize() * pMonitor->m_scale;
|
||||
const auto scaledButtonsSize = buttonSizes * pMonitor->m_scale;
|
||||
const auto scaledBarPadding = BARPADDING * pMonitor->m_scale;
|
||||
const auto xOffset = ALIGN == "left" ? std::round(scaledBarPadding + (BUTTONSRIGHT ? 0 : scaledButtonsSize))
|
||||
: std::round(((BARBUF.x - scaledBorderSize) / 2.0 - m_pTextTex->m_size.x / 2.0));
|
||||
const auto yOffset = std::round((BARBUF.y - m_pTextTex->m_size.y) / 2.0);
|
||||
CBox titleBox = {textBox.x + xOffset, textBox.y + yOffset, m_pTextTex->m_size.x, m_pTextTex->m_size.y};
|
||||
|
||||
g_pHyprOpenGL->renderTexture(m_pTextTex, titleBox, {.a = a});
|
||||
}
|
||||
|
||||
renderBarButtons(&textBox, pMonitor->m_scale, a);
|
||||
m_bButtonsDirty = false;
|
||||
|
||||
g_pHyprOpenGL->scissor(nullptr);
|
||||
|
||||
renderBarButtonsText(&textBox, pMonitor->m_scale, a);
|
||||
|
||||
m_bWindowSizeChanged = false;
|
||||
m_bTitleColorChanged = false;
|
||||
|
||||
// dynamic updates change the extents
|
||||
if (m_iLastHeight != HEIGHT) {
|
||||
PWINDOW->layoutTarget()->recalc();
|
||||
m_iLastHeight = HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
eDecorationType COmarchyFloatingHyprBar::getDecorationType() { return DECORATION_CUSTOM; }
|
||||
|
||||
void COmarchyFloatingHyprBar::updateWindow(PHLWINDOW pWindow) {
|
||||
updateRules();
|
||||
damageEntire();
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::onConfigReloaded() {
|
||||
m_bButtonsDirty = true;
|
||||
m_bTitleColorChanged = true;
|
||||
m_pTextTex = nullptr;
|
||||
|
||||
g_pDecorationPositioner->repositionDeco(this);
|
||||
damageEntire();
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::damageEntire() { g_pHyprRenderer->damageBox(assignedBoxGlobal()); }
|
||||
|
||||
Vector2D COmarchyFloatingHyprBar::cursorRelativeToBar() { return g_pInputManager->getMouseCoordsInternal() - assignedBoxGlobal().pos(); }
|
||||
|
||||
eDecorationLayer COmarchyFloatingHyprBar::getDecorationLayer() { return DECORATION_LAYER_UNDER; }
|
||||
|
||||
uint64_t COmarchyFloatingHyprBar::getDecorationFlags() {
|
||||
return DECORATION_ALLOWS_MOUSE_INPUT | (g_pOmarchyFloatingGlobalState->config.barPartOfWindow->value() ? DECORATION_PART_OF_MAIN_WINDOW : 0);
|
||||
}
|
||||
|
||||
CBox COmarchyFloatingHyprBar::assignedBoxGlobal() {
|
||||
if (!validMapped(m_pWindow))
|
||||
return {};
|
||||
|
||||
CBox box = m_bAssignedBox;
|
||||
box.translate(g_pDecorationPositioner->getEdgeDefinedPoint(DECORATION_EDGE_TOP, m_pWindow.lock()));
|
||||
|
||||
const auto PWORKSPACE = m_pWindow->m_workspace;
|
||||
const auto WORKSPACEOFFSET = PWORKSPACE && !m_pWindow->m_pinned ? PWORKSPACE->m_renderOffset->value() : Vector2D();
|
||||
|
||||
return box.translate(WORKSPACEOFFSET);
|
||||
}
|
||||
|
||||
PHLWINDOW COmarchyFloatingHyprBar::getOwner() { return m_pWindow.lock(); }
|
||||
|
||||
void COmarchyFloatingHyprBar::updateRules() {
|
||||
const auto PWINDOW = m_pWindow.lock();
|
||||
if (!PWINDOW)
|
||||
return;
|
||||
auto prevHidden = m_hidden;
|
||||
auto prevForcedTitleColor = m_bForcedTitleColor;
|
||||
|
||||
m_bForcedBarColor = std::nullopt;
|
||||
m_bForcedTitleColor = std::nullopt;
|
||||
m_hidden = false;
|
||||
|
||||
if (PWINDOW->m_ruleApplicator->m_otherProps.props.contains(g_pOmarchyFloatingGlobalState->nobarRuleIdx))
|
||||
m_hidden = truthy(PWINDOW->m_ruleApplicator->m_otherProps.props.at(g_pOmarchyFloatingGlobalState->nobarRuleIdx)->effect);
|
||||
const auto workspaceTag = g_pOmarchyFloatingGlobalState->config.workspaceTag->value();
|
||||
if (!workspaceTag.empty())
|
||||
m_hidden = m_hidden || !PWINDOW->m_isFloating || !PWINDOW->m_ruleApplicator->m_tagKeeper.isTagged(workspaceTag);
|
||||
if (PWINDOW->m_ruleApplicator->m_otherProps.props.contains(g_pOmarchyFloatingGlobalState->barColorRuleIdx))
|
||||
m_bForcedBarColor = CHyprColor(
|
||||
Config::ParserUtils::parseColor(PWINDOW->m_ruleApplicator->m_otherProps.props.at(g_pOmarchyFloatingGlobalState->barColorRuleIdx)->effect).value_or(0));
|
||||
if (PWINDOW->m_ruleApplicator->m_otherProps.props.contains(g_pOmarchyFloatingGlobalState->titleColorRuleIdx))
|
||||
m_bForcedTitleColor =
|
||||
CHyprColor(Config::ParserUtils::parseColor(PWINDOW->m_ruleApplicator->m_otherProps.props.at(g_pOmarchyFloatingGlobalState->titleColorRuleIdx)->effect)
|
||||
.value_or(0));
|
||||
|
||||
if (prevHidden != m_hidden) {
|
||||
g_pDecorationPositioner->repositionDeco(this);
|
||||
if (const auto monitor = PWINDOW->m_monitor.lock())
|
||||
monitor->m_scheduledRecalc = true;
|
||||
}
|
||||
if (prevForcedTitleColor != m_bForcedTitleColor)
|
||||
m_bTitleColorChanged = true;
|
||||
}
|
||||
|
||||
void COmarchyFloatingHyprBar::damageOnButtonHover() {
|
||||
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
|
||||
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
|
||||
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
|
||||
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
|
||||
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
|
||||
|
||||
float offset = BARPADDING;
|
||||
|
||||
const auto COORDS = cursorRelativeToBar();
|
||||
|
||||
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
|
||||
const auto BARBUF = Vector2D{(int)assignedBoxGlobal().w, HEIGHT};
|
||||
Vector2D currentPos = Vector2D{(BUTTONSRIGHT ? BARBUF.x - BARBUTTONPADDING - b.size - offset : offset), (BARBUF.y - b.size) / 2.0}.floor();
|
||||
|
||||
bool hover = VECINRECT(COORDS, currentPos.x, currentPos.y, currentPos.x + b.size + BARBUTTONPADDING, currentPos.y + b.size);
|
||||
|
||||
if (hover != m_bButtonHovered) {
|
||||
m_bButtonHovered = hover;
|
||||
damageEntire();
|
||||
}
|
||||
|
||||
offset += BARBUTTONPADDING + b.size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include <hyprland/src/render/decorations/IHyprWindowDecoration.hpp>
|
||||
#include <hyprland/src/render/OpenGL.hpp>
|
||||
#include <hyprland/src/render/gl/GLTexture.hpp>
|
||||
#include <hyprland/src/devices/IPointer.hpp>
|
||||
#include <hyprland/src/devices/ITouch.hpp>
|
||||
#include <hyprland/src/desktop/rule/windowRule/WindowRule.hpp>
|
||||
#include <hyprland/src/helpers/AnimatedVariable.hpp>
|
||||
#include <hyprland/src/helpers/time/Time.hpp>
|
||||
#include <hyprland/src/helpers/signal/Signal.hpp>
|
||||
#include "globals.hpp"
|
||||
|
||||
#define private public
|
||||
#include <hyprland/src/managers/input/InputManager.hpp>
|
||||
#undef private
|
||||
|
||||
namespace Event {
|
||||
struct SCallbackInfo;
|
||||
}
|
||||
|
||||
class COmarchyFloatingHyprBar : public IHyprWindowDecoration {
|
||||
public:
|
||||
COmarchyFloatingHyprBar(PHLWINDOW);
|
||||
virtual ~COmarchyFloatingHyprBar();
|
||||
|
||||
virtual SDecorationPositioningInfo getPositioningInfo();
|
||||
|
||||
virtual void onPositioningReply(const SDecorationPositioningReply &reply);
|
||||
|
||||
virtual void draw(PHLMONITOR, float const &a);
|
||||
|
||||
virtual eDecorationType getDecorationType();
|
||||
|
||||
virtual void updateWindow(PHLWINDOW);
|
||||
|
||||
virtual void damageEntire();
|
||||
|
||||
virtual eDecorationLayer getDecorationLayer();
|
||||
|
||||
virtual uint64_t getDecorationFlags();
|
||||
|
||||
bool m_bButtonsDirty = true;
|
||||
|
||||
virtual std::string getDisplayName();
|
||||
|
||||
PHLWINDOW getOwner();
|
||||
|
||||
void updateRules();
|
||||
void onConfigReloaded();
|
||||
|
||||
WP<COmarchyFloatingHyprBar> m_self;
|
||||
|
||||
private:
|
||||
SBoxExtents m_seExtents;
|
||||
|
||||
PHLWINDOWREF m_pWindow;
|
||||
|
||||
CBox m_bAssignedBox;
|
||||
|
||||
SP<Render::ITexture> m_pTextTex;
|
||||
|
||||
bool m_bWindowSizeChanged = false;
|
||||
bool m_hidden = false;
|
||||
bool m_bTitleColorChanged = false;
|
||||
bool m_bButtonHovered = false;
|
||||
bool m_bLastEnabledState = false;
|
||||
bool m_bWindowHasFocus = false;
|
||||
std::optional<CHyprColor> m_bForcedBarColor;
|
||||
std::optional<CHyprColor> m_bForcedTitleColor;
|
||||
|
||||
Time::steady_tp m_lastMouseDown{};
|
||||
|
||||
PHLANIMVAR<CHyprColor> m_cRealBarColor;
|
||||
|
||||
Vector2D cursorRelativeToBar();
|
||||
|
||||
void renderPass(PHLMONITOR, float const &a);
|
||||
void renderBarTitle(const Vector2D &bufferSize, const float scale);
|
||||
void renderBarButtons(CBox *barBox, const float scale, const float a);
|
||||
void renderBarButtonsText(CBox *barBox, const float scale, const float a);
|
||||
void damageOnButtonHover();
|
||||
|
||||
bool inputIsValid();
|
||||
void onMouseButton(Event::SCallbackInfo &info, IPointer::SButtonEvent e);
|
||||
void onTouchDown(Event::SCallbackInfo &info, ITouch::SDownEvent e);
|
||||
void onTouchUp(Event::SCallbackInfo &info, ITouch::SUpEvent e);
|
||||
void onMouseMove(Vector2D coords);
|
||||
void onTouchMove(Event::SCallbackInfo &info, ITouch::SMotionEvent e);
|
||||
|
||||
void handleDownEvent(Event::SCallbackInfo &info, std::optional<ITouch::SDownEvent> touchEvent);
|
||||
void handleUpEvent(Event::SCallbackInfo &info);
|
||||
void handleMovement();
|
||||
bool doButtonPress(Config::INTEGER barPadding, Config::INTEGER barButtonPadding, Config::INTEGER barHeight, Vector2D COORDS, bool BUTTONSRIGHT);
|
||||
|
||||
CBox assignedBoxGlobal();
|
||||
|
||||
CHyprSignalListener m_pMouseButtonCallback;
|
||||
CHyprSignalListener m_pTouchDownCallback;
|
||||
CHyprSignalListener m_pTouchUpCallback;
|
||||
|
||||
CHyprSignalListener m_pTouchMoveCallback;
|
||||
CHyprSignalListener m_pMouseMoveCallback;
|
||||
|
||||
std::string m_szLastTitle;
|
||||
|
||||
bool m_bDraggingThis = false;
|
||||
bool m_bTouchEv = false;
|
||||
bool m_bDragPending = false;
|
||||
bool m_bCancelledDown = false;
|
||||
int m_touchId = 0;
|
||||
|
||||
// store hover state for buttons as a bitfield
|
||||
unsigned int m_iButtonHoverState = 0;
|
||||
|
||||
// for dynamic updates
|
||||
int m_iLastHeight = 0;
|
||||
|
||||
size_t getVisibleButtonCount(Config::INTEGER barButtonPadding, Config::INTEGER barPadding, const Vector2D &bufferSize, const float scale);
|
||||
|
||||
friend class COmarchyFloatingBarPassElement;
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
#include "edgeSnap.hpp"
|
||||
#include "globals.hpp"
|
||||
|
||||
#include <hyprland/src/desktop/view/Window.hpp>
|
||||
#include <hyprland/src/event/EventBus.hpp>
|
||||
#include <hyprland/src/layout/LayoutManager.hpp>
|
||||
#include <hyprland/src/managers/EventManager.hpp>
|
||||
#include <hyprland/src/managers/KeybindManager.hpp>
|
||||
#include <hyprland/src/managers/fullscreen/FullscreenController.hpp>
|
||||
#include <hyprland/src/output/Monitor.hpp>
|
||||
#include <hyprland/src/state/MonitorState.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
PHLWINDOWREF dragged;
|
||||
CBox originalBox;
|
||||
CBox snapBox;
|
||||
std::string zone;
|
||||
std::string preview;
|
||||
std::unordered_map<PHLWINDOWREF, CBox> restoreBoxes;
|
||||
std::vector<CHyprSignalListener> listeners;
|
||||
|
||||
void hidePreview() {
|
||||
if (!preview.empty())
|
||||
g_pEventManager->postEvent({"omarchy_snap_preview", ",none,0,0,0,0"});
|
||||
preview.clear();
|
||||
zone.clear();
|
||||
}
|
||||
|
||||
bool enabled() { return g_pOmarchyFloatingGlobalState->config.enabled->value() && g_pOmarchyFloatingGlobalState->config.edgeSnap->value(); }
|
||||
|
||||
void updatePreview(const Vector2D &cursor) {
|
||||
const auto &drag = g_layoutManager->dragController();
|
||||
const auto target = drag->target();
|
||||
if (!enabled() || drag->mode() != MBIND_MOVE || !target || !target->floating() || !validMapped(target->window())) {
|
||||
omarchyFloatingSnapReset();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto window = target->window();
|
||||
const auto workspaceTag = g_pOmarchyFloatingGlobalState->config.workspaceTag->value();
|
||||
if (!workspaceTag.empty() && !window->m_ruleApplicator->m_tagKeeper.isTagged(workspaceTag)) {
|
||||
omarchyFloatingSnapReset();
|
||||
return;
|
||||
}
|
||||
if (dragged != window) {
|
||||
dragged = window;
|
||||
originalBox = target->position();
|
||||
}
|
||||
|
||||
const auto monitor = State::monitorState()->query().vec(cursor).run();
|
||||
if (!monitor) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto work = monitor->logicalBoxMinusReserved();
|
||||
const auto threshold = std::clamp<int>(g_pOmarchyFloatingGlobalState->config.edgeThreshold->value(), 1, 100);
|
||||
const auto gap = std::clamp<int>(g_pOmarchyFloatingGlobalState->config.snapGap->value(), 0, 100);
|
||||
std::string nextZone;
|
||||
if (cursor.y <= work.y + threshold)
|
||||
nextZone = "maximize";
|
||||
else if (cursor.x <= monitor->m_position.x + threshold)
|
||||
nextZone = "left";
|
||||
else if (cursor.x >= monitor->m_position.x + monitor->m_size.x - threshold)
|
||||
nextZone = "right";
|
||||
|
||||
if (nextZone.empty()) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
auto box = nextZone == "maximize" ? work : work.copy().expand(-gap);
|
||||
if (nextZone != "maximize") {
|
||||
box.w = std::floor((work.w - 3 * gap) / 2.0);
|
||||
if (nextZone == "right")
|
||||
box.x = work.x + work.w - gap - box.w;
|
||||
}
|
||||
|
||||
const auto reserved = window->getFullWindowReservedArea();
|
||||
const auto min = target->minSize().value_or(Vector2D{1, 1});
|
||||
// Applications with a larger minimum cannot occupy a half without overlap.
|
||||
// Keep their free placement instead of previewing geometry we cannot honor.
|
||||
if (nextZone != "maximize" && (box.w - reserved.topLeft.x - reserved.bottomRight.x < min.x || box.h - reserved.topLeft.y - reserved.bottomRight.y < min.y)) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
zone = nextZone;
|
||||
snapBox = box;
|
||||
const auto next = std::format("{},{},{:.0f},{:.0f},{:.0f},{:.0f}", monitor->m_name, zone, box.x, box.y, box.w, box.h);
|
||||
if (next != preview) {
|
||||
preview = next;
|
||||
g_pEventManager->postEvent({"omarchy_snap_preview", preview});
|
||||
}
|
||||
}
|
||||
|
||||
void release(IPointer::SButtonEvent event) {
|
||||
if (event.button != BTN_LEFT || event.state != WL_POINTER_BUTTON_STATE_RELEASED)
|
||||
return;
|
||||
|
||||
const auto window = dragged.lock();
|
||||
const auto target = g_layoutManager->dragController()->target();
|
||||
const auto nextZone = zone;
|
||||
const auto box = snapBox;
|
||||
const auto before = originalBox;
|
||||
const bool commit =
|
||||
enabled() && validMapped(window) && target && target->window() == window && g_layoutManager->dragController()->mode() == MBIND_MOVE && !nextZone.empty();
|
||||
omarchyFloatingSnapReset();
|
||||
if (!commit)
|
||||
return;
|
||||
|
||||
// End the native drag before changing geometry, otherwise its final motion
|
||||
// overwrites the snap. The titlebar's release handler is idempotent.
|
||||
g_pKeybindManager->changeMouseBindMode(MBIND_INVALID);
|
||||
if (nextZone == "maximize") {
|
||||
restoreBoxes.erase(window);
|
||||
Fullscreen::controller()->setFullscreenMode(window, Fullscreen::FSMODE_MAXIMIZED);
|
||||
} else {
|
||||
restoreBoxes.try_emplace(window, before);
|
||||
Fullscreen::controller()->setFullscreenMode(window, Fullscreen::FSMODE_NONE);
|
||||
const auto reserved = window->getFullWindowReservedArea();
|
||||
const CBox clientBox{box.pos() + reserved.topLeft, box.size() - reserved.topLeft - reserved.bottomRight};
|
||||
g_layoutManager->setTargetGeom(clientBox, window->layoutTarget());
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void omarchyFloatingSnapReset() {
|
||||
hidePreview();
|
||||
dragged.reset();
|
||||
}
|
||||
|
||||
bool omarchyFloatingRestoreSnap(PHLWINDOW window) {
|
||||
const auto found = restoreBoxes.find(window);
|
||||
if (found == restoreBoxes.end())
|
||||
return false;
|
||||
const auto box = found->second;
|
||||
restoreBoxes.erase(found);
|
||||
g_layoutManager->setTargetGeom(box, window->layoutTarget());
|
||||
return true;
|
||||
}
|
||||
|
||||
void omarchyFloatingSnapInit() {
|
||||
listeners.emplace_back(Event::bus()->m_events.input.mouse.move.listen([](Vector2D cursor, Event::SCallbackInfo &) { updatePreview(cursor); }));
|
||||
listeners.emplace_back(Event::bus()->m_events.input.mouse.button.listen([](IPointer::SButtonEvent event, Event::SCallbackInfo &) { release(event); }));
|
||||
listeners.emplace_back(Event::bus()->m_events.window.close.listen([](PHLWINDOW window) {
|
||||
restoreBoxes.erase(window);
|
||||
if (dragged == window)
|
||||
omarchyFloatingSnapReset();
|
||||
}));
|
||||
}
|
||||
|
||||
void omarchyFloatingSnapExit() {
|
||||
omarchyFloatingSnapReset();
|
||||
listeners.clear();
|
||||
restoreBoxes.clear();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <hyprland/src/desktop/DesktopTypes.hpp>
|
||||
|
||||
void omarchyFloatingSnapInit();
|
||||
void omarchyFloatingSnapReset();
|
||||
void omarchyFloatingSnapExit();
|
||||
bool omarchyFloatingRestoreSnap(PHLWINDOW window);
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||
#include <hyprland/src/render/Texture.hpp>
|
||||
#include <hyprland/src/config/values/types/BoolValue.hpp>
|
||||
#include <hyprland/src/config/values/types/IntValue.hpp>
|
||||
#include <hyprland/src/config/values/types/StringValue.hpp>
|
||||
#include <hyprland/src/config/values/types/ColorValue.hpp>
|
||||
#include <hyprland/src/config/values/types/FontWeightValue.hpp>
|
||||
|
||||
inline HANDLE OMARCHY_FLOATING_PHANDLE = nullptr;
|
||||
|
||||
struct SOmarchyFloatingHyprButton {
|
||||
std::string cmd = "";
|
||||
bool userfg = false;
|
||||
CHyprColor fgcol = CHyprColor(0, 0, 0, 0);
|
||||
CHyprColor bgcol = CHyprColor(0, 0, 0, 0);
|
||||
float size = 10;
|
||||
std::string icon = "";
|
||||
SP<Render::ITexture> iconTex;
|
||||
};
|
||||
|
||||
class COmarchyFloatingHyprBar;
|
||||
|
||||
struct SOmarchyFloatingGlobalState {
|
||||
std::vector<SOmarchyFloatingHyprButton> buttons;
|
||||
std::vector<WP<COmarchyFloatingHyprBar>> bars;
|
||||
uint32_t nobarRuleIdx = 0;
|
||||
uint32_t barColorRuleIdx = 0;
|
||||
uint32_t titleColorRuleIdx = 0;
|
||||
|
||||
struct {
|
||||
SP<Config::Values::CColorValue> barColor, textColor, inactiveBarColor, inactiveButtonColor, buttonBorderColor;
|
||||
SP<Config::Values::CIntValue> barHeight;
|
||||
SP<Config::Values::CIntValue> barTextSize;
|
||||
SP<Config::Values::CFontWeightValue> barTextWeight;
|
||||
SP<Config::Values::CIntValue> barPadding;
|
||||
SP<Config::Values::CIntValue> barButtonPadding;
|
||||
SP<Config::Values::CIntValue> buttonRounding;
|
||||
SP<Config::Values::CIntValue> buttonBorderSize;
|
||||
SP<Config::Values::CIntValue> edgeThreshold, snapGap;
|
||||
SP<Config::Values::CBoolValue> barBlur, barTitleEnabled, barPartOfWindow, barPrecedenceOverBorder, enabled, iconOnHover, edgeSnap;
|
||||
SP<Config::Values::CStringValue> barTextFont, barTextAlign, barButtonsAlignment, onDoubleClick, workspaceTag;
|
||||
} config;
|
||||
};
|
||||
|
||||
inline UP<SOmarchyFloatingGlobalState> g_pOmarchyFloatingGlobalState;
|
||||
@@ -0,0 +1,324 @@
|
||||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <any>
|
||||
#include <hyprland/src/Compositor.hpp>
|
||||
#include <hyprland/src/desktop/view/Window.hpp>
|
||||
#include <hyprland/src/desktop/state/WindowState.hpp>
|
||||
#include <hyprland/src/config/ConfigManager.hpp>
|
||||
#include <hyprland/src/config/shared/parserUtils/ParserUtils.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#include <hyprland/src/event/EventBus.hpp>
|
||||
#include <hyprland/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp>
|
||||
#include <hyprland/src/config/lua/bindings/LuaBindingsInternal.hpp>
|
||||
#include <hyprland/src/config/lua/types/LuaConfigColor.hpp>
|
||||
#include <hyprland/src/state/MonitorState.hpp>
|
||||
|
||||
#include <hyprutils/string/VarList.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "barDeco.hpp"
|
||||
#include "globals.hpp"
|
||||
#include "edgeSnap.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
}
|
||||
|
||||
// Do NOT change this function.
|
||||
APICALL EXPORT std::string PLUGIN_API_VERSION() { return HYPRLAND_API_VERSION; }
|
||||
|
||||
static void onNewWindow(PHLWINDOW window) {
|
||||
if (!window->m_X11DoesntWantBorders) {
|
||||
if (std::ranges::any_of(window->m_windowDecorations, [](const auto &d) { return d->getDisplayName() == "Hyprbar"; }))
|
||||
return;
|
||||
|
||||
auto bar = makeUnique<COmarchyFloatingHyprBar>(window);
|
||||
g_pOmarchyFloatingGlobalState->bars.emplace_back(bar);
|
||||
bar->m_self = bar;
|
||||
HyprlandAPI::addWindowDecoration(OMARCHY_FLOATING_PHANDLE, window, std::move(bar));
|
||||
}
|
||||
}
|
||||
|
||||
static void onPreConfigReload() {
|
||||
omarchyFloatingSnapReset();
|
||||
g_pOmarchyFloatingGlobalState->buttons.clear();
|
||||
}
|
||||
|
||||
static void onConfigReloaded() {
|
||||
for (auto &b : g_pOmarchyFloatingGlobalState->bars) {
|
||||
if (!b)
|
||||
continue;
|
||||
|
||||
b->onConfigReloaded();
|
||||
}
|
||||
}
|
||||
|
||||
static void onUpdateWindowRules(PHLWINDOW window) {
|
||||
const auto BARIT = std::find_if(g_pOmarchyFloatingGlobalState->bars.begin(), g_pOmarchyFloatingGlobalState->bars.end(),
|
||||
[window](const auto &bar) { return bar->getOwner() == window; });
|
||||
|
||||
if (BARIT == g_pOmarchyFloatingGlobalState->bars.end())
|
||||
return;
|
||||
|
||||
(*BARIT)->updateRules();
|
||||
window->updateWindowDecos();
|
||||
}
|
||||
|
||||
Hyprlang::CParseResult onNewOmarchyFloatingButton(const char *K, const char *V) {
|
||||
std::string v = V;
|
||||
Hyprutils::String::CVarList vars(v);
|
||||
|
||||
Hyprlang::CParseResult result;
|
||||
|
||||
// hyprbars-button = bgcolor, size, icon, action, fgcolor
|
||||
|
||||
if (vars[0].empty() || vars[1].empty()) {
|
||||
result.setError("bgcolor and size cannot be empty");
|
||||
return result;
|
||||
}
|
||||
|
||||
float size = 10;
|
||||
try {
|
||||
size = std::stof(vars[1]);
|
||||
} catch (std::exception &e) {
|
||||
result.setError("failed to parse size");
|
||||
return result;
|
||||
}
|
||||
|
||||
bool userfg = false;
|
||||
auto fgcolor = Config::ParserUtils::parseColor("rgb(ffffff)");
|
||||
auto bgcolor = Config::ParserUtils::parseColor(vars[0]);
|
||||
|
||||
if (!bgcolor) {
|
||||
result.setError("invalid bgcolor");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (vars.size() == 5) {
|
||||
userfg = true;
|
||||
fgcolor = Config::ParserUtils::parseColor(vars[4]);
|
||||
}
|
||||
|
||||
if (!fgcolor) {
|
||||
result.setError("invalid fgcolor");
|
||||
return result;
|
||||
}
|
||||
|
||||
g_pOmarchyFloatingGlobalState->buttons.push_back(SOmarchyFloatingHyprButton{vars[3], userfg, *fgcolor, *bgcolor, size, vars[2]});
|
||||
|
||||
for (auto &b : g_pOmarchyFloatingGlobalState->bars) {
|
||||
b->m_bButtonsDirty = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int newOmarchyFloatingLuaButton(lua_State *L) {
|
||||
if (!lua_istable(L, 1))
|
||||
return Config::Lua::Bindings::Internal::configError(L, "add_button: expected a table { bg_color, fg_color, size, icon, action }");
|
||||
|
||||
SOmarchyFloatingHyprButton button;
|
||||
|
||||
{
|
||||
Hyprutils::Utils::CScopeGuard x([L] { lua_pop(L, 1); });
|
||||
|
||||
lua_getfield(L, 1, "bg_color");
|
||||
|
||||
Config::Lua::CLuaConfigColor parser(0);
|
||||
auto err = parser.parse(L);
|
||||
if (err.errorCode != Config::Lua::PARSE_ERROR_OK)
|
||||
return Config::Lua::Bindings::Internal::configError(L, "add_button: failed to parse bg_color");
|
||||
|
||||
button.bgcol = parser.parsed();
|
||||
}
|
||||
|
||||
{
|
||||
Hyprutils::Utils::CScopeGuard x([L] { lua_pop(L, 1); });
|
||||
|
||||
lua_getfield(L, 1, "fg_color");
|
||||
|
||||
Config::Lua::CLuaConfigColor parser(0);
|
||||
auto err = parser.parse(L);
|
||||
if (err.errorCode != Config::Lua::PARSE_ERROR_OK)
|
||||
return Config::Lua::Bindings::Internal::configError(L, "add_button: failed to parse fg_color");
|
||||
|
||||
button.userfg = true;
|
||||
button.fgcol = parser.parsed();
|
||||
}
|
||||
|
||||
{
|
||||
Hyprutils::Utils::CScopeGuard x([L] { lua_pop(L, 1); });
|
||||
|
||||
lua_getfield(L, 1, "size");
|
||||
|
||||
if (!lua_isnumber(L, -1))
|
||||
return Config::Lua::Bindings::Internal::configError(L, "add_button: size must be an integer");
|
||||
|
||||
button.size = lua_tointeger(L, -1);
|
||||
}
|
||||
|
||||
{
|
||||
Hyprutils::Utils::CScopeGuard x([L] { lua_pop(L, 1); });
|
||||
|
||||
lua_getfield(L, 1, "icon");
|
||||
|
||||
if (!lua_isstring(L, -1))
|
||||
return Config::Lua::Bindings::Internal::configError(L, "add_button: icon must be a string");
|
||||
|
||||
button.icon = lua_tostring(L, -1);
|
||||
}
|
||||
|
||||
{
|
||||
Hyprutils::Utils::CScopeGuard x([L] { lua_pop(L, 1); });
|
||||
|
||||
lua_getfield(L, 1, "action");
|
||||
|
||||
if (!lua_isstring(L, -1))
|
||||
return Config::Lua::Bindings::Internal::configError(L, "add_button: action must be a string");
|
||||
|
||||
button.cmd = lua_tostring(L, -1);
|
||||
}
|
||||
|
||||
g_pOmarchyFloatingGlobalState->buttons.push_back(std::move(button));
|
||||
|
||||
for (auto &b : g_pOmarchyFloatingGlobalState->bars) {
|
||||
b->m_bButtonsDirty = true;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PLUGIN_DESCRIPTION_INFO initializeOmarchyFloating(HANDLE handle) {
|
||||
OMARCHY_FLOATING_PHANDLE = handle;
|
||||
|
||||
const std::string HASH = __hyprland_api_get_hash();
|
||||
const std::string CLIENT_HASH = __hyprland_api_get_client_hash();
|
||||
|
||||
if (HASH != CLIENT_HASH) {
|
||||
HyprlandAPI::addNotification(OMARCHY_FLOATING_PHANDLE,
|
||||
"[hyprbars] Failure in initialization: Version mismatch (headers ver is not equal to running hyprland ver)",
|
||||
CHyprColor{1.0, 0.2, 0.2, 1.0}, 5000);
|
||||
throw std::runtime_error("[hb] Version mismatch");
|
||||
}
|
||||
|
||||
g_pOmarchyFloatingGlobalState = makeUnique<SOmarchyFloatingGlobalState>();
|
||||
g_pOmarchyFloatingGlobalState->nobarRuleIdx = Desktop::Rule::windowEffects()->registerEffect("hyprbars:no_bar");
|
||||
g_pOmarchyFloatingGlobalState->barColorRuleIdx = Desktop::Rule::windowEffects()->registerEffect("hyprbars:bar_color");
|
||||
g_pOmarchyFloatingGlobalState->titleColorRuleIdx = Desktop::Rule::windowEffects()->registerEffect("hyprbars:title_color");
|
||||
|
||||
static auto P = Event::bus()->m_events.window.open.listen([&](PHLWINDOW w) { onNewWindow(w); });
|
||||
static auto P3 = Event::bus()->m_events.window.updateRules.listen([&](PHLWINDOW w) { onUpdateWindowRules(w); });
|
||||
|
||||
g_pOmarchyFloatingGlobalState->config.barColor = makeShared<Config::Values::CColorValue>("plugin:hyprbars:bar_color", "Change the bar color", 0x88333333);
|
||||
g_pOmarchyFloatingGlobalState->config.textColor = makeShared<Config::Values::CColorValue>("plugin:hyprbars:col.text", "Change the text color", 0xffffffff);
|
||||
g_pOmarchyFloatingGlobalState->config.inactiveBarColor =
|
||||
makeShared<Config::Values::CColorValue>("plugin:hyprbars:bar_color_inactive", "Inactive title bar color; transparent means use bar_color", 0x00000000);
|
||||
g_pOmarchyFloatingGlobalState->config.inactiveButtonColor = makeShared<Config::Values::CColorValue>(
|
||||
"plugin:hyprbars:inactive_button_color", "Change the inactive button's color. 0x00000000 means unset", 0x00000000);
|
||||
g_pOmarchyFloatingGlobalState->config.barHeight = makeShared<Config::Values::CIntValue>("plugin:hyprbars:bar_height", "Change the bar's height", 15);
|
||||
g_pOmarchyFloatingGlobalState->config.barTextSize = makeShared<Config::Values::CIntValue>("plugin:hyprbars:bar_text_size", "Change the bar's text size", 10);
|
||||
g_pOmarchyFloatingGlobalState->config.barTextWeight =
|
||||
makeShared<Config::Values::CFontWeightValue>("plugin:hyprbars:bar_text_weight", "Bar's title text weight (e.g. \"bold\" or an integer 100-1000)", 400);
|
||||
g_pOmarchyFloatingGlobalState->config.barTitleEnabled =
|
||||
makeShared<Config::Values::CBoolValue>("plugin:hyprbars:bar_title_enabled", "Whether to enable titles in the bar", true);
|
||||
g_pOmarchyFloatingGlobalState->config.barBlur =
|
||||
makeShared<Config::Values::CBoolValue>("plugin:hyprbars:bar_blur", "Whether to enable blur of the bar", false);
|
||||
g_pOmarchyFloatingGlobalState->config.barTextFont = makeShared<Config::Values::CStringValue>("plugin:hyprbars:bar_text_font", "Bar's text font", "Sans");
|
||||
g_pOmarchyFloatingGlobalState->config.barTextAlign =
|
||||
makeShared<Config::Values::CStringValue>("plugin:hyprbars:bar_text_align", "Bar's text alignment", "center");
|
||||
g_pOmarchyFloatingGlobalState->config.barPartOfWindow =
|
||||
makeShared<Config::Values::CBoolValue>("plugin:hyprbars:bar_part_of_window", "Whether the bar is a part of the window (reserves space)", true);
|
||||
g_pOmarchyFloatingGlobalState->config.barPrecedenceOverBorder =
|
||||
makeShared<Config::Values::CBoolValue>("plugin:hyprbars:bar_precedence_over_border", "Whether the bar is before, or after the border", false);
|
||||
g_pOmarchyFloatingGlobalState->config.barButtonsAlignment =
|
||||
makeShared<Config::Values::CStringValue>("plugin:hyprbars:bar_buttons_alignment", "Alignment of the bar buttons", "right");
|
||||
g_pOmarchyFloatingGlobalState->config.barPadding = makeShared<Config::Values::CIntValue>("plugin:hyprbars:bar_padding", "Padding of the bar", 7);
|
||||
g_pOmarchyFloatingGlobalState->config.barButtonPadding =
|
||||
makeShared<Config::Values::CIntValue>("plugin:hyprbars:bar_button_padding", "Padding of the bar buttons", 5);
|
||||
g_pOmarchyFloatingGlobalState->config.buttonRounding =
|
||||
makeShared<Config::Values::CIntValue>("plugin:hyprbars:button_rounding", "Button corner radius. -1 preserves the upstream circular style", -1);
|
||||
g_pOmarchyFloatingGlobalState->config.buttonBorderSize =
|
||||
makeShared<Config::Values::CIntValue>("plugin:hyprbars:button_border_size", "Width of the button border", 0);
|
||||
g_pOmarchyFloatingGlobalState->config.buttonBorderColor =
|
||||
makeShared<Config::Values::CColorValue>("plugin:hyprbars:button_border_color", "Color of the button border", 0x00000000);
|
||||
g_pOmarchyFloatingGlobalState->config.enabled = makeShared<Config::Values::CBoolValue>("plugin:hyprbars:enabled", "Whether bars are enabled", true);
|
||||
g_pOmarchyFloatingGlobalState->config.iconOnHover =
|
||||
makeShared<Config::Values::CBoolValue>("plugin:hyprbars:icon_on_hover", "Whether to use an icon on hover of the buttons", false);
|
||||
g_pOmarchyFloatingGlobalState->config.onDoubleClick =
|
||||
makeShared<Config::Values::CStringValue>("plugin:hyprbars:on_double_click", "Action to execute on double click of the bar", "");
|
||||
g_pOmarchyFloatingGlobalState->config.workspaceTag =
|
||||
makeShared<Config::Values::CStringValue>("plugin:hyprbars:workspace_tag", "Restrict bars and snapping to floating windows with this tag", "");
|
||||
g_pOmarchyFloatingGlobalState->config.edgeSnap =
|
||||
makeShared<Config::Values::CBoolValue>("plugin:hyprbars:edge_snap", "Snap dragged floating windows at monitor edges", false);
|
||||
g_pOmarchyFloatingGlobalState->config.edgeThreshold =
|
||||
makeShared<Config::Values::CIntValue>("plugin:hyprbars:edge_threshold", "Edge activation distance in logical pixels", 24);
|
||||
g_pOmarchyFloatingGlobalState->config.snapGap =
|
||||
makeShared<Config::Values::CIntValue>("plugin:hyprbars:snap_gap", "Outer and center spacing in logical pixels", 8);
|
||||
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barColor);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.textColor);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.inactiveBarColor);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.inactiveButtonColor);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barHeight);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barTextSize);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barTextWeight);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barTitleEnabled);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barBlur);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barTextFont);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barTextAlign);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barPartOfWindow);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barPrecedenceOverBorder);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barButtonsAlignment);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barPadding);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.barButtonPadding);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.buttonRounding);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.buttonBorderSize);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.buttonBorderColor);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.enabled);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.workspaceTag);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.iconOnHover);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.onDoubleClick);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.edgeSnap);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.edgeThreshold);
|
||||
HyprlandAPI::addConfigValueV2(OMARCHY_FLOATING_PHANDLE, g_pOmarchyFloatingGlobalState->config.snapGap);
|
||||
|
||||
omarchyFloatingSnapInit();
|
||||
|
||||
if (Config::mgr()->type() == Config::CONFIG_LEGACY)
|
||||
HyprlandAPI::addConfigKeyword(OMARCHY_FLOATING_PHANDLE, "plugin:hyprbars:hyprbars-button", onNewOmarchyFloatingButton, Hyprlang::SHandlerOptions{});
|
||||
else
|
||||
HyprlandAPI::addLuaFunction(OMARCHY_FLOATING_PHANDLE, "hyprbars", "add_button", ::newOmarchyFloatingLuaButton);
|
||||
static auto P4 = Event::bus()->m_events.config.preReload.listen([&] { onPreConfigReload(); });
|
||||
static auto P5 = Event::bus()->m_events.config.reloaded.listen([&] { onConfigReloaded(); });
|
||||
|
||||
// add deco to existing windows
|
||||
for (auto &w : Desktop::windowState()->windows()) {
|
||||
if (w->isHidden() || !w->m_isMapped)
|
||||
continue;
|
||||
|
||||
onNewWindow(w);
|
||||
}
|
||||
|
||||
HyprlandAPI::reloadConfig();
|
||||
|
||||
return {"hyprbars", "A plugin to add title bars to windows.", "Vaxry", "1.0"};
|
||||
}
|
||||
|
||||
static void exitOmarchyFloating() {
|
||||
omarchyFloatingSnapExit();
|
||||
for (auto &m : State::monitorState()->monitors())
|
||||
m->m_scheduledRecalc = true;
|
||||
|
||||
g_pHyprRenderer->m_renderPass.removeAllOfType("COmarchyFloatingBarPassElement");
|
||||
|
||||
Desktop::Rule::windowEffects()->unregisterEffect(g_pOmarchyFloatingGlobalState->barColorRuleIdx);
|
||||
Desktop::Rule::windowEffects()->unregisterEffect(g_pOmarchyFloatingGlobalState->titleColorRuleIdx);
|
||||
Desktop::Rule::windowEffects()->unregisterEffect(g_pOmarchyFloatingGlobalState->nobarRuleIdx);
|
||||
}
|
||||
|
||||
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { return initializeOmarchyFloating(handle); }
|
||||
APICALL EXPORT void PLUGIN_EXIT() { exitOmarchyFloating(); }
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <wayland-client.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "pointer-protocol.h"
|
||||
|
||||
static struct zwlr_virtual_pointer_manager_v1 *manager;
|
||||
|
||||
static void global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
|
||||
if (!strcmp(interface, "zwlr_virtual_pointer_manager_v1"))
|
||||
manager = wl_registry_bind(registry, name, &zwlr_virtual_pointer_manager_v1_interface, 1);
|
||||
}
|
||||
|
||||
static void removed(void *data, struct wl_registry *registry, uint32_t name) {}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3)
|
||||
return 2;
|
||||
struct wl_display *display = wl_display_connect(NULL);
|
||||
if (!display)
|
||||
return 1;
|
||||
struct wl_registry *registry = wl_display_get_registry(display);
|
||||
const struct wl_registry_listener listener = {global, removed};
|
||||
wl_registry_add_listener(registry, &listener, NULL);
|
||||
wl_display_roundtrip(display);
|
||||
if (!manager)
|
||||
return 3;
|
||||
struct zwlr_virtual_pointer_v1 *pointer = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, NULL);
|
||||
char line[128], command[32];
|
||||
int x, y;
|
||||
while (fgets(line, sizeof(line), stdin)) {
|
||||
if (sscanf(line, "%31s %d %d", command, &x, &y) < 1)
|
||||
continue;
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
uint32_t now = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
if (!strcmp(command, "move"))
|
||||
zwlr_virtual_pointer_v1_motion_absolute(pointer, now, x, y, atoi(argv[1]), atoi(argv[2]));
|
||||
else if (!strcmp(command, "down"))
|
||||
zwlr_virtual_pointer_v1_button(pointer, now, 272, WL_POINTER_BUTTON_STATE_PRESSED);
|
||||
else if (!strcmp(command, "up"))
|
||||
zwlr_virtual_pointer_v1_button(pointer, now, 272, WL_POINTER_BUTTON_STATE_RELEASED);
|
||||
else if (!strcmp(command, "wait"))
|
||||
usleep(x * 1000);
|
||||
zwlr_virtual_pointer_v1_frame(pointer);
|
||||
wl_display_roundtrip(display);
|
||||
}
|
||||
zwlr_virtual_pointer_v1_destroy(pointer);
|
||||
zwlr_virtual_pointer_manager_v1_destroy(manager);
|
||||
wl_registry_destroy(registry);
|
||||
wl_display_roundtrip(display);
|
||||
wl_display_disconnect(display);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Verify omarchy-hyprland-titlebars lifecycle and pointer gestures in an isolated nested Hyprland.
|
||||
|
||||
The candidate library is only ever loaded into the disposable nested compositor,
|
||||
never into the parent session.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
source = Path(__file__).resolve().parent
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('library', type=Path, help='built titlebars.so to test')
|
||||
parser.add_argument('--artifacts', type=Path, default=Path('/tmp/omarchy-hyprland-titlebars-verification'))
|
||||
args = parser.parse_args()
|
||||
args.library = args.library.resolve()
|
||||
if not args.library.is_file():
|
||||
parser.error(f'{args.library} does not exist')
|
||||
args.artifacts.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='oht-') as scratch:
|
||||
scratch = Path(scratch)
|
||||
for kind, target in [('client-header', 'pointer-protocol.h'), ('private-code', 'pointer-protocol.c')]:
|
||||
subprocess.run(['wayland-scanner', kind, str(source / 'wlr-virtual-pointer-unstable-v1.xml'), str(scratch / target)], check=True)
|
||||
subprocess.run(['cc', str(source / 'pointer.c'), str(scratch / 'pointer-protocol.c'), '-I', str(scratch), '-lwayland-client', '-o', str(scratch / 'pointer')], check=True)
|
||||
config = scratch / 'hyprland.lua'
|
||||
config.write_text('''
|
||||
hl.monitor({ output = "", mode = "1280x800@60", position = "0x0", scale = 1 })
|
||||
hl.config({ general = { gaps_in = 8, gaps_out = 8, border_size = 2, resize_on_border = true }, animations = { enabled = false }, input = { follow_mouse = 0 }, misc = { disable_hyprland_logo = true, disable_splash_rendering = true } })
|
||||
hl.window_rule({ name = "test-float", match = { class = ".*" }, float = true, size = { 600, 400 }, center = true })
|
||||
if hl.plugin and hl.plugin.hyprbars then
|
||||
hl.config({ plugin = { hyprbars = {
|
||||
enabled = true, bar_height = 30, bar_color = "rgb(24283b)", bar_color_inactive = "rgb(1a1b26)", col = { text = "rgb(c0caf5)" },
|
||||
button_rounding = 4, bar_text_align = "left", bar_text_font = "Sans", bar_text_size = 12, bar_part_of_window = true,
|
||||
edge_snap = true, edge_threshold = 24, snap_gap = 8,
|
||||
on_double_click = [[hyprctl dispatch 'hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle", window = "address:%WINDOW%" })']],
|
||||
} } })
|
||||
hl.plugin.hyprbars.add_button({ bg_color = "rgb(f7768e)", fg_color = "rgb(1a1b26)", size = 18, icon = "×", action = [[hyprctl dispatch 'hl.dsp.window.close({ window = "address:%WINDOW%" })']] })
|
||||
end
|
||||
''')
|
||||
parent_instances = json.loads(subprocess.check_output(['hyprctl', 'instances', '-j'], text=True))
|
||||
if len(parent_instances) != 1:
|
||||
raise RuntimeError('Verification expects exactly one parent compositor')
|
||||
parent_signature = parent_instances[0]['instance']
|
||||
parent_display = str(Path(os.environ['XDG_RUNTIME_DIR']) / parent_instances[0]['wl_socket'])
|
||||
env = dict(os.environ, XDG_RUNTIME_DIR=str(scratch), AQ_BACKEND='wayland', WAYLAND_DISPLAY=parent_display, HYPRLAND_NO_SD_NOTIFY='1', HYPRLAND_NO_SD_VARS='1')
|
||||
env.pop('HYPRLAND_INSTANCE_SIGNATURE', None)
|
||||
log = (args.artifacts / 'compositor.log').open('w')
|
||||
compositor = subprocess.Popen(['Hyprland', '--config', str(config)], env=env, stdout=log, stderr=log)
|
||||
terminal = None
|
||||
secondary = None
|
||||
|
||||
def ctl(*argv):
|
||||
# Never let a plugin command reach the parent compositor.
|
||||
if argv[0] == 'plugin':
|
||||
signature = env.get('HYPRLAND_INSTANCE_SIGNATURE')
|
||||
if not signature or signature == parent_signature or env['XDG_RUNTIME_DIR'] != str(scratch):
|
||||
raise RuntimeError('Refusing to send a plugin command outside the nested compositor')
|
||||
result = subprocess.run(['hyprctl', *argv], env=env, capture_output=True, text=True, timeout=5, check=True)
|
||||
if result.stdout.startswith('Err') or "Couldn't" in result.stdout:
|
||||
raise RuntimeError(result.stdout)
|
||||
return result.stdout
|
||||
|
||||
def client():
|
||||
return json.loads(ctl('clients', '-j'))[0]
|
||||
|
||||
def pointer(commands):
|
||||
subprocess.run([str(scratch / 'pointer'), '1280', '800'], input='\n'.join(commands) + '\n', text=True, env=env, check=True, timeout=10)
|
||||
time.sleep(.15)
|
||||
|
||||
def drag_to(x, y):
|
||||
window = client()
|
||||
start_x = window['at'][0] + min(window['size'][0] // 2, 220)
|
||||
start_y = window['at'][1] - 15
|
||||
commands = [f'move {start_x} {start_y}', 'wait 100', 'down', 'wait 120']
|
||||
for i in range(1, 21):
|
||||
commands += [f'move {round(start_x + (x - start_x) * i / 20)} {round(start_y + (y - start_y) * i / 20)}', 'wait 20']
|
||||
commands += ['wait 100', 'up']
|
||||
pointer(commands)
|
||||
return client()
|
||||
|
||||
def capture(name):
|
||||
monitor = json.loads(ctl('monitors', '-j'))[0]['name']
|
||||
subprocess.run(['grim', '-o', monitor, str(args.artifacts / f'{name}.png')], env=env, check=True)
|
||||
|
||||
try:
|
||||
for _ in range(80):
|
||||
if compositor.poll() is not None:
|
||||
raise RuntimeError('Isolated compositor exited; inspect compositor.log')
|
||||
try:
|
||||
instances = json.loads(ctl('instances', '-j'))
|
||||
except (ValueError, subprocess.SubprocessError):
|
||||
instances = []
|
||||
if instances:
|
||||
env['HYPRLAND_INSTANCE_SIGNATURE'] = instances[0]['instance']
|
||||
env['WAYLAND_DISPLAY'] = instances[0]['wl_socket']
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise RuntimeError('Isolated compositor did not start')
|
||||
time.sleep(.2)
|
||||
terminal = subprocess.Popen(['foot', '--app-id=omarchy-titlebars-isolated-test', '--title=Titlebars — isolated verification', 'sleep', '90'], env=env, stdout=log, stderr=log)
|
||||
for _ in range(60):
|
||||
if json.loads(ctl('clients', '-j')):
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise RuntimeError('Test client did not map')
|
||||
|
||||
xp = Path('/usr/lib/omarchy-windows-xp/hyprbars.so')
|
||||
sequence = [args.library, args.library]
|
||||
if xp.exists():
|
||||
sequence = [xp, args.library, xp, args.library]
|
||||
for library in sequence:
|
||||
ctl('plugin', 'load', str(library))
|
||||
time.sleep(.15)
|
||||
for _ in range(3):
|
||||
ctl('reload')
|
||||
time.sleep(.1)
|
||||
if library == args.library:
|
||||
assert not ctl('configerrors').strip(), ctl('configerrors')
|
||||
assert compositor.poll() is None
|
||||
assert client()
|
||||
ctl('plugin', 'unload', str(library))
|
||||
time.sleep(.15)
|
||||
assert not json.loads(ctl('plugin', 'list', '-j'))
|
||||
print('PASS: repeated load/reload/unload, interleaved with the XP fork', flush=True)
|
||||
|
||||
ctl('plugin', 'load', str(args.library))
|
||||
ctl('reload')
|
||||
time.sleep(.25)
|
||||
assert not ctl('configerrors').strip(), ctl('configerrors')
|
||||
original = client()
|
||||
events = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
events.connect(str(scratch / 'hypr' / env['HYPRLAND_INSTANCE_SIGNATURE'] / '.socket2.sock'))
|
||||
events.settimeout(.2)
|
||||
capture('floating')
|
||||
|
||||
left = drag_to(1, 360)
|
||||
assert left['at'][0] <= 12 and 610 <= left['size'][0] <= 630, left
|
||||
capture('snap-left')
|
||||
right = drag_to(1278, 360)
|
||||
assert 640 <= right['at'][0] <= 656 and 610 <= right['size'][0] <= 630, right
|
||||
capture('snap-right')
|
||||
free = drag_to(650, 220)
|
||||
assert free['size'] == original['size'], (free, original)
|
||||
top = drag_to(650, 1)
|
||||
assert top['fullscreen'] == 1 and top['at'][1] >= 30, top
|
||||
capture('maximized')
|
||||
restored = drag_to(650, 220)
|
||||
assert restored['fullscreen'] == 0 and restored['size'] == original['size'], restored
|
||||
assert 205 <= restored['at'][1] <= 255, restored
|
||||
print('PASS: real pointer titlebar drag left/right, restore size, top maximize, drag-down restore', flush=True)
|
||||
|
||||
window = client()
|
||||
x, y = window['at'][0] + 120, window['at'][1] - 15
|
||||
pointer([f'move {x} {y}', 'wait 450', 'down', 'up', 'wait 100', 'down', 'up'])
|
||||
assert client()['fullscreen'] == 1, client()
|
||||
print('PASS: titlebar double-click maximize', flush=True)
|
||||
messages = b''
|
||||
try:
|
||||
while chunk := events.recv(65536):
|
||||
messages += chunk
|
||||
except TimeoutError:
|
||||
pass
|
||||
events.close()
|
||||
text = messages.decode()
|
||||
for marker in [',left,', ',right,', ',maximize,', ',none,']:
|
||||
assert f'omarchy_snap_preview>>' in text and marker in text, (marker, text)
|
||||
(args.artifacts / 'events.log').write_text(text)
|
||||
print('PASS: snap preview events and release cleanup', flush=True)
|
||||
|
||||
# Make dispatch deliberately slow, then focus another window after clicking.
|
||||
# Both double-click and button actions must keep the decoration owner.
|
||||
config.write_text(config.read_text().replace('hyprctl dispatch', 'sleep 0.6; hyprctl dispatch'))
|
||||
ctl('reload')
|
||||
first_address = client()['address']
|
||||
ctl('dispatch', f'hl.dsp.window.fullscreen({{ mode = "maximized", action = "unset", window = "address:{first_address}" }})')
|
||||
ctl('dispatch', f'hl.dsp.window.resize({{ x = 500, y = 350, window = "address:{first_address}" }})')
|
||||
ctl('dispatch', f'hl.dsp.window.move({{ x = 60, y = 320, window = "address:{first_address}" }})')
|
||||
secondary = subprocess.Popen(['foot', '--app-id=omarchy-titlebars-focus-race', '--title=Second window must stay unchanged', 'sleep', '90'], env=env, stdout=log, stderr=log)
|
||||
for _ in range(60):
|
||||
windows = json.loads(ctl('clients', '-j'))
|
||||
if len(windows) == 2:
|
||||
break
|
||||
time.sleep(.1)
|
||||
assert len(windows) == 2, windows
|
||||
second_address = next(window['address'] for window in windows if window['address'] != first_address)
|
||||
ctl('dispatch', f'hl.dsp.window.move({{ x = 690, y = 320, window = "address:{second_address}" }})')
|
||||
pointer(['move 180 305', 'wait 450', 'down', 'up', 'wait 100', 'down', 'up'])
|
||||
ctl('dispatch', f'hl.dsp.focus({{ window = "address:{second_address}" }})')
|
||||
time.sleep(.7)
|
||||
windows = {window['address']: window for window in json.loads(ctl('clients', '-j'))}
|
||||
assert windows[first_address]['fullscreen'] == 1 and windows[second_address]['fullscreen'] == 0, windows
|
||||
ctl('dispatch', f'hl.dsp.window.fullscreen({{ mode = "maximized", action = "unset", window = "address:{first_address}" }})')
|
||||
ctl('dispatch', f'hl.dsp.window.move({{ x = 60, y = 320, window = "address:{first_address}" }})')
|
||||
pointer(['move 540 305', 'wait 450', 'down', 'up'])
|
||||
ctl('dispatch', f'hl.dsp.focus({{ window = "address:{second_address}" }})')
|
||||
time.sleep(.7)
|
||||
remaining = json.loads(ctl('clients', '-j'))
|
||||
assert len(remaining) == 1 and remaining[0]['address'] == second_address, remaining
|
||||
print('PASS: delayed double-click and close target the clicked window after focus changes', flush=True)
|
||||
|
||||
# A workspace-scoped bar must release its reserved space as soon as a
|
||||
# window joins tiling, without waiting for a compositor config reload.
|
||||
ctl('eval', 'hl.config({ plugin = { hyprbars = { workspace_tag = "floating-test" } } })')
|
||||
ctl('dispatch', f'hl.dsp.window.tag({{ tag = "+floating-test", window = "address:{second_address}" }})')
|
||||
ctl('dispatch', f'hl.dsp.window.float({{ action = "disable", window = "address:{second_address}" }})')
|
||||
time.sleep(.3)
|
||||
tiled = client()
|
||||
assert not tiled['floating'] and tiled['at'][1] < 30, tiled
|
||||
capture('workspace-tiled-no-titlebar')
|
||||
ctl('dispatch', f'hl.dsp.window.float({{ action = "enable", window = "address:{second_address}" }})')
|
||||
ctl('dispatch', f'hl.dsp.window.fullscreen({{ mode = "maximized", action = "set", window = "address:{second_address}" }})')
|
||||
time.sleep(.3)
|
||||
assert client()['at'][1] >= 30, client()
|
||||
ctl('dispatch', f'hl.dsp.window.tag({{ tag = "-floating-test", window = "address:{second_address}" }})')
|
||||
time.sleep(.3)
|
||||
assert client()['at'][1] < 30, client()
|
||||
capture('workspace-untagged-no-titlebar')
|
||||
print('PASS: workspace-scoped titlebars release space immediately when tiled or untagged', flush=True)
|
||||
finally:
|
||||
if secondary and secondary.poll() is None:
|
||||
secondary.terminate()
|
||||
secondary.wait(timeout=5)
|
||||
if terminal and terminal.poll() is None:
|
||||
terminal.terminate()
|
||||
terminal.wait(timeout=5)
|
||||
compositor.terminate()
|
||||
try:
|
||||
compositor.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
compositor.kill()
|
||||
compositor.wait()
|
||||
log.close()
|
||||
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="wlr_virtual_pointer_unstable_v1">
|
||||
<copyright>
|
||||
Copyright © 2019 Josef Gajdusek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="zwlr_virtual_pointer_v1" version="2">
|
||||
<description summary="virtual pointer">
|
||||
This protocol allows clients to emulate a physical pointer device. The
|
||||
requests are mostly mirror opposites of those specified in wl_pointer.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_axis" value="0"
|
||||
summary="client sent invalid axis enumeration value" />
|
||||
<entry name="invalid_axis_source" value="1"
|
||||
summary="client sent invalid axis source enumeration value" />
|
||||
</enum>
|
||||
|
||||
<request name="motion">
|
||||
<description summary="pointer relative motion event">
|
||||
The pointer has moved by a relative amount to the previous request.
|
||||
|
||||
Values are in the global compositor space.
|
||||
</description>
|
||||
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
|
||||
<arg name="dx" type="fixed" summary="displacement on the x-axis"/>
|
||||
<arg name="dy" type="fixed" summary="displacement on the y-axis"/>
|
||||
</request>
|
||||
|
||||
<request name="motion_absolute">
|
||||
<description summary="pointer absolute motion event">
|
||||
The pointer has moved in an absolute coordinate frame.
|
||||
|
||||
Value of x can range from 0 to x_extent, value of y can range from 0
|
||||
to y_extent.
|
||||
</description>
|
||||
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
|
||||
<arg name="x" type="uint" summary="position on the x-axis"/>
|
||||
<arg name="y" type="uint" summary="position on the y-axis"/>
|
||||
<arg name="x_extent" type="uint" summary="extent of the x-axis"/>
|
||||
<arg name="y_extent" type="uint" summary="extent of the y-axis"/>
|
||||
</request>
|
||||
|
||||
<request name="button">
|
||||
<description summary="button event">
|
||||
A button was pressed or released.
|
||||
</description>
|
||||
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
|
||||
<arg name="button" type="uint" summary="button that produced the event"/>
|
||||
<arg name="state" type="uint" enum="wl_pointer.button_state" summary="physical state of the button"/>
|
||||
</request>
|
||||
|
||||
<request name="axis">
|
||||
<description summary="axis event">
|
||||
Scroll and other axis requests.
|
||||
</description>
|
||||
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
|
||||
<arg name="axis" type="uint" enum="wl_pointer.axis" summary="axis type"/>
|
||||
<arg name="value" type="fixed" summary="length of vector in touchpad coordinates"/>
|
||||
</request>
|
||||
|
||||
<request name="frame">
|
||||
<description summary="end of a pointer event sequence">
|
||||
Indicates the set of events that logically belong together.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="axis_source">
|
||||
<description summary="axis source event">
|
||||
Source information for scroll and other axis.
|
||||
</description>
|
||||
<arg name="axis_source" type="uint" enum="wl_pointer.axis_source" summary="source of the axis event"/>
|
||||
</request>
|
||||
|
||||
<request name="axis_stop">
|
||||
<description summary="axis stop event">
|
||||
Stop notification for scroll and other axes.
|
||||
</description>
|
||||
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
|
||||
<arg name="axis" type="uint" enum="wl_pointer.axis" summary="the axis stopped with this event"/>
|
||||
</request>
|
||||
|
||||
<request name="axis_discrete">
|
||||
<description summary="axis click event">
|
||||
Discrete step information for scroll and other axes.
|
||||
|
||||
This event allows the client to extend data normally sent using the axis
|
||||
event with discrete value.
|
||||
</description>
|
||||
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
|
||||
<arg name="axis" type="uint" enum="wl_pointer.axis" summary="axis type"/>
|
||||
<arg name="value" type="fixed" summary="length of vector in touchpad coordinates"/>
|
||||
<arg name="discrete" type="int" summary="number of steps"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor" since="1">
|
||||
<description summary="destroy the virtual pointer object"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zwlr_virtual_pointer_manager_v1" version="2">
|
||||
<description summary="virtual pointer manager">
|
||||
This object allows clients to create individual virtual pointer objects.
|
||||
</description>
|
||||
|
||||
<request name="create_virtual_pointer">
|
||||
<description summary="Create a new virtual pointer">
|
||||
Creates a new virtual pointer. The optional seat is a suggestion to the
|
||||
compositor.
|
||||
</description>
|
||||
<arg name="seat" type="object" interface="wl_seat" allow-null="true"/>
|
||||
<arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor" since="1">
|
||||
<description summary="destroy the virtual pointer manager"/>
|
||||
</request>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
<request name="create_virtual_pointer_with_output" since="2">
|
||||
<description summary="Create a new virtual pointer">
|
||||
Creates a new virtual pointer. The seat and the output arguments are
|
||||
optional. If the seat argument is set, the compositor should assign the
|
||||
input device to the requested seat. If the output argument is set, the
|
||||
compositor should map the input device to the requested output.
|
||||
</description>
|
||||
<arg name="seat" type="object" interface="wl_seat" allow-null="true"/>
|
||||
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
|
||||
<arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
Reference in new issue
Block a user