Merge pull request #852 from omacom/omarchy-hyprland-titlebars

Add omarchy-hyprland-titlebars
This commit is contained in:
Ryan Hughes authored and GitHub committed 2026-10-08 02:35:38 -04:00
commit efc09808c6
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>