Compare commits

...

64 commits

Author SHA1 Message Date
b41882c169
maybe do import?
Some checks failed
Build Hyprland / Build Hyprland (Arch) (push) Has been cancelled
Build Hyprland / Code Style (push) Has been cancelled
Nix / update-inputs (push) Has been cancelled
Nix / hyprland (push) Has been cancelled
Nix / test (push) Has been cancelled
Security Checks / Flawfinder Checks (push) Has been cancelled
Nix / xdph (push) Has been cancelled
2026-03-07 15:45:46 +10:00
bcab43b181
bump glaze version
Some checks are pending
Build Hyprland / Build Hyprland (Arch) (push) Waiting to run
Build Hyprland / Code Style (push) Waiting to run
Nix / update-inputs (push) Waiting to run
Nix / hyprland (push) Waiting to run
Nix / xdph (push) Blocked by required conditions
Nix / test (push) Waiting to run
Security Checks / Flawfinder Checks (push) Waiting to run
2026-03-07 14:59:09 +10:00
08e215c9bf
temp
Some checks are pending
Build Hyprland / Build Hyprland (Arch) (push) Waiting to run
Build Hyprland / Code Style (push) Waiting to run
Nix / update-inputs (push) Waiting to run
Nix / hyprland (push) Waiting to run
Nix / xdph (push) Blocked by required conditions
Nix / test (push) Waiting to run
Security Checks / Flawfinder Checks (push) Waiting to run
2026-03-07 14:51:29 +10:00
UjinT34
4152ac76d0
renderer: refactor Texture, Framebuffer and Renderbuffer (#13437)
Some checks are pending
Build Hyprland / Build Hyprland (Arch) (push) Waiting to run
Build Hyprland / Code Style (push) Waiting to run
Nix / update-inputs (push) Waiting to run
Nix / hyprland (push) Waiting to run
Nix / xdph (push) Blocked by required conditions
Nix / test (push) Waiting to run
Security Checks / Flawfinder Checks (push) Waiting to run
Part 1 of the renderer refactors
2026-03-06 21:44:10 +00:00
UjinT34
a5858018d8
renderer: shader variants refactor (#13434)
Part 0 of renderer reworks.
2026-03-06 21:05:10 +00:00
Mikhail
8685fd7b0c
dwindle: add rotatesplit layoutmsg and tests (#13235) 2026-03-06 20:47:48 +00:00
Logan Collins
1fa157cf6d
compositor: fix missing recheckWorkArea to prevent CReservedArea assert failure (#13590) 2026-03-06 20:47:39 +00:00
Virt
e0c5710059
layerrules: add dynamically registered rules for plugins (#13331)
* layerrules: add dynamically registered rules for plugins

* be gone

* layerrules: add layer tests with waybar

* fix: use kitty layers instead of waybar
2026-03-06 20:11:42 +00:00
Nikolai Nechaev
42f0a6005b
keybinds: Remove removed keybinds (#13605)
There seems to be no reason for them to remain.

But if they are kept, no notification appears to warn
a user that a dispatcher used in their config is no
longer valid. The config remains valid, but the bindings
do not work anymore.
2026-03-06 16:33:08 +00:00
JaSha256
ae9ca17b40
pointer: fix hardware cursor rendering on rotated/flipped monitors (#13574)
Replace the broken cairo_matrix_rotate() approach with explicit
per-transform pattern matrices for all 8 wl_output_transform values.
2026-03-05 15:14:23 +00:00
Vaxry
972f23efe8
screencopy: fix isOutputBeingSSd (#13586)
use sessions instead of pending frames
2026-03-05 15:14:13 +00:00
justin4046
4c60d9df70
desktop/rules: fix empty workspace handling (#13544) 2026-03-05 15:14:05 +00:00
Vaxry
b7dfb47566
config/descriptions: add missing desc entry 2026-03-05 14:10:22 +00:00
Thedudeman
3284dd729b
algo/scrolling: add config options for focus and swapcol wrapping (#13518) 2026-03-05 14:08:40 +00:00
Ikalco
803e81ac39
screenshare: improve destroy logic of objects (#13554) 2026-03-05 14:06:55 +00:00
Vũ Xuân Trường
34c7cc7d38
i18n: update Vietnamese translations (#13489) 2026-03-04 20:02:04 +00:00
Ikalco
c47ae950f4
screencopy: fix minor crash (#13566) 2026-03-04 20:01:37 +00:00
Harsh Narayan Jha
3f169ee5de
socket2: emit kill event (hyprctl kill) (#13104) 2026-03-04 20:00:00 +00:00
Vaxry
10754745a9
render/cm: add ICC profile pipeline (#12711)
Adds an ICC profile pipeline, loading via config and applying via 3D LUTs.
2026-03-04 19:50:28 +00:00
Florian "sp1rit
8271cfc97b
core: fix i586 build (#13550)
Signed-off-by: Florian "sp1rit"​ <sp1rit@disroot.org>
2026-03-04 11:33:44 +00:00
Vaxry
c11cadd8d6
desktop/window: don't group modals 2026-03-03 21:00:33 +00:00
Vaxry
dc4b082ee8
algo/scrolling: fix rare crash 2026-03-03 20:59:18 +00:00
André Silva
edf7098345
desktop/window: fix floating windows being auto-grouped (#13475)
---------

Co-authored-by: Aqa-Ib <16420574+Aqa-Ib@users.noreply.github.com>
2026-03-03 20:56:02 +00:00
Vaxry
7299a3b0d5
hyprctl: fix workspace dynamic effect reloading (#13537)
ref https://github.com/hyprwm/Hyprland/discussions/12806
2026-03-03 13:03:47 +00:00
Vaxry
b06a4b5e13
layout/windowTarget: override maximized box status in updateGeom (#13535)
ref https://github.com/hyprwm/Hyprland/discussions/13525
2026-03-03 12:33:46 +00:00
Vaxry
3faddf40d0
algo/dwindle: don't crash on empty swapsplit (#13533)
ref https://github.com/hyprwm/Hyprland/discussions/13530
2026-03-03 11:55:57 +00:00
Vaxry
a6e3a2478c
tests/workspace: fix one test case failing 2026-03-03 11:27:16 +00:00
Vaxry
ff0b706ea3
renderer: fix crash on mirrored outputs needing recalc (#13534)
ref https://github.com/hyprwm/Hyprland/discussions/13517
2026-03-03 11:25:58 +00:00
UjinT34
4f44df7b17
algo/master: fix crash after dpms (#13522) 2026-03-03 11:06:32 +00:00
Vaxry
be03497b82
layout/algos: use binds:window_direction_monitor_fallback for moves (#13508)
ref https://github.com/hyprwm/Hyprland/discussions/13473
2026-03-02 21:39:06 +00:00
Vaxry
ff20cbf89c
algo/scrolling: fix offset on removeTarget (#13515) 2026-03-02 21:23:24 +00:00
André Silva
fe0a202137
desktop/group: respect direction when moving window out of group (#13490) 2026-03-02 21:12:27 +00:00
Vaxry
75a815fbf2
algo/dwindle: use focal point correctly for x-ws moves (#13514) 2026-03-02 21:10:21 +00:00
Vaxry
3b7401b065
algo/scroll: improve directional moves (#13423) 2026-03-02 19:31:33 +00:00
Vaxry
d4d17d5d52
compositor: damage monitors on workspace attachment updates
ref https://github.com/hyprwm/Hyprland/discussions/13386
2026-03-02 19:26:06 +00:00
Mihai Fufezan
52ece2b017
treewide: alejandra -> nixfmt 2026-03-02 21:03:44 +02:00
Vaxry
d98f7ffaf5
layout: store and preserve size and pos after fullscreen (#13500)
ref https://github.com/hyprwm/Hyprland/discussions/13401
2026-03-02 18:57:09 +00:00
Vaxry
5f650f8ed9
layout/windowTarget: don't use swar on maximized (#13501) 2026-03-02 16:54:33 +00:00
Vaxry
9f98f7440b
algo/dwindle: add back splitratio (#13498) 2026-03-02 16:21:20 +00:00
Vaxry
5cb1281035
layout/windowTarget: damage before and after moves (#13496) 2026-03-02 12:52:22 +00:00
Thedudeman
743dffd638
layout/scroll: fix configuredWidths not setting properly on new workspaces (#13476) 2026-03-02 12:51:56 +00:00
Vaxry
5c370c3333
hyprpm: fix url sanitization in add
this could've been used to exec additional commands with hyprpm
2026-03-01 21:55:12 +00:00
Vaxry
cf0d256c13
layout/windowTarget: fix size_limits_tiled (#13445)
fucks up on scroll, also, wtf
2026-03-01 19:21:53 +00:00
Yujon Pradhananga
6ebafcf107
layout/scrolling: fix size_t underflow in idxForHeight (#13465) 2026-03-01 18:19:02 +00:00
Vaxry
8ad96a95d6
screencopy: fix nullptr deref if shm format is weird 2026-03-01 15:31:22 +00:00
Vaxry
f41e3c2203
scroll: clamp column widths properly
ref https://github.com/hyprwm/Hyprland/discussions/13458
2026-03-01 10:15:22 +00:00
Vaxry
f0a80ce5e0
keybinds: fixup changegroupactive 2026-03-01 10:12:15 +00:00
Vaxry
2928d6af0a
layouts: fix crash on missed relayout updates (#13444) 2026-02-28 23:06:27 +00:00
vaxerski
93aacfc0dc [gha] Nix: update inputs 2026-02-28 22:55:48 +00:00
Vaxry
19c263e53c
screencopy: scale window region for toplevel export (#13442) 2026-02-28 22:54:10 +00:00
Vaxry
a032090098
monitor: damage old special monitor on change
ref https://github.com/hyprwm/Hyprland/discussions/13419
2026-02-28 22:51:33 +00:00
Vaxry
0b55c55f4a
monitor: update pinned window states properly on changeWorkspace (#13441)
ref https://github.com/hyprwm/Hyprland/discussions/13440
2026-02-28 22:42:06 +00:00
Vaxry
85c2764f5e
deco/border: fix damageEntire 2026-02-28 22:03:14 +00:00
Zynix
c2bed4103c
monitor: keep workspace monitor bindings on full reconnect (#13384)
When all monitors disconnect, non-active workspaces could migrate to the wrong output after reconnect. Preserve workspace ownership and re-apply assigned monitor bindings on connect.
2026-02-28 21:49:47 +00:00
LionHeartP
82729db330
build: fix build on gcc 16.x after #6b2c08d (#13429) 2026-02-28 21:45:16 +00:00
Vaxry
f12904e641
layout/algo: fix swar on removing a target (#13427)
ref https://github.com/hyprwm/Hyprland/discussions/13422
2026-02-28 18:53:36 +00:00
Vaxry
b90c61c04f
compositor: fix focus edge detection (#13425)
fixes edge detection, making it more relaxed and intuitive
2026-02-28 18:53:26 +00:00
André Silva
e333a330c0
desktop/group: fix movegroupwindow not following focus (#13426) 2026-02-28 18:19:29 +00:00
Vaxry
1c64ef06d9
desktop/window: fix idealBB reserved (#13421)
ref https://github.com/hyprwm/Hyprland/discussions/12766\#discussioncomment-15955638
2026-02-28 16:55:34 +00:00
Vaxry
6b2c08d3e8
pointer: damage entire buffer in begin of rendering hw
ref #13391
2026-02-28 15:55:30 +00:00
Christian Fredrik Johnsen
db8509dfe2
build: remove auto-generated hyprctl/hw-protocols/ files during make clear (#13399) 2026-02-28 15:51:24 +00:00
Tom Englund
d2b9957fab
format: safeguard drmGetFormat functions (#13416)
they can return null if not found.
2026-02-28 15:29:22 +00:00
Vaxry
362ea7b0f3
hyprctl: fix buffer overflowing writes to the socket 2026-02-28 15:06:10 +00:00
Vaxry
f7114016c6
desktop/rule: fix matching for content type by str 2026-02-28 15:03:49 +00:00
176 changed files with 6239 additions and 3162 deletions

View file

@ -125,6 +125,7 @@ find_package(Threads REQUIRED)
set(GLES_VERSION "GLES3") set(GLES_VERSION "GLES3")
find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION}) find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
find_package(glslang CONFIG REQUIRED)
set(AQUAMARINE_MINIMUM_VERSION 0.9.3) set(AQUAMARINE_MINIMUM_VERSION 0.9.3)
set(HYPRLANG_MINIMUM_VERSION 0.6.7) set(HYPRLANG_MINIMUM_VERSION 0.6.7)
@ -266,7 +267,8 @@ pkg_check_modules(
gbm gbm
gio-2.0 gio-2.0
re2 re2
muparser) muparser
lcms2)
find_package(hyprwayland-scanner 0.3.10 REQUIRED) find_package(hyprwayland-scanner 0.3.10 REQUIRED)
@ -478,9 +480,9 @@ function(protocolWayland)
endfunction() endfunction()
if(TARGET OpenGL::GL) if(TARGET OpenGL::GL)
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GL Threads::Threads) target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GL glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
else() else()
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GLES3 Threads::Threads) target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GLES3 glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
endif() endif()
pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4) pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4)

View file

@ -18,6 +18,7 @@ nopch:
clear: clear:
rm -rf build rm -rf build
rm -f ./protocols/*.h ./protocols/*.c ./protocols/*.cpp ./protocols/*.hpp rm -f ./protocols/*.h ./protocols/*.c ./protocols/*.cpp ./protocols/*.hpp
rm -f ./hyprctl/hw-protocols/*.cpp ./hyprctl/hw-protocols/*.hpp
all: all:
$(MAKE) clear $(MAKE) clear

18
flake.lock generated
View file

@ -16,11 +16,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1771610171, "lastModified": 1772292445,
"narHash": "sha256-+DeInuhbm6a6PpHDNUS7pozDouq2+8xSDefoNaZLW0E=", "narHash": "sha256-4F1Q7U313TKUDDovCC96m/Za4wZcJ3yqtu4eSrj8lk8=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "aquamarine", "repo": "aquamarine",
"rev": "7f9eb087703ec4acc6b288d02fa9ea3db803cd3d", "rev": "1dbbba659c1cef0b0202ce92cadfe13bae550e8f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -325,11 +325,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1771848320, "lastModified": 1772198003,
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2fc6539b481e1d2569f25f8799236694180c0993", "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -348,11 +348,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1771858127, "lastModified": 1772024342,
"narHash": "sha256-Gtre9YoYl3n25tJH2AoSdjuwcqij5CPxL3U3xysYD08=", "narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
"owner": "cachix", "owner": "cachix",
"repo": "git-hooks.nix", "repo": "git-hooks.nix",
"rev": "49bbbfc218bf3856dfa631cead3b052d78248b83", "rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -88,23 +88,28 @@
}; };
}; };
outputs = inputs @ { outputs =
inputs@{
self, self,
nixpkgs, nixpkgs,
systems, systems,
... ...
}: let }:
let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems); eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (system: pkgsFor = eachSystem (
system:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
overlays = with self.overlays; [ overlays = with self.overlays; [
hyprland-packages hyprland-packages
hyprland-extras hyprland-extras
]; ];
}); }
pkgsCrossFor = eachSystem (system: crossSystem: );
pkgsCrossFor = eachSystem (
system: crossSystem:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
inherit crossSystem; inherit crossSystem;
@ -112,29 +117,36 @@
hyprland-packages hyprland-packages
hyprland-extras hyprland-extras
]; ];
}); }
pkgsDebugFor = eachSystem (system: );
pkgsDebugFor = eachSystem (
system:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
overlays = with self.overlays; [ overlays = with self.overlays; [
hyprland-debug hyprland-debug
]; ];
}); }
pkgsDebugCrossFor = eachSystem (system: crossSystem: );
pkgsDebugCrossFor = eachSystem (
system: crossSystem:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
inherit crossSystem; inherit crossSystem;
overlays = with self.overlays; [ overlays = with self.overlays; [
hyprland-debug hyprland-debug
]; ];
}); }
in { );
in
{
overlays = import ./nix/overlays.nix { inherit self lib inputs; }; overlays = import ./nix/overlays.nix { inherit self lib inputs; };
checks = eachSystem (system: checks = eachSystem (
(lib.filterAttrs system:
(n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n)) (lib.filterAttrs (
self.packages.${system}) n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n)
) self.packages.${system})
// { // {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland; inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run { pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
@ -150,12 +162,12 @@
}; };
}; };
} }
// (import ./nix/tests inputs pkgsFor.${system})); // (import ./nix/tests inputs pkgsFor.${system})
);
packages = eachSystem (system: { packages = eachSystem (system: {
default = self.packages.${system}.hyprland; default = self.packages.${system}.hyprland;
inherit inherit (pkgsFor.${system})
(pkgsFor.${system})
# hyprland-packages # hyprland-packages
hyprland hyprland
hyprland-unwrapped hyprland-unwrapped
@ -170,9 +182,11 @@
devShells = eachSystem (system: { devShells = eachSystem (system: {
default = default =
pkgsFor.${system}.mkShell.override { pkgsFor.${system}.mkShell.override
{
inherit (self.packages.${system}.default) stdenv; inherit (self.packages.${system}.default) stdenv;
} { }
{
name = "hyprland-shell"; name = "hyprland-shell";
hardeningDisable = [ "fortify" ]; hardeningDisable = [ "fortify" ];
inputsFrom = [ pkgsFor.${system}.hyprland ]; inputsFrom = [ pkgsFor.${system}.hyprland ];

View file

@ -228,6 +228,9 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
constexpr size_t BUFFER_SIZE = 8192; constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE] = {0}; char buffer[BUFFER_SIZE] = {0};
// read all data until server closes the connection
// this handles partial writes on the server side under high load
while (true) {
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE); sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) { if (sizeWritten < 0) {
@ -237,14 +240,11 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
return 6; return 6;
} }
reply += std::string(buffer, sizeWritten); if (sizeWritten == 0) {
// server closed connection, we're done
while (sizeWritten == BUFFER_SIZE) { break;
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) {
log("Couldn't read (6)");
return 6;
} }
reply += std::string(buffer, sizeWritten); reply += std::string(buffer, sizeWritten);
} }

View file

@ -11,9 +11,9 @@ set(CMAKE_CXX_STANDARD 23)
pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.7.0) pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.7.0)
find_package(glaze 7.0.0 QUIET) find_package(glaze 6.0.1 QUIET)
if (NOT glaze_FOUND) if (NOT glaze_FOUND)
set(GLAZE_VERSION v7.0.0) set(GLAZE_VERSION v6.0.1)
message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent") message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent")
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(

View file

@ -131,9 +131,18 @@ bool CPluginManager::createSafeDirectory(const std::string& path) {
return true; return true;
} }
bool CPluginManager::validArg(const std::string& s) {
return !s.contains("'") && !s.ends_with("\\") && !s.starts_with("\\");
}
bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string& rev) { bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string& rev) {
const auto HLVER = getHyprlandVersion(); const auto HLVER = getHyprlandVersion();
if (!validArg(url) || !validArg(rev)) {
std::println(stderr, "\n{}", failureString("url or rev invalid"));
return false;
}
if (!hasDeps()) { if (!hasDeps()) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc")); std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc"));
return false; return false;
@ -198,7 +207,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
progress.printMessageAbove(infoString("Cloning {}", url)); progress.printMessageAbove(infoString("Cloning {}", url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), url, USERNAME)); std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) { if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. shell returned:\n{}", ret)); std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. shell returned:\n{}", ret));
@ -503,11 +512,11 @@ bool CPluginManager::updateHeaders(bool force) {
progress.printMessageAbove(verboseString("will shallow since: {}", SHALLOW_DATE)); progress.printMessageAbove(verboseString("will shallow since: {}", SHALLOW_DATE));
std::string ret = std::string ret =
execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : ""))); execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : "")));
if (!std::filesystem::exists(WORKINGDIR)) { if (!std::filesystem::exists(WORKINGDIR)) {
progress.printMessageAbove(failureString("Clone failed. Retrying without shallow.")); progress.printMessageAbove(failureString("Clone failed. Retrying without shallow."));
ret = execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}", getTempRoot(), HL_URL, USERNAME)); ret = execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}", getTempRoot(), HL_URL, USERNAME));
} }
if (!std::filesystem::exists(WORKINGDIR + "/.git")) { if (!std::filesystem::exists(WORKINGDIR + "/.git")) {
@ -648,7 +657,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
const auto HLVER = getHyprlandVersion(false); const auto HLVER = getHyprlandVersion(false);
CProgressBar progress; CProgressBar progress;
progress.m_iMaxSteps = REPOS.size() * 2 + 2; progress.m_iMaxSteps = (REPOS.size() * 2) + 2;
progress.m_iSteps = 0; progress.m_iSteps = 0;
progress.m_szCurrentMessage = "Updating repositories"; progress.m_szCurrentMessage = "Updating repositories";
progress.print(); progress.print();
@ -669,7 +678,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.printMessageAbove(infoString("Cloning {}", repo.url)); progress.printMessageAbove(infoString("Cloning {}", repo.url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), repo.url, USERNAME)); std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), repo.url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) { if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println("{}", failureString("could not clone repo: shell returned: {}", ret)); std::println("{}", failureString("could not clone repo: shell returned: {}", ret));
@ -679,7 +688,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
if (!repo.rev.empty()) { if (!repo.rev.empty()) {
progress.printMessageAbove(infoString("Plugin has revision set, resetting: {}", repo.rev)); progress.printMessageAbove(infoString("Plugin has revision set, resetting: {}", repo.rev));
std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules " + repo.rev); std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules \'" + repo.rev + "\'");
if (ret.compare(0, 6, "fatal:") == 0) { if (ret.compare(0, 6, "fatal:") == 0) {
std::println(stderr, "\n{}", failureString("could not check out revision {}: shell returned:\n{}", repo.rev, ret)); std::println(stderr, "\n{}", failureString("could not check out revision {}: shell returned:\n{}", repo.rev, ret));

View file

@ -81,6 +81,7 @@ class CPluginManager {
private: private:
std::string headerError(const eHeadersErrors err); std::string headerError(const eHeadersErrors err);
std::string headerErrorShort(const eHeadersErrors err); std::string headerErrorShort(const eHeadersErrors err);
bool validArg(const std::string& s);
std::expected<std::string, std::string> nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver); std::expected<std::string, std::string> nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver);

View file

@ -10,7 +10,9 @@
#include <src/managers/PointerManager.hpp> #include <src/managers/PointerManager.hpp>
#include <src/managers/input/trackpad/TrackpadGestures.hpp> #include <src/managers/input/trackpad/TrackpadGestures.hpp>
#include <src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp> #include <src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp>
#include <src/desktop/rule/layerRule/LayerRuleEffectContainer.hpp>
#include <src/desktop/rule/windowRule/WindowRuleApplicator.hpp> #include <src/desktop/rule/windowRule/WindowRuleApplicator.hpp>
#include <src/desktop/view/LayerSurface.hpp>
#include <src/Compositor.hpp> #include <src/Compositor.hpp>
#include <src/desktop/state/FocusState.hpp> #include <src/desktop/state/FocusState.hpp>
#include <src/layout/LayoutManager.hpp> #include <src/layout/LayoutManager.hpp>
@ -270,32 +272,67 @@ static SDispatchResult keybind(std::string in) {
return {}; return {};
} }
static Desktop::Rule::CWindowRuleEffectContainer::storageType ruleIDX = 0; static Desktop::Rule::CWindowRuleEffectContainer::storageType windowRuleIDX = 0;
// //
static SDispatchResult addRule(std::string in) { static SDispatchResult addWindowRule(std::string in) {
ruleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule"); windowRuleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != ruleIDX) if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != windowRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"}; return {.success = false, .error = "re-registering returned a different id?"};
return {}; return {};
} }
static SDispatchResult checkRule(std::string in) { static SDispatchResult checkWindowRule(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window(); const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW) if (!PLASTWINDOW)
return {.success = false, .error = "No window"}; return {.success = false, .error = "No window"};
if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(ruleIDX)) if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(windowRuleIDX))
return {.success = false, .error = "No rule"}; return {.success = false, .error = "No rule"};
if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[ruleIDX]->effect != "effect") if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[windowRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""}; return {.success = false, .error = "Effect isn't \"effect\""};
return {}; return {};
} }
static Desktop::Rule::CLayerRuleEffectContainer::storageType layerRuleIDX = 0;
static SDispatchResult addLayerRule(std::string in) {
layerRuleIDX = Desktop::Rule::layerEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::layerEffects()->registerEffect("plugin_rule") != layerRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkLayerRule(std::string in) {
if (g_pCompositor->m_layers.size() != 3)
return {.success = false, .error = "Layers under test not here"};
for (const auto& layer : g_pCompositor->m_layers) {
if (layer->m_namespace == "rule-layer") {
if (!layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "No rule"};
if (layer->m_ruleApplicator->m_otherProps.props[layerRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
} else if (layer->m_namespace == "norule-layer") {
if (layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "Rule even though it shouldn't"};
} else
return {.success = false, .error = "Unrecognized layer"};
}
return {};
}
static SDispatchResult floatingFocusOnFullscreen(std::string in) { static SDispatchResult floatingFocusOnFullscreen(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window(); const auto PLASTWINDOW = Desktop::focusState()->window();
@ -325,8 +362,10 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:click", ::click); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:click", ::click);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_rule", ::addRule); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_window_rule", ::addWindowRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_rule", ::checkRule); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_window_rule", ::checkWindowRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_layer_rule", ::addLayerRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_layer_rule", ::checkLayerRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:floating_focus_on_fullscreen", ::floatingFocusOnFullscreen); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:floating_focus_on_fullscreen", ::floatingFocusOnFullscreen);
// init mouse // init mouse

View file

@ -39,6 +39,16 @@ namespace Colors {
TESTS_PASSED++; \ TESTS_PASSED++; \
} }
#define EXPECT_NOT(expr, val) \
if (const auto RESULT = expr; RESULT == (val)) { \
NLog::log("{}Failed: {}{}, expected not {}, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, val, RESULT, __FILE__, __LINE__); \
ret = 1; \
TESTS_FAILED++; \
} else { \
NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, val); \
TESTS_PASSED++; \
}
#define EXPECT_VECTOR2D(expr, val) \ #define EXPECT_VECTOR2D(expr, val) \
do { \ do { \
const auto& RESULT = expr; \ const auto& RESULT = expr; \

View file

@ -118,6 +118,33 @@ static bool test() {
Tests::killAllWindows(); Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0); EXPECT(Tests::windowCount(), 0);
// test that child windows (shouldBeFloated) are not auto-grouped
NLog::log("{}Test child windows are not auto-grouped", Colors::GREEN);
auto kitty = Tests::spawnKitty();
if (!kitty) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// create group and enable auto-grouping
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword group:auto_group true"));
SClient client2;
if (!startClient(client2))
return false;
EXPECT(Tests::windowCount(), 2);
createChild(client2);
EXPECT(Tests::windowCount(), 3);
// child has set_parent so shouldBeFloated returns true, it should not be auto-grouped
EXPECT_COUNT_STRING(getFromSocket("/clients"), "grouped: 0", 1);
stopClient(client2);
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret; return !ret;
} }

View file

@ -64,16 +64,16 @@ static void test13349() {
{ {
auto str = getFromSocket("/activewindow"); auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,547"); EXPECT_CONTAINS(str, "at: 497,22");
EXPECT_CONTAINS(str, "size: 931,511"); EXPECT_CONTAINS(str, "size: 456,1036");
} }
OK(getFromSocket("/dispatch movewindow r")); OK(getFromSocket("/dispatch movewindow r"));
{ {
auto str = getFromSocket("/activewindow"); auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 967,547"); EXPECT_CONTAINS(str, "at: 967,22");
EXPECT_CONTAINS(str, "size: 931,511"); EXPECT_CONTAINS(str, "size: 456,1036");
} }
// clean up // clean up
@ -81,6 +81,152 @@ static void test13349() {
Tests::killAllWindows(); Tests::killAllWindows();
} }
static void testSplit() {
// Test various split methods
Tests::spawnKitty("a");
// these must not crash
EXPECT_NOT(getFromSocket("/dispatch layoutmsg swapsplit"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio 1 exact"), "ok");
Tests::spawnKitty("b");
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg splitratio -0.2"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 743,1036");
}
OK(getFromSocket("/dispatch layoutmsg splitratio 1.6 exact"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1495,1036");
}
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio fhne exact"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio exact"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio -....9"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio ..9"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio"), "ok");
OK(getFromSocket("/dispatch layoutmsg togglesplit"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,823");
}
OK(getFromSocket("/dispatch layoutmsg swapsplit"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,859");
EXPECT_CONTAINS(str, "size: 1876,199");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testRotatesplit() {
OK(getFromSocket("r/keyword general:gaps_in 0"));
OK(getFromSocket("r/keyword general:gaps_out 0"));
OK(getFromSocket("r/keyword general:border_size 0"));
for (auto const& win : {"a", "b"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test 4 repeated rotations by 90 degrees
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test different angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit 180"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 270"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 360"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
// test negative angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit -90"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit -180"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}
static bool test() { static bool test() {
NLog::log("{}Testing Dwindle layout", Colors::GREEN); NLog::log("{}Testing Dwindle layout", Colors::GREEN);
@ -91,6 +237,12 @@ static bool test() {
NLog::log("{}Testing #13349", Colors::GREEN); NLog::log("{}Testing #13349", Colors::GREEN);
test13349(); test13349();
NLog::log("{}Testing splits", Colors::GREEN);
testSplit();
NLog::log("{}Testing rotatesplit", Colors::GREEN);
testRotatesplit();
// clean up // clean up
NLog::log("Cleaning up", Colors::YELLOW); NLog::log("Cleaning up", Colors::YELLOW);
getFromSocket("/dispatch workspace 1"); getFromSocket("/dispatch workspace 1");

View file

@ -127,6 +127,34 @@ static bool test() {
ret = 1; ret = 1;
} }
// test movegroupwindow: focus should follow the moved window
NLog::log("{}Test movegroupwindow focus follows window", Colors::YELLOW);
try {
auto str = getFromSocket("/activewindow");
auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
OK(getFromSocket("/dispatch movegroupwindow f"));
str = getFromSocket("/activewindow");
auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
EXPECT(activeAfterMove, activeBeforeMove);
} catch (...) {
NLog::log("{}Fail at getting prop", Colors::RED);
ret = 1;
}
// and backwards
NLog::log("{}Test movegroupwindow backwards", Colors::YELLOW);
try {
auto str = getFromSocket("/activewindow");
auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
OK(getFromSocket("/dispatch movegroupwindow b"));
str = getFromSocket("/activewindow");
auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
EXPECT(activeAfterMove, activeBeforeMove);
} catch (...) {
NLog::log("{}Fail at getting prop", Colors::RED);
ret = 1;
}
NLog::log("{}Disable autogrouping", Colors::YELLOW); NLog::log("{}Disable autogrouping", Colors::YELLOW);
OK(getFromSocket("/keyword group:auto_group false")); OK(getFromSocket("/keyword group:auto_group false"));
@ -173,6 +201,99 @@ static bool test() {
NLog::log("{}Expecting 0 windows", Colors::YELLOW); NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0); EXPECT(Tests::windowCount(), 0);
// test movewindoworgroup: direction should be respected when extracting from group
NLog::log("{}Test movewindoworgroup respects direction out of group", Colors::YELLOW);
OK(getFromSocket("/keyword group:groupbar:enabled 0"));
{
auto kittyE = Tests::spawnKitty();
if (!kittyE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// group kitty, and new windows should be auto-grouped
OK(getFromSocket("/dispatch togglegroup"));
auto kittyF = Tests::spawnKitty();
if (!kittyF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(Tests::windowCount(), 2);
// both windows should be grouped at the same position
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 2);
}
// move active window out of group to the right
NLog::log("{}Test movewindoworgroup r", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup r"));
// the group should stay at x=22, the extracted window should be to the right
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
// move it back into the group
OK(getFromSocket("/dispatch moveintogroup l"));
// move active window out of group downward
NLog::log("{}Test movewindoworgroup d", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup d"));
// the group should stay at y=22, the extracted window should be below
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
}
// test that we deny a floated window getting auto-grouped into a tiled group.
NLog::log("{}Test that we deny a floated window getting auto-grouped into a tiled group.", Colors::GREEN);
OK(getFromSocket("/keyword windowrule[kitty-tiled]:match:class kitty_tiled"));
OK(getFromSocket("/keyword windowrule[kitty-tiled]:tile yes"));
auto kittyProcE = Tests::spawnKitty("kitty_tiled");
if (!kittyProcE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:match:class kitty_floated"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:float yes"));
auto kittyProcF = Tests::spawnKitty("kitty_floated");
if (!kittyProcF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(Tests::windowCount(), 2);
{
auto clients = getFromSocket("/clients");
auto classPos = clients.find("class: kitty_floated");
if (classPos == std::string::npos) {
NLog::log("{}Could not find kitty_floated in clients output", Colors::RED);
ret = 1;
} else {
auto entryStart = clients.rfind("Window ", classPos);
auto entryEnd = clients.find("\n\n", classPos);
auto windowEntry = clients.substr(entryStart, entryEnd - entryStart);
EXPECT_CONTAINS(windowEntry, "floating: 1");
EXPECT_CONTAINS(windowEntry, "grouped: 0");
}
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret; return !ret;
} }

View file

@ -0,0 +1,53 @@
#include "../../Log.hpp"
#include "../shared.hpp"
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
static int ret = 0;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
static bool spawnLayer(const std::string& namespace_) {
NLog::log("{}Spawning kitty layer {}", Colors::YELLOW, namespace_);
if (!Tests::spawnLayerKitty(namespace_)) {
NLog::log("{}Error: {} layer did not spawn", Colors::RED, namespace_);
return false;
}
return true;
}
static bool test() {
NLog::log("{}Testing plugin layerrules", Colors::GREEN);
if (!spawnLayer("rule-layer"))
return false;
OK(getFromSocket("/dispatch plugin:test:add_layer_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword layerrule match:namespace rule-layer, plugin_rule effect"));
if (!spawnLayer("rule-layer"))
return false;
if (!spawnLayer("norule-layer"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_layer_rule"));
OK(getFromSocket("/reload"));
NLog::log("{}Killing all layers", Colors::YELLOW);
Tests::killAllLayers();
NLog::log("{}Expecting 0 layers", Colors::YELLOW);
EXPECT(Tests::layerCount(), 0);
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -0,0 +1,127 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void swar() {
OK(getFromSocket("/keyword layout:single_window_aspect_ratio 1 1"));
Tests::spawnKitty();
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
Tests::spawnKitty();
OK(getFromSocket("/dispatch killwindow activewindow"));
Tests::waitUntilWindowsN(1);
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
// don't use swar on maximized
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
// Don't crash when focus after global geometry changes
static void testCrashOnGeomUpdate() {
Tests::spawnKitty();
Tests::spawnKitty();
Tests::spawnKitty();
// move the layout
OK(getFromSocket("/keyword monitor HEADLESS-2,1920x1080@60,1000x0,1"));
// shouldnt crash
OK(getFromSocket("/dispatch movefocus r"));
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
// Test if size + pos is preserved after fs cycle
static void testPosPreserve() {
Tests::spawnKitty();
OK(getFromSocket("/dispatch setfloating class:kitty"));
OK(getFromSocket("/dispatch resizewindowpixel exact 1337 69, class:kitty"));
OK(getFromSocket("/dispatch movewindowpixel exact 420 420, class:kitty"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 420,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch fullscreen"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 581,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch fullscreen"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 581,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing layout generic", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace 10"));
// test
NLog::log("{}Testing `single_window_aspect_ratio`", Colors::GREEN);
swar();
testCrashOnGeomUpdate();
testPosPreserve();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -97,6 +97,67 @@ static void focusMasterPrevious() {
Tests::killAllWindows(); Tests::killAllWindows();
} }
static void testFsBehavior() {
// Master will re-send data to fullscreen / maximized windows, which can interfere with misc:on_focus_under_fullscreen
// check that it doesn't.
for (auto const& win : {"master", "slave1", "slave2"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:master"));
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: master");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1"));
Tests::spawnKitty("new_master");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0"));
Tests::spawnKitty("ignored");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2"));
Tests::spawnKitty("vaxwashere");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: vaxwashere");
EXPECT_CONTAINS(str, "fullscreen: 0");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() { static bool test() {
NLog::log("{}Testing Master layout", Colors::GREEN); NLog::log("{}Testing Master layout", Colors::GREEN);
@ -108,6 +169,9 @@ static bool test() {
NLog::log("{}Testing `focusmaster previous` layoutmsg", Colors::GREEN); NLog::log("{}Testing `focusmaster previous` layoutmsg", Colors::GREEN);
focusMasterPrevious(); focusMasterPrevious();
NLog::log("{}Testing fs behavior", Colors::GREEN);
testFsBehavior();
// clean up // clean up
NLog::log("Cleaning up", Colors::YELLOW); NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1")); OK(getFromSocket("/dispatch workspace 1"));

View file

@ -0,0 +1,228 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void testFocusCycling() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movefocus u"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testFocusWrapping() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_focus to true
OK(getFromSocket("/keyword scrolling:wrap_focus true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
// set wrap_focus to false
OK(getFromSocket("/keyword scrolling:wrap_focus false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testSwapcolWrapping() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_swapcol to true
OK(getFromSocket("/keyword scrolling:wrap_swapcol true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_swapcol to false
OK(getFromSocket("/keyword scrolling:wrap_swapcol false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing Scroll layout", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace name:scroll"));
OK(getFromSocket("/keyword general:layout scrolling"));
// test
NLog::log("{}Testing focus cycling", Colors::GREEN);
testFocusCycling();
// test
NLog::log("{}Testing focus wrap", Colors::GREEN);
testFocusWrapping();
// test
NLog::log("{}Testing swapcol wrap", Colors::GREEN);
testSwapcolWrapping();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -93,9 +93,9 @@ static void testSwapWindow() {
{ {
getFromSocket("/dispatch focuswindow class:kitty_A"); getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:"); auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:");
NLog::log("{}Testing kitty_A {}, swapwindow with direction 'l'", Colors::YELLOW, pos); NLog::log("{}Testing kitty_A {}, swapwindow with direction 'r'", Colors::YELLOW, pos);
OK(getFromSocket("/dispatch swapwindow l")); OK(getFromSocket("/dispatch swapwindow r"));
OK(getFromSocket("/dispatch focuswindow class:kitty_B")); OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos)); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
@ -566,6 +566,98 @@ static bool testWindowRuleFocusOnActivate() {
return true; return true;
} }
// tests if a pinned window contains the valid workspace after change
static bool testPinnedWorkspacesValid() {
OK(getFromSocket("/reload"));
getFromSocket("/dispatch workspace 1337");
if (!spawnKitty("kitty")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch setfloating class:kitty"));
OK(getFromSocket("/dispatch pin class:kitty"));
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1337"), true);
EXPECT(str.contains("pinned: 1"), true);
}
getFromSocket("/dispatch workspace 1338");
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1338"), true);
EXPECT(str.contains("pinned: 1"), true);
}
OK(getFromSocket("/dispatch settiled class:kitty"))
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1338"), true);
EXPECT(str.contains("pinned: 0"), true);
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return true;
}
static bool testWindowRuleWorkspaceEmpty() {
NLog::log("{}Testing windowrule workspace empty", Colors::YELLOW);
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class kitty_A, workspace empty"));
OK(getFromSocket("/keyword windowrule match:class kitty_B, workspace emptyn"));
getFromSocket("/dispatch workspace 3");
if (!spawnKitty("kitty")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 3"), true);
}
if (!spawnKitty("kitty_A")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1"), true);
}
getFromSocket("/dispatch workspace 3");
if (!spawnKitty("kitty_B")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 4"), true);
}
Tests::killAllWindows();
return true;
}
static bool test() { static bool test() {
NLog::log("{}Testing windows", Colors::GREEN); NLog::log("{}Testing windows", Colors::GREEN);
@ -994,7 +1086,7 @@ static bool test() {
OK(getFromSocket("/reload")); OK(getFromSocket("/reload"));
Tests::killAllWindows(); Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule")); OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload")); OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect")); OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect"));
@ -1002,12 +1094,12 @@ static bool test() {
if (!spawnKitty("plugin_kitty")) if (!spawnKitty("plugin_kitty"))
return false; return false;
OK(getFromSocket("/dispatch plugin:test:check_rule")); OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload")); OK(getFromSocket("/reload"));
Tests::killAllWindows(); Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule")); OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload")); OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty")); OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty"));
@ -1016,7 +1108,7 @@ static bool test() {
if (!spawnKitty("plugin_kitty")) if (!spawnKitty("plugin_kitty"))
return false; return false;
OK(getFromSocket("/dispatch plugin:test:check_rule")); OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload")); OK(getFromSocket("/reload"));
Tests::killAllWindows(); Tests::killAllWindows();
@ -1028,6 +1120,8 @@ static bool test() {
testGroupFallbackFocus(); testGroupFallbackFocus();
testInitialFloatSize(); testInitialFloatSize();
testWindowRuleFocusOnActivate(); testWindowRuleFocusOnActivate();
testPinnedWorkspacesValid();
testWindowRuleWorkspaceEmpty();
NLog::log("{}Reloading config", Colors::YELLOW); NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload")); OK(getFromSocket("/reload"));

View file

@ -255,6 +255,144 @@ static void testMultimonBAF() {
Tests::killAllWindows(); Tests::killAllWindows();
} }
static void testMultimonFocus() {
NLog::log("{}Testing multimon focus and move", Colors::YELLOW);
OK(getFromSocket("/keyword input:follow_mouse 0"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-3"));
OK(getFromSocket("/dispatch workspace 8"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
OK(getFromSocket("/dispatch workspace 7"));
for (auto const& win : {"a", "b"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
Tests::spawnKitty("c");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
OK(getFromSocket("/keyword general:no_focus_fallback true"));
OK(getFromSocket("/keyword binds:window_direction_monitor_fallback false"));
EXPECT_NOT(getFromSocket("/dispatch movefocus l"), "ok");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static void testDynamicWsEffects() {
// test dynamic workspace effects, they shouldn't lag
OK(getFromSocket("/dispatch workspace 69"));
Tests::spawnKitty("bitch");
OK(getFromSocket("r/keyword workspace 69,bordersize:20"));
OK(getFromSocket("r/keyword workspace 69,rounding:false"));
EXPECT(getFromSocket("/getprop class:bitch border_size"), "20");
EXPECT(getFromSocket("/getprop class:bitch rounding"), "0");
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static bool test() { static bool test() {
NLog::log("{}Testing workspaces", Colors::GREEN); NLog::log("{}Testing workspaces", Colors::GREEN);
@ -594,13 +732,14 @@ static bool test() {
Tests::killAllWindows(); Tests::killAllWindows();
testMultimonBAF(); testMultimonBAF();
testMultimonFocus();
// destroy the headless output // destroy the headless output
OK(getFromSocket("/output remove HEADLESS-3")); OK(getFromSocket("/output remove HEADLESS-3"));
testSpecialWorkspaceFullscreen(); testSpecialWorkspaceFullscreen();
testAsymmetricGaps(); testAsymmetricGaps();
testDynamicWsEffects();
NLog::log("{}Expecting 0 windows", Colors::YELLOW); NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0); EXPECT(Tests::windowCount(), 0);

View file

@ -3,6 +3,7 @@
#include <cerrno> #include <cerrno>
#include <thread> #include <thread>
#include <print> #include <print>
#include <fstream>
#include "../shared.hpp" #include "../shared.hpp"
#include "../hyprctlCompat.hpp" #include "../hyprctlCompat.hpp"
@ -39,6 +40,38 @@ CUniquePointer<CProcess> Tests::spawnKitty(const std::string& class_, const std:
return kitty; return kitty;
} }
CUniquePointer<CProcess> Tests::spawnLayerKitty(const std::string& namespace_, const std::vector<std::string> args) {
std::vector<std::string> programArgs = args;
if (!namespace_.empty()) {
programArgs.insert(programArgs.begin(), "--class");
programArgs.insert(programArgs.begin() + 1, namespace_);
}
programArgs.insert(programArgs.begin(), "+kitten");
programArgs.insert(programArgs.begin() + 1, "panel");
CUniquePointer<CProcess> kitty = makeUnique<CProcess>("kitty", programArgs);
kitty->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
kitty->runAsync();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// wait while the layer spawns
int counter = 0;
while (processAlive(kitty->pid()) && countOccurrences(getFromSocket("/layers"), std::format("pid: {}", kitty->pid())) == 0) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50)
return nullptr;
}
if (!processAlive(kitty->pid()))
return nullptr;
return kitty;
}
bool Tests::processAlive(pid_t pid) { bool Tests::processAlive(pid_t pid) {
errno = 0; errno = 0;
int ret = kill(pid, 0); int ret = kill(pid, 0);
@ -96,6 +129,38 @@ void Tests::waitUntilWindowsN(int n) {
} }
} }
int Tests::layerCount() {
return countOccurrences(getFromSocket("/layers"), "namespace: ");
}
bool Tests::killAllLayers() {
auto str = getFromSocket("/layers");
auto pos = str.find("pid: ");
while (pos != std::string::npos) {
auto pid = stoi(str.substr(pos + 5, str.find('\n', pos)));
kill(pid, 15);
// we need to wait for a bit because for some reason otherwise we'll end up
// with layers with pid -1 if they are all removed at the same time
std::this_thread::sleep_for(std::chrono::milliseconds(100));
pos = str.find("pid: ", pos + 5);
}
int counter = 0;
while (Tests::layerCount() != 0) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
std::println("{}Timed out waiting for layers to close", Colors::RED);
return false;
}
}
return true;
}
std::string Tests::execAndGet(const std::string& cmd) { std::string Tests::execAndGet(const std::string& cmd) {
CProcess proc("/bin/sh", {"-c", cmd}); CProcess proc("/bin/sh", {"-c", cmd});
@ -105,3 +170,14 @@ std::string Tests::execAndGet(const std::string& cmd) {
return proc.stdOut(); return proc.stdOut();
} }
bool Tests::writeFile(const std::string& name, const std::string& contents) {
std::ofstream of(name, std::ios::trunc);
if (!of.good())
return false;
of << contents;
of.close();
return true;
}

View file

@ -9,10 +9,14 @@
//NOLINTNEXTLINE //NOLINTNEXTLINE
namespace Tests { namespace Tests {
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnKitty(const std::string& class_ = "", const std::vector<std::string> args = {}); Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnKitty(const std::string& class_ = "", const std::vector<std::string> args = {});
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnLayerKitty(const std::string& namespace_ = "", const std::vector<std::string> args = {});
bool processAlive(pid_t pid); bool processAlive(pid_t pid);
int windowCount(); int windowCount();
int countOccurrences(const std::string& in, const std::string& what); int countOccurrences(const std::string& in, const std::string& what);
bool killAllWindows(); bool killAllWindows();
void waitUntilWindowsN(int n); void waitUntilWindowsN(int n);
int layerCount();
bool killAllLayers();
std::string execAndGet(const std::string& cmd); std::string execAndGet(const std::string& cmd);
bool writeFile(const std::string& name, const std::string& contents);
}; };

View file

@ -179,6 +179,17 @@ master {
new_status = master new_status = master
} }
scrolling {
fullscreen_on_one_column = true
column_width = 0.5
focus_fit_method = 1
follow_focus = true
follow_min_visible = 1
explicit_column_widths = 0.25, 0.333, 0.5, 0.667, 0.75, 1.0
wrap_focus = true
wrap_swapcol = true
}
# https://wiki.hyprland.org/Configuring/Variables/#misc # https://wiki.hyprland.org/Configuring/Variables/#misc
misc { misc {
force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers
@ -239,7 +250,7 @@ bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating, bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu bind = $mainMod, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, togglesplit, # dwindle bind = $mainMod, J, layoutmsg, togglesplit, # dwindle
# Move focus with mainMod + arrow keys # Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l bind = $mainMod, left, movefocus, l

View file

@ -12,6 +12,7 @@
epoll-shim, epoll-shim,
git, git,
glaze-hyprland, glaze-hyprland,
glslang,
gtest, gtest,
hyprcursor, hyprcursor,
hyprgraphics, hyprgraphics,
@ -21,6 +22,7 @@
hyprutils, hyprutils,
hyprwayland-scanner, hyprwayland-scanner,
hyprwire, hyprwire,
lcms2,
libGL, libGL,
libdrm, libdrm,
libexecinfo, libexecinfo,
@ -64,8 +66,20 @@
inherit (builtins) foldl' readFile; inherit (builtins) foldl' readFile;
inherit (lib.asserts) assertMsg; inherit (lib.asserts) assertMsg;
inherit (lib.attrsets) mapAttrsToList; inherit (lib.attrsets) mapAttrsToList;
inherit (lib.lists) flatten concatLists optional optionals; inherit
inherit (lib.strings) makeBinPath optionalString cmakeBool trim; (lib.lists)
flatten
concatLists
optional
optionals
;
inherit
(lib.strings)
makeBinPath
optionalString
cmakeBool
trim
;
fs = lib.fileset; fs = lib.fileset;
adapters = flatten [ adapters = flatten [
@ -77,7 +91,8 @@
in in
assert assertMsg (!nvidiaPatches) "The option `nvidiaPatches` has been removed."; assert assertMsg (!nvidiaPatches) "The option `nvidiaPatches` has been removed.";
assert assertMsg (!enableNvidiaPatches) "The option `enableNvidiaPatches` has been removed."; assert assertMsg (!enableNvidiaPatches) "The option `enableNvidiaPatches` has been removed.";
assert assertMsg (!hidpiXWayland) "The option `hidpiXWayland` has been removed. Please refer https://wiki.hypr.land/Configuring/XWayland"; assert assertMsg (!hidpiXWayland)
"The option `hidpiXWayland` has been removed. Please refer https://wiki.hypr.land/Configuring/XWayland";
assert assertMsg (!legacyRenderer) "The option `legacyRenderer` has been removed. Legacy renderer is no longer supported."; assert assertMsg (!legacyRenderer) "The option `legacyRenderer` has been removed. Legacy renderer is no longer supported.";
assert assertMsg (!withHyprtester) "The option `withHyprtester` has been removed. Hyprtester is always built now."; assert assertMsg (!withHyprtester) "The option `withHyprtester` has been removed. Hyprtester is always built now.";
customStdenv.mkDerivation (finalAttrs: { customStdenv.mkDerivation (finalAttrs: {
@ -90,7 +105,8 @@ in
fs.intersection fs.intersection
# allows non-flake builds to only include files tracked by git # allows non-flake builds to only include files tracked by git
(fs.gitTracked ../.) (fs.gitTracked ../.)
(fs.unions (flatten [ (
fs.unions (flatten [
../assets/hyprland-portals.conf ../assets/hyprland-portals.conf
../assets/install ../assets/install
../hyprctl ../hyprctl
@ -106,8 +122,12 @@ in
(fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example) (fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example)
(fs.fileFilter (file: file.hasExt "sh") ../scripts) (fs.fileFilter (file: file.hasExt "sh") ../scripts)
(fs.fileFilter (file: file.name == "CMakeLists.txt") ../.) (fs.fileFilter (file: file.name == "CMakeLists.txt") ../.)
(optional withTests [../tests ../hyprtester]) (optional withTests [
])); ../tests
../hyprtester
])
])
);
}; };
postPatch = '' postPatch = ''
@ -123,7 +143,10 @@ in
GIT_COMMITS = revCount; GIT_COMMITS = revCount;
GIT_COMMIT_DATE = date; GIT_COMMIT_DATE = date;
GIT_COMMIT_HASH = commit; GIT_COMMIT_HASH = commit;
GIT_DIRTY = if (commit == "") then "clean" else "dirty"; GIT_DIRTY =
if (commit == "")
then "clean"
else "dirty";
GIT_TAG = "v${trim (readFile "${finalAttrs.src}/VERSION")}"; GIT_TAG = "v${trim (readFile "${finalAttrs.src}/VERSION")}";
}; };
@ -151,6 +174,7 @@ in
cairo cairo
git git
glaze-hyprland glaze-hyprland
glslang
gtest gtest
hyprcursor hyprcursor
hyprgraphics hyprgraphics
@ -158,6 +182,7 @@ in
hyprlang hyprlang
hyprutils hyprutils
hyprwire hyprwire
lcms2
libdrm libdrm
libgbm libgbm
libGL libGL
@ -218,12 +243,14 @@ in
postInstall = '' postInstall = ''
${optionalString wrapRuntimeDeps '' ${optionalString wrapRuntimeDeps ''
wrapProgram $out/bin/Hyprland \ wrapProgram $out/bin/Hyprland \
--suffix PATH : ${makeBinPath [ --suffix PATH : ${
makeBinPath [
binutils binutils
hyprland-guiutils hyprland-guiutils
pciutils pciutils
pkgconf pkgconf
]} ]
}
''} ''}
${optionalString withTests '' ${optionalString withTests ''

View file

@ -2,7 +2,7 @@
writeShellApplication, writeShellApplication,
deadnix, deadnix,
statix, statix,
alejandra, nixfmt,
llvmPackages_19, llvmPackages_19,
fd, fd,
}: }:
@ -11,7 +11,7 @@ writeShellApplication {
runtimeInputs = [ runtimeInputs = [
deadnix deadnix
statix statix
alejandra nixfmt
llvmPackages_19.clang-tools llvmPackages_19.clang-tools
fd fd
]; ];
@ -24,14 +24,14 @@ writeShellApplication {
nix_format() { nix_format() {
if [ "$*" = 0 ]; then if [ "$*" = 0 ]; then
fd '.*\.nix' . -E "$excludes" -x statix fix -- {} \; fd '.*\.nix' . -E "$excludes" -x statix fix -- {} \;
fd '.*\.nix' . -E "$excludes" -X deadnix -e -- {} \; -X alejandra {} \; fd '.*\.nix' . -E "$excludes" -X deadnix -e -- {} \; -X nixfmt {} \;
elif [ -d "$1" ]; then elif [ -d "$1" ]; then
fd '.*\.nix' "$1" -E "$excludes" -i -x statix fix -- {} \; fd '.*\.nix' "$1" -E "$excludes" -i -x statix fix -- {} \;
fd '.*\.nix' "$1" -E "$excludes" -i -X deadnix -e -- {} \; -X alejandra {} \; fd '.*\.nix' "$1" -E "$excludes" -i -X deadnix -e -- {} \; -X nixfmt {} \;
else else
statix fix -- "$1" statix fix -- "$1"
deadnix -e "$1" deadnix -e "$1"
alejandra "$1" nixfmt "$1"
fi fi
} }

View file

@ -1,13 +1,15 @@
self: { self:
config, {
lib, lib,
pkgs, pkgs,
... ...
}: let }:
let
inherit (pkgs.stdenv.hostPlatform) system; inherit (pkgs.stdenv.hostPlatform) system;
package = self.packages.${system}.default; package = self.packages.${system}.default;
in { in
{
config = { config = {
wayland.windowManager.hyprland.package = lib.mkDefault package; wayland.windowManager.hyprland.package = lib.mkDefault package;
}; };

View file

@ -1,4 +1,5 @@
lib: let lib:
let
inherit (lib) inherit (lib)
attrNames attrNames
filterAttrs filterAttrs
@ -81,11 +82,19 @@ lib: let
::: :::
*/ */
toHyprlang = { toHyprlang =
topCommandsPrefixes ? ["$" "bezier"], {
topCommandsPrefixes ? [
"$"
"bezier"
],
bottomCommandsPrefixes ? [ ], bottomCommandsPrefixes ? [ ],
}: attrs: let }:
toHyprlang' = attrs: let attrs:
let
toHyprlang' =
attrs:
let
# Specially configured `toKeyValue` generator with support for duplicate keys # Specially configured `toKeyValue` generator with support for duplicate keys
# and a legible key-value separator. # and a legible key-value separator.
mkCommands = generators.toKeyValue { mkCommands = generators.toKeyValue {
@ -99,8 +108,7 @@ lib: let
commands = flattenAttrs (p: k: "${p}:${k}") attrs; commands = flattenAttrs (p: k: "${p}:${k}") attrs;
# General filtering function to check if a key starts with any prefix in a given list. # General filtering function to check if a key starts with any prefix in a given list.
filterCommands = list: n: filterCommands = list: n: foldl (acc: prefix: acc || hasPrefix prefix n) false list;
foldl (acc: prefix: acc || hasPrefix prefix n) false list;
# Partition keys into top commands and the rest # Partition keys into top commands and the rest
result = partition (filterCommands topCommandsPrefixes) (attrNames commands); result = partition (filterCommands topCommandsPrefixes) (attrNames commands);
@ -174,24 +182,19 @@ lib: let
``` ```
::: :::
*/ */
flattenAttrs = pred: attrs: let flattenAttrs =
flattenAttrs' = prefix: attrs: pred: attrs:
let
flattenAttrs' =
prefix: attrs:
builtins.foldl' ( builtins.foldl' (
acc: key: let acc: key:
let
value = attrs.${key}; value = attrs.${key};
newKey = newKey = if prefix == "" then key else pred prefix key;
if prefix == ""
then key
else pred prefix key;
in in
acc acc // (if builtins.isAttrs value then flattenAttrs' newKey value else { "${newKey}" = value; })
// (
if builtins.isAttrs value
then flattenAttrs' newKey value
else {"${newKey}" = value;}
)
) { } (builtins.attrNames attrs); ) { } (builtins.attrNames attrs);
in in
flattenAttrs' "" attrs; flattenAttrs' "" attrs;

View file

@ -1,13 +1,16 @@
inputs: { inputs:
{
config, config,
lib, lib,
pkgs, pkgs,
... ...
}: let }:
let
inherit (pkgs.stdenv.hostPlatform) system; inherit (pkgs.stdenv.hostPlatform) system;
selflib = import ./lib.nix lib; selflib = import ./lib.nix lib;
cfg = config.programs.hyprland; cfg = config.programs.hyprland;
in { in
{
options = { options = {
programs.hyprland = { programs.hyprland = {
plugins = lib.mkOption { plugins = lib.mkOption {
@ -20,7 +23,9 @@ in {
}; };
settings = lib.mkOption { settings = lib.mkOption {
type = with lib.types; let type =
with lib.types;
let
valueType = valueType =
nullOr (oneOf [ nullOr (oneOf [
bool bool
@ -92,8 +97,15 @@ in {
topPrefixes = lib.mkOption { topPrefixes = lib.mkOption {
type = with lib.types; listOf str; type = with lib.types; listOf str;
default = ["$" "bezier"]; default = [
example = ["$" "bezier" "source"]; "$"
"bezier"
];
example = [
"$"
"bezier"
"source"
];
description = '' description = ''
List of prefix of attributes to put at the top of the config. List of prefix of attributes to put at the top of the config.
''; '';
@ -117,20 +129,22 @@ in {
}; };
} }
(lib.mkIf cfg.enable { (lib.mkIf cfg.enable {
environment.etc."xdg/hypr/hyprland.conf" = let environment.etc."xdg/hypr/hyprland.conf" =
let
shouldGenerate = cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ]; shouldGenerate = cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ];
pluginsToHyprlang = plugins: pluginsToHyprlang =
selflib.toHyprlang { _plugins:
selflib.toHyprlang
{
topCommandsPrefixes = cfg.topPrefixes; topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes; bottomCommandsPrefixes = cfg.bottomPrefixes;
} }
{ {
"exec-once" = let "exec-once" =
mkEntry = entry: let
if lib.types.package.check entry mkEntry =
then "${entry}/lib/lib${entry.pname}.so" entry: if lib.types.package.check entry then "${entry}/lib/lib${entry.pname}.so" else entry;
else entry;
hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl"; hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl";
in in
map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins; map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins;
@ -138,14 +152,13 @@ in {
in in
lib.mkIf shouldGenerate { lib.mkIf shouldGenerate {
text = text =
lib.optionalString (cfg.plugins != []) lib.optionalString (cfg.plugins != [ ]) (pluginsToHyprlang cfg.plugins)
(pluginsToHyprlang cfg.plugins) + lib.optionalString (cfg.settings != { }) (
+ lib.optionalString (cfg.settings != {}) selflib.toHyprlang {
(selflib.toHyprlang {
topCommandsPrefixes = cfg.topPrefixes; topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes; bottomCommandsPrefixes = cfg.bottomPrefixes;
} } cfg.settings
cfg.settings) )
+ lib.optionalString (cfg.extraConfig != "") cfg.extraConfig; + lib.optionalString (cfg.extraConfig != "") cfg.extraConfig;
}; };
}) })

View file

@ -2,20 +2,27 @@
self, self,
lib, lib,
inputs, inputs,
}: let }:
mkDate = longDate: (lib.concatStringsSep "-" [ let
mkDate =
longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate) (builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate) (builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate) (builtins.substring 6 2 longDate)
]); ]);
ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION); ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION);
in { in
{
# Contains what a user is most likely to care about: # Contains what a user is most likely to care about:
# Hyprland itself, XDPH and the Share Picker. # Hyprland itself, XDPH and the Share Picker.
default = lib.composeManyExtensions (with self.overlays; [ default = lib.composeManyExtensions (
with self.overlays;
[
hyprland-packages hyprland-packages
hyprland-extras hyprland-extras
]); ]
);
# Packages for variations of Hyprland, dependencies included. # Packages for variations of Hyprland, dependencies included.
hyprland-packages = lib.composeManyExtensions [ hyprland-packages = lib.composeManyExtensions [
@ -33,10 +40,13 @@ in {
self.overlays.glaze self.overlays.glaze
# Hyprland packages themselves # Hyprland packages themselves
(final: _prev: let (
final: _prev:
let
date = mkDate (self.lastModifiedDate or "19700101"); date = mkDate (self.lastModifiedDate or "19700101");
version = "${ver}+date=${date}_${self.shortRev or "dirty"}"; version = "${ver}+date=${date}_${self.shortRev or "dirty"}";
in { in
{
hyprland = final.callPackage ./default.nix { hyprland = final.callPackage ./default.nix {
stdenv = final.gcc15Stdenv; stdenv = final.gcc15Stdenv;
commit = self.rev or ""; commit = self.rev or "";
@ -47,35 +57,28 @@ in {
hyprland-with-tests = final.hyprland.override { withTests = true; }; hyprland-with-tests = final.hyprland.override { withTests = true; };
hyprland-with-hyprtester = hyprland-with-hyprtester = builtins.trace ''
builtins.trace ''
hyprland-with-hyprtester was removed. Please use the hyprland package. hyprland-with-hyprtester was removed. Please use the hyprland package.
Hyprtester is always built now. Hyprtester is always built now.
'' '' final.hyprland;
final.hyprland;
# deprecated packages # deprecated packages
hyprland-legacy-renderer = hyprland-legacy-renderer = builtins.trace ''
builtins.trace ''
hyprland-legacy-renderer was removed. Please use the hyprland package. hyprland-legacy-renderer was removed. Please use the hyprland package.
Legacy renderer is no longer supported. Legacy renderer is no longer supported.
'' '' final.hyprland;
final.hyprland;
hyprland-nvidia = hyprland-nvidia = builtins.trace ''
builtins.trace ''
hyprland-nvidia was removed. Please use the hyprland package. hyprland-nvidia was removed. Please use the hyprland package.
Nvidia patches are no longer needed. Nvidia patches are no longer needed.
'' '' final.hyprland;
final.hyprland;
hyprland-hidpi = hyprland-hidpi = builtins.trace ''
builtins.trace ''
hyprland-hidpi was removed. Please use the hyprland package. hyprland-hidpi was removed. Please use the hyprland package.
For more information, refer to https://wiki.hypr.land/Configuring/XWayland. For more information, refer to https://wiki.hypr.land/Configuring/XWayland.
'' '' final.hyprland;
final.hyprland; }
}) )
]; ];
# Debug # Debug
@ -83,7 +86,7 @@ in {
# Dependencies # Dependencies
self.overlays.hyprland-packages self.overlays.hyprland-packages
(final: prev: { (_final: prev: {
aquamarine = prev.aquamarine.override { debug = true; }; aquamarine = prev.aquamarine.override { debug = true; };
hyprutils = prev.hyprutils.override { debug = true; }; hyprutils = prev.hyprutils.override { debug = true; };
hyprland-debug = prev.hyprland.override { debug = true; }; hyprland-debug = prev.hyprland.override { debug = true; };
@ -100,7 +103,8 @@ in {
# this version is the one used in the git submodule, and allows us to # this version is the one used in the git submodule, and allows us to
# fetch the source without '?submodules=1' # fetch the source without '?submodules=1'
udis86 = final: prev: { udis86 = final: prev: {
udis86-hyprland = prev.udis86.overrideAttrs (_self: _super: { udis86-hyprland = prev.udis86.overrideAttrs (
_self: _super: {
src = final.fetchFromGitHub { src = final.fetchFromGitHub {
owner = "canihavesomecoffee"; owner = "canihavesomecoffee";
repo = "udis86"; repo = "udis86";
@ -109,12 +113,13 @@ in {
}; };
patches = [ ]; patches = [ ];
}); }
);
}; };
# Even though glaze itself disables it by default, nixpkgs sets ENABLE_SSL set to true. # Even though glaze itself disables it by default, nixpkgs sets ENABLE_SSL set to true.
# Since we don't include openssl, the build failes without the `enableSSL = false;` override # Since we don't include openssl, the build failes without the `enableSSL = false;` override
glaze = final: prev: { glaze = _final: prev: {
glaze-hyprland = prev.glaze.override { glaze-hyprland = prev.glaze.override {
enableSSL = false; enableSSL = false;
enableInterop = false; enableInterop = false;

View file

@ -1,11 +1,15 @@
inputs: pkgs: let inputs: pkgs:
let
flake = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}; flake = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
hyprland = flake.hyprland-with-tests; hyprland = flake.hyprland-with-tests;
in { in
{
tests = pkgs.testers.runNixOSTest { tests = pkgs.testers.runNixOSTest {
name = "hyprland-tests"; name = "hyprland-tests";
nodes.machine = {pkgs, ...}: { nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
# Programs needed for tests # Programs needed for tests
jq jq

View file

@ -15,7 +15,7 @@ echo 'static const std::map<std::string, std::string> SHADERS = {' >> ./src/rend
for filename in `ls ${SHADERS_SRC}`; do for filename in `ls ${SHADERS_SRC}`; do
echo "-- ${filename}" echo "-- ${filename}"
{ echo 'R"#('; cat ${SHADERS_SRC}/${filename}; echo ')#"'; } > ./src/render/shaders/${filename}.inc { echo -n 'R"#('; cat ${SHADERS_SRC}/${filename}; echo ')#"'; } > ./src/render/shaders/${filename}.inc
echo "{\"${filename}\"," >> ./src/render/shaders/Shaders.hpp echo "{\"${filename}\"," >> ./src/render/shaders/Shaders.hpp
echo "#include \"./${filename}.inc\"" >> ./src/render/shaders/Shaders.hpp echo "#include \"./${filename}.inc\"" >> ./src/render/shaders/Shaders.hpp
echo "}," >> ./src/render/shaders/Shaders.hpp echo "}," >> ./src/render/shaders/Shaders.hpp

View file

@ -47,6 +47,7 @@
#include "protocols/core/Compositor.hpp" #include "protocols/core/Compositor.hpp"
#include "protocols/core/Subcompositor.hpp" #include "protocols/core/Subcompositor.hpp"
#include "desktop/view/LayerSurface.hpp" #include "desktop/view/LayerSurface.hpp"
#include "layout/space/Space.hpp"
#include "render/Renderer.hpp" #include "render/Renderer.hpp"
#include "xwayland/XWayland.hpp" #include "xwayland/XWayland.hpp"
#include "helpers/ByteOperations.hpp" #include "helpers/ByteOperations.hpp"
@ -1404,6 +1405,35 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
PHLWINDOW leaderWindow = nullptr; PHLWINDOW leaderWindow = nullptr;
if (!useVectorAngles) { if (!useVectorAngles) {
// helper to check if two rectangles are adjacent along an axis, considering slight overlaps.
// returns true if: STICKS (delta <= 2) OR rectangles overlap but no more than 50% of the smaller dimension.
static auto isAdjacent = [](const double aMin, const double aMax, const double bMin, const double bMax) -> bool {
constexpr double STICK_THRESHOLD = 2.0;
constexpr double MAX_OVERLAP_RATIO = 0.5;
const double aEdge = aMin;
const double bEdge = bMax;
const double delta = aEdge - bEdge;
// old STICKS check for 2px
if (std::abs(delta) < STICK_THRESHOLD)
return true;
if (delta >= 0)
return false;
const double overlap = -delta;
const double sizeA = aMax - aMin;
const double sizeB = bMax - bMin;
// reject if one rectangle fully contains the other
if ((bMin <= aMin && bMax >= aMax) || (aMin <= bMin && aMax >= bMax))
return false;
// accept if overlap is at most 50% of the smaller dimension
return overlap <= std::min(sizeA, sizeB) * MAX_OVERLAP_RATIO;
};
for (auto const& w : m_windows) { for (auto const& w : m_windows) {
if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || w->isHidden() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible()) if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || w->isHidden() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible())
continue; continue;
@ -1426,24 +1456,20 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
switch (dir) { switch (dir) {
case Math::DIRECTION_LEFT: case Math::DIRECTION_LEFT:
if (STICKS(POSA.x, POSB.x + SIZEB.x)) { if (isAdjacent(POSA.x, POSA.x + SIZEA.x, POSB.x, POSB.x + SIZEB.x))
intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y)); intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
}
break; break;
case Math::DIRECTION_RIGHT: case Math::DIRECTION_RIGHT:
if (STICKS(POSA.x + SIZEA.x, POSB.x)) { if (isAdjacent(POSB.x, POSB.x + SIZEB.x, POSA.x, POSA.x + SIZEA.x))
intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y)); intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
}
break; break;
case Math::DIRECTION_UP: case Math::DIRECTION_UP:
if (STICKS(POSA.y, POSB.y + SIZEB.y)) { if (isAdjacent(POSA.y, POSA.y + SIZEA.y, POSB.y, POSB.y + SIZEB.y))
intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x)); intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
}
break; break;
case Math::DIRECTION_DOWN: case Math::DIRECTION_DOWN:
if (STICKS(POSA.y + SIZEA.y, POSB.y)) { if (isAdjacent(POSB.y, POSB.y + SIZEB.y, POSA.y, POSA.y + SIZEA.y))
intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x)); intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
}
break; break;
default: break; default: break;
} }
@ -1799,6 +1825,9 @@ void CCompositor::swapActiveWorkspaces(PHLMONITOR pMonitorA, PHLMONITOR pMonitor
g_layoutManager->recalculateMonitor(pMonitorA); g_layoutManager->recalculateMonitor(pMonitorA);
g_layoutManager->recalculateMonitor(pMonitorB); g_layoutManager->recalculateMonitor(pMonitorB);
g_pHyprRenderer->damageMonitor(pMonitorB);
g_pHyprRenderer->damageMonitor(pMonitorA);
g_pDesktopAnimationManager->setFullscreenFadeAnimation( g_pDesktopAnimationManager->setFullscreenFadeAnimation(
PWORKSPACEB, PWORKSPACEB->m_hasFullscreenWindow ? CDesktopAnimationManager::ANIMATION_TYPE_IN : CDesktopAnimationManager::ANIMATION_TYPE_OUT); PWORKSPACEB, PWORKSPACEB->m_hasFullscreenWindow ? CDesktopAnimationManager::ANIMATION_TYPE_IN : CDesktopAnimationManager::ANIMATION_TYPE_OUT);
g_pDesktopAnimationManager->setFullscreenFadeAnimation( g_pDesktopAnimationManager->setFullscreenFadeAnimation(
@ -1953,6 +1982,7 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
// move the workspace // move the workspace
pWorkspace->m_monitor = pMonitor; pWorkspace->m_monitor = pMonitor;
pWorkspace->m_space->recheckWorkArea();
pWorkspace->m_events.monitorChanged.emit(); pWorkspace->m_events.monitorChanged.emit();
for (auto const& w : m_windows) { for (auto const& w : m_windows) {
@ -2008,6 +2038,7 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
pWorkspace->m_events.activeChanged.emit(); pWorkspace->m_events.activeChanged.emit();
g_layoutManager->recalculateMonitor(pMonitor); g_layoutManager->recalculateMonitor(pMonitor);
g_pHyprRenderer->damageMonitor(pMonitor);
g_pDesktopAnimationManager->startAnimation(pWorkspace, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true); g_pDesktopAnimationManager->startAnimation(pWorkspace, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
pWorkspace->m_visible = true; pWorkspace->m_visible = true;
@ -2747,6 +2778,7 @@ void CCompositor::arrangeMonitors() {
} }
PROTO::xdgOutput->updateAllOutputs(); PROTO::xdgOutput->updateAllOutputs();
Event::bus()->m_events.monitor.layoutChanged.emit();
#ifndef NO_XWAYLAND #ifndef NO_XWAYLAND
const auto box = g_pCompositor->calculateX11WorkArea(); const auto box = g_pCompositor->calculateX11WorkArea();
@ -3042,6 +3074,27 @@ void CCompositor::ensurePersistentWorkspacesPresent(const std::vector<SWorkspace
} }
} }
void CCompositor::ensureWorkspacesOnAssignedMonitors() {
for (auto const& ws : getWorkspacesCopy()) {
if (!valid(ws) || ws->m_isSpecialWorkspace)
continue;
const auto RULE = g_pConfigManager->getWorkspaceRuleFor(ws);
if (RULE.monitor.empty())
continue;
const auto PMONITOR = getMonitorFromString(RULE.monitor);
if (!PMONITOR)
continue;
if (ws->m_monitor == PMONITOR)
continue;
Log::logger->log(Log::DEBUG, "ensureWorkspacesOnAssignedMonitors: moving workspace {} to {}", ws->m_name, PMONITOR->m_name);
moveWorkspaceToMonitor(ws, PMONITOR, true);
}
}
std::optional<unsigned int> CCompositor::getVTNr() { std::optional<unsigned int> CCompositor::getVTNr() {
if (!m_aqBackend->hasSession()) if (!m_aqBackend->hasSession())
return std::nullopt; return std::nullopt;

View file

@ -9,7 +9,7 @@
#include "managers/KeybindManager.hpp" #include "managers/KeybindManager.hpp"
#include "managers/SessionLockManager.hpp" #include "managers/SessionLockManager.hpp"
#include "desktop/view/Window.hpp" #include "desktop/view/Window.hpp"
#include "protocols/types/ColorManagement.hpp" #include "helpers/cm/ColorManagement.hpp"
#include <aquamarine/backend/Backend.hpp> #include <aquamarine/backend/Backend.hpp>
#include <aquamarine/output/Output.hpp> #include <aquamarine/output/Output.hpp>
@ -161,6 +161,7 @@ class CCompositor {
void updateSuspendedStates(); void updateSuspendedStates();
void onNewMonitor(SP<Aquamarine::IOutput> output); void onNewMonitor(SP<Aquamarine::IOutput> output);
void ensurePersistentWorkspacesPresent(const std::vector<SWorkspaceRule>& rules, PHLWORKSPACE pWorkspace = nullptr); void ensurePersistentWorkspacesPresent(const std::vector<SWorkspaceRule>& rules, PHLWORKSPACE pWorkspace = nullptr);
void ensureWorkspacesOnAssignedMonitors();
std::optional<unsigned int> getVTNr(); std::optional<unsigned int> getVTNr();
bool isVRRActiveOnAnyMonitor() const; bool isVRRActiveOnAnyMonitor() const;

View file

@ -1579,6 +1579,18 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_BOOL, .type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true}, .data = SConfigOptionDescription::SBoolData{true},
}, },
SConfigOptionDescription{
.value = "render:icc_vcgt_enabled",
.description = "Enable sending VCGT ramps to KMS with ICC profiles",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true},
},
{
.value = "render:use_shader_blur_blend",
.description = "Use experimental blurred bg blending",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
/* /*
* cursor: * cursor:
@ -2093,6 +2105,18 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_CHOICE, .type = CONFIG_OPTION_CHOICE,
.data = SConfigOptionDescription::SChoiceData{.firstIndex = 0, .choices = "right,left,down,up"}, .data = SConfigOptionDescription::SChoiceData{.firstIndex = 0, .choices = "right,left,down,up"},
}, },
SConfigOptionDescription{
.value = "scrolling:wrap_focus",
.description = "Determines if column focus wraps around when going before the first column or past the last column",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{.value = true},
},
SConfigOptionDescription{
.value = "scrolling:wrap_swapcol",
.description = "Determines if column movement wraps around when moving to before the first column or past the last column",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{.value = true},
},
/* /*
* Quirks * Quirks

View file

@ -655,6 +655,8 @@ CConfigManager::CConfigManager() {
registerConfigVar("scrolling:follow_min_visible", Hyprlang::FLOAT{0.4}); registerConfigVar("scrolling:follow_min_visible", Hyprlang::FLOAT{0.4});
registerConfigVar("scrolling:explicit_column_widths", Hyprlang::STRING{"0.333, 0.5, 0.667, 1.0"}); registerConfigVar("scrolling:explicit_column_widths", Hyprlang::STRING{"0.333, 0.5, 0.667, 1.0"});
registerConfigVar("scrolling:direction", Hyprlang::STRING{"right"}); registerConfigVar("scrolling:direction", Hyprlang::STRING{"right"});
registerConfigVar("scrolling:wrap_focus", Hyprlang::INT{1});
registerConfigVar("scrolling:wrap_swapcol", Hyprlang::INT{1});
registerConfigVar("animations:enabled", Hyprlang::INT{1}); registerConfigVar("animations:enabled", Hyprlang::INT{1});
registerConfigVar("animations:workspace_wraparound", Hyprlang::INT{0}); registerConfigVar("animations:workspace_wraparound", Hyprlang::INT{0});
@ -797,6 +799,8 @@ CConfigManager::CConfigManager() {
registerConfigVar("render:non_shader_cm", Hyprlang::INT{3}); registerConfigVar("render:non_shader_cm", Hyprlang::INT{3});
registerConfigVar("render:cm_sdr_eotf", {"default"}); registerConfigVar("render:cm_sdr_eotf", {"default"});
registerConfigVar("render:commit_timing_enabled", Hyprlang::INT{1}); registerConfigVar("render:commit_timing_enabled", Hyprlang::INT{1});
registerConfigVar("render:icc_vcgt_enabled", Hyprlang::INT{1});
registerConfigVar("render:use_shader_blur_blend", Hyprlang::INT{0});
registerConfigVar("ecosystem:no_update_news", Hyprlang::INT{0}); registerConfigVar("ecosystem:no_update_news", Hyprlang::INT{0});
registerConfigVar("ecosystem:no_donation_nag", Hyprlang::INT{0}); registerConfigVar("ecosystem:no_donation_nag", Hyprlang::INT{0});
@ -871,6 +875,7 @@ CConfigManager::CConfigManager() {
m_config->addSpecialConfigValue("monitorv2", "min_luminance", Hyprlang::FLOAT{-1.0}); m_config->addSpecialConfigValue("monitorv2", "min_luminance", Hyprlang::FLOAT{-1.0});
m_config->addSpecialConfigValue("monitorv2", "max_luminance", Hyprlang::INT{-1}); m_config->addSpecialConfigValue("monitorv2", "max_luminance", Hyprlang::INT{-1});
m_config->addSpecialConfigValue("monitorv2", "max_avg_luminance", Hyprlang::INT{-1}); m_config->addSpecialConfigValue("monitorv2", "max_avg_luminance", Hyprlang::INT{-1});
m_config->addSpecialConfigValue("monitorv2", "icc", Hyprlang::STRING{""});
// windowrule v3 // windowrule v3
m_config->addSpecialCategory("windowrule", {.key = "name"}); m_config->addSpecialCategory("windowrule", {.key = "name"});
@ -1257,6 +1262,10 @@ std::optional<std::string> CConfigManager::handleMonitorv2(const std::string& ou
if (VAL && VAL->m_bSetByUser) if (VAL && VAL->m_bSetByUser)
parser.rule().maxAvgLuminance = std::any_cast<Hyprlang::INT>(VAL->getValue()); parser.rule().maxAvgLuminance = std::any_cast<Hyprlang::INT>(VAL->getValue());
VAL = m_config->getSpecialConfigValuePtr("monitorv2", "icc", output.c_str());
if (VAL && VAL->m_bSetByUser)
parser.rule().iccFile = std::any_cast<Hyprlang::STRING>(VAL->getValue());
auto newrule = parser.rule(); auto newrule = parser.rule();
std::erase_if(m_monitorRules, [&](const auto& other) { return other.name == newrule.name; }); std::erase_if(m_monitorRules, [&](const auto& other) { return other.name == newrule.name; });
@ -2275,6 +2284,15 @@ bool CMonitorRuleParser::parseVRR(const std::string& value) {
return true; return true;
} }
bool CMonitorRuleParser::parseICC(const std::string& val) {
if (val.empty()) {
m_error += "invalid icc ";
return false;
}
m_rule.iccFile = val;
return true;
}
void CMonitorRuleParser::setDisabled() { void CMonitorRuleParser::setDisabled() {
m_rule.disabled = true; m_rule.disabled = true;
} }
@ -2369,6 +2387,9 @@ std::optional<std::string> CConfigManager::handleMonitor(const std::string& comm
} else if (ARGS[argno] == "vrr") { } else if (ARGS[argno] == "vrr") {
parser.parseVRR(std::string(ARGS[argno + 1])); parser.parseVRR(std::string(ARGS[argno + 1]));
argno++; argno++;
} else if (ARGS[argno] == "icc") {
parser.parseICC(std::string(ARGS[argno + 1]));
argno++;
} else if (ARGS[argno] == "workspace") { } else if (ARGS[argno] == "workspace") {
const auto& [id, name, isAutoID] = getWorkspaceIDNameFromString(std::string(ARGS[argno + 1])); const auto& [id, name, isAutoID] = getWorkspaceIDNameFromString(std::string(ARGS[argno + 1]));

View file

@ -177,6 +177,7 @@ class CMonitorRuleParser {
bool parseSDRBrightness(const std::string& value); bool parseSDRBrightness(const std::string& value);
bool parseSDRSaturation(const std::string& value); bool parseSDRSaturation(const std::string& value);
bool parseVRR(const std::string& value); bool parseVRR(const std::string& value);
bool parseICC(const std::string& value);
void setDisabled(); void setDisabled();
void setMirror(const std::string& value); void setMirror(const std::string& value);

View file

@ -2049,7 +2049,12 @@ static std::string submapRequest(eHyprCtlOutputFormat format, std::string reques
} }
static std::string reloadShaders(eHyprCtlOutputFormat format, std::string request) { static std::string reloadShaders(eHyprCtlOutputFormat format, std::string request) {
if (g_pHyprOpenGL->initShaders()) CVarList vars(request, 0, ' ');
if (vars.size() > 2)
return "too many args";
if (g_pHyprOpenGL && g_pHyprRenderer->reloadShaders(vars.size() == 2 ? vars[1] : ""))
return format == FORMAT_JSON ? "{\"ok\": true}" : "ok"; return format == FORMAT_JSON ? "{\"ok\": true}" : "ok";
else else
return format == FORMAT_JSON ? "{\"ok\": false}" : "error"; return format == FORMAT_JSON ? "{\"ok\": false}" : "error";
@ -2076,8 +2081,8 @@ CHyprCtl::CHyprCtl() {
registerCommand(SHyprCtlCommand{"locked", true, getIsLocked}); registerCommand(SHyprCtlCommand{"locked", true, getIsLocked});
registerCommand(SHyprCtlCommand{"descriptions", true, getDescriptions}); registerCommand(SHyprCtlCommand{"descriptions", true, getDescriptions});
registerCommand(SHyprCtlCommand{"submap", true, submapRequest}); registerCommand(SHyprCtlCommand{"submap", true, submapRequest});
registerCommand(SHyprCtlCommand{.name = "reloadshaders", .exact = true, .fn = reloadShaders});
registerCommand(SHyprCtlCommand{.name = "reloadshaders", .exact = false, .fn = reloadShaders});
registerCommand(SHyprCtlCommand{"monitors", false, monitorsRequest}); registerCommand(SHyprCtlCommand{"monitors", false, monitorsRequest});
registerCommand(SHyprCtlCommand{"reload", false, reloadRequest}); registerCommand(SHyprCtlCommand{"reload", false, reloadRequest});
registerCommand(SHyprCtlCommand{"plugin", false, dispatchPlugin}); registerCommand(SHyprCtlCommand{"plugin", false, dispatchPlugin});
@ -2197,6 +2202,15 @@ std::string CHyprCtl::getReply(std::string request) {
Desktop::Rule::ruleEngine()->updateAllRules(); Desktop::Rule::ruleEngine()->updateAllRules();
} }
for (const auto& ws : g_pCompositor->getWorkspaces()) {
if (!ws)
continue;
ws->updateWindows();
ws->updateWindowData();
ws->updateWindowDecos();
}
for (auto const& m : g_pCompositor->m_monitors) { for (auto const& m : g_pCompositor->m_monitors) {
g_pHyprRenderer->damageMonitor(m); g_pHyprRenderer->damageMonitor(m);
} }
@ -2210,18 +2224,40 @@ std::string CHyprCtl::makeDynamicCall(const std::string& input) {
} }
static bool successWrite(int fd, const std::string& data, bool needLog = true) { static bool successWrite(int fd, const std::string& data, bool needLog = true) {
if (write(fd, data.c_str(), data.length()) > 0) size_t totalWritten = 0;
return true; size_t remaining = data.length();
size_t waitsDone = 0;
constexpr const size_t MAX_WAITS = 20; // 2000µs = 2ms
if (errno == EAGAIN) while (totalWritten < data.length()) {
return true; ssize_t written = write(fd, data.c_str() + totalWritten, remaining);
if (needLog)
Log::logger->log(Log::ERR, "Couldn't write to socket. Error: " + std::string(strerror(errno)));
if (waitsDone > MAX_WAITS) {
Log::logger->log(Log::ERR, "Couldn't write to socket. Buffer was full and the client couldn't read in time.");
return false; return false;
} }
if (written < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// socket buffer full, wait a bit and retry
std::this_thread::sleep_for(std::chrono::microseconds(100));
waitsDone++;
continue;
}
if (needLog)
Log::logger->log(Log::ERR, "Couldn't write to socket. Error: {}", strerror(errno));
return false;
}
waitsDone = 0;
totalWritten += written;
remaining -= written;
}
return true;
}
static void runWritingDebugLogThread(const int conn) { static void runWritingDebugLogThread(const int conn) {
using namespace std::chrono_literals; using namespace std::chrono_literals;
Log::logger->log(Log::DEBUG, "In followlog thread, got connection, start writing: {}", conn); Log::logger->log(Log::DEBUG, "In followlog thread, got connection, start writing: {}", conn);

View file

@ -8,7 +8,7 @@
#include "../desktop/state/FocusState.hpp" #include "../desktop/state/FocusState.hpp"
CHyprDebugOverlay::CHyprDebugOverlay() { CHyprDebugOverlay::CHyprDebugOverlay() {
m_texture = makeShared<CTexture>(); m_texture = g_pHyprRenderer->createTexture();
} }
void CHyprMonitorDebugOverlay::renderData(PHLMONITOR pMonitor, float durationUs) { void CHyprMonitorDebugOverlay::renderData(PHLMONITOR pMonitor, float durationUs) {
@ -259,15 +259,7 @@ void CHyprDebugOverlay::draw() {
cairo_surface_flush(m_cairoSurface); cairo_surface_flush(m_cairoSurface);
// copy the data to an OpenGL texture we have // copy the data to an OpenGL texture we have
const auto DATA = cairo_image_surface_get_data(m_cairoSurface); m_texture = g_pHyprRenderer->createTexture(m_cairoSurface);
m_texture->allocate();
m_texture->bind();
m_texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_R, GL_BLUE);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_B, GL_RED);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
CTexPassElement::SRenderData data; CTexPassElement::SRenderData data;
data.tex = m_texture; data.tex = m_texture;

View file

@ -42,7 +42,7 @@ class CHyprDebugOverlay {
cairo_surface_t* m_cairoSurface = nullptr; cairo_surface_t* m_cairoSurface = nullptr;
cairo_t* m_cairo = nullptr; cairo_t* m_cairo = nullptr;
SP<CTexture> m_texture; SP<ITexture> m_texture;
friend class CHyprMonitorDebugOverlay; friend class CHyprMonitorDebugOverlay;
friend class CHyprRenderer; friend class CHyprRenderer;

View file

@ -28,8 +28,6 @@ CHyprNotificationOverlay::CHyprNotificationOverlay() {
g_pHyprRenderer->damageBox(m_lastDamage); g_pHyprRenderer->damageBox(m_lastDamage);
}); });
m_texture = makeShared<CTexture>();
} }
CHyprNotificationOverlay::~CHyprNotificationOverlay() { CHyprNotificationOverlay::~CHyprNotificationOverlay() {
@ -232,16 +230,7 @@ void CHyprNotificationOverlay::draw(PHLMONITOR pMonitor) {
m_lastDamage = damage; m_lastDamage = damage;
// copy the data to an OpenGL texture we have m_texture = g_pHyprRenderer->createTexture(m_cairoSurface);
const auto DATA = cairo_image_surface_get_data(m_cairoSurface);
m_texture->allocate();
m_texture->bind();
m_texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_R, GL_BLUE);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_B, GL_RED);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, MONSIZE.x, MONSIZE.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
CTexPassElement::SRenderData data; CTexPassElement::SRenderData data;
data.tex = m_texture; data.tex = m_texture;

View file

@ -18,7 +18,7 @@ enum eIconBackend : uint8_t {
static const std::array<std::array<std::string, ICON_NONE + 1>, 3 /* backends */> ICONS_ARRAY = { static const std::array<std::array<std::string, ICON_NONE + 1>, 3 /* backends */> ICONS_ARRAY = {
std::array<std::string, ICON_NONE + 1>{"[!]", "[i]", "[Hint]", "[Err]", "[?]", "[ok]", ""}, std::array<std::string, ICON_NONE + 1>{"[!]", "[i]", "[Hint]", "[Err]", "[?]", "[ok]", ""},
std::array<std::string, ICON_NONE + 1>{"", "", "", "", "", "󰸞", ""}, std::array<std::string, ICON_NONE + 1>{"", "", "", "", "", ""}}; std::array<std::string, ICON_NONE + 1>{"", "", "", "", "", "󰸞", ""}, std::array<std::string, ICON_NONE + 1>{"", "", "", "", "", ""}};
static const std::array<CHyprColor, ICON_NONE + 1> ICONS_COLORS = {CHyprColor{255.0 / 255.0, 204 / 255.0, 102 / 255.0, 1.0}, static const std::array<CHyprColor, ICON_NONE + 1> ICONS_COLORS = {CHyprColor{1.0, 204 / 255.0, 102 / 255.0, 1.0},
CHyprColor{128 / 255.0, 255 / 255.0, 255 / 255.0, 1.0}, CHyprColor{128 / 255.0, 255 / 255.0, 255 / 255.0, 1.0},
CHyprColor{179 / 255.0, 255 / 255.0, 204 / 255.0, 1.0}, CHyprColor{179 / 255.0, 255 / 255.0, 204 / 255.0, 1.0},
CHyprColor{255 / 255.0, 77 / 255.0, 77 / 255.0, 1.0}, CHyprColor{255 / 255.0, 77 / 255.0, 77 / 255.0, 1.0},
@ -57,7 +57,7 @@ class CHyprNotificationOverlay {
PHLMONITORREF m_lastMonitor; PHLMONITORREF m_lastMonitor;
Vector2D m_lastSize = Vector2D(-1, -1); Vector2D m_lastSize = Vector2D(-1, -1);
SP<CTexture> m_texture; SP<ITexture> m_texture;
}; };
inline UP<CHyprNotificationOverlay> g_pHyprNotificationOverlay; inline UP<CHyprNotificationOverlay> g_pHyprNotificationOverlay;

View file

@ -4,6 +4,7 @@
#include "../../view/LayerSurface.hpp" #include "../../view/LayerSurface.hpp"
#include "../../types/OverridableVar.hpp" #include "../../types/OverridableVar.hpp"
#include "../../../helpers/MiscFunctions.hpp" #include "../../../helpers/MiscFunctions.hpp"
#include "../../../event/EventBus.hpp"
using namespace Desktop; using namespace Desktop;
using namespace Desktop::Rule; using namespace Desktop::Rule;
@ -32,11 +33,38 @@ void CLayerRuleApplicator::resetProps(std::underlying_type_t<eRuleProperty> prop
UNSET(aboveLock) UNSET(aboveLock)
UNSET(ignoreAlpha) UNSET(ignoreAlpha)
UNSET(animationStyle) UNSET(animationStyle)
#undef UNSET
if (prio == Types::PRIORITY_WINDOW_RULE)
std::erase_if(m_otherProps.props, [props](const auto& el) { return !el.second || el.second->propMask & props; });
} }
void CLayerRuleApplicator::applyDynamicRule(const SP<CLayerRule>& rule) { void CLayerRuleApplicator::applyDynamicRule(const SP<CLayerRule>& rule) {
for (const auto& [key, effect] : rule->effects()) { for (const auto& [key, effect] : rule->effects()) {
switch (key) { switch (key) {
default: {
if (key <= LAYER_RULE_EFFECT_LAST_STATIC) {
Log::logger->log(Log::TRACE, "CLayerRuleApplicator::applyDynamicRule: Skipping effect {}, not dynamic", sc<std::underlying_type_t<eLayerRuleEffect>>(key));
break;
}
// custom type, add to our vec
if (!m_otherProps.props.contains(key)) {
m_otherProps.props.emplace(key,
makeUnique<SCustomPropContainer>(SCustomPropContainer{
.idx = key,
.propMask = rule->getPropertiesMask(),
.effect = effect,
}));
} else {
auto& e = m_otherProps.props[key];
e->propMask |= rule->getPropertiesMask();
e->effect = effect;
}
break;
}
case LAYER_RULE_EFFECT_NONE: { case LAYER_RULE_EFFECT_NONE: {
Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: BUG THIS: LAYER_RULE_EFFECT_NONE??"); Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: BUG THIS: LAYER_RULE_EFFECT_NONE??");
break; break;
@ -125,4 +153,7 @@ void CLayerRuleApplicator::propertiesChanged(std::underlying_type_t<eRulePropert
applyDynamicRule(wr); applyDynamicRule(wr);
} }
// for plugins
Event::bus()->m_events.layer.updateRules.emit(m_ls.lock());
} }

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "LayerRuleEffectContainer.hpp"
#include "../../DesktopTypes.hpp" #include "../../DesktopTypes.hpp"
#include "../Rule.hpp" #include "../Rule.hpp"
#include "../../types/OverridableVar.hpp" #include "../../types/OverridableVar.hpp"
@ -21,6 +22,17 @@ namespace Desktop::Rule {
void propertiesChanged(std::underlying_type_t<eRuleProperty> props); void propertiesChanged(std::underlying_type_t<eRuleProperty> props);
void resetProps(std::underlying_type_t<eRuleProperty> props, Types::eOverridePriority prio = Types::PRIORITY_WINDOW_RULE); void resetProps(std::underlying_type_t<eRuleProperty> props, Types::eOverridePriority prio = Types::PRIORITY_WINDOW_RULE);
struct SCustomPropContainer {
CLayerRuleEffectContainer::storageType idx = LAYER_RULE_EFFECT_NONE;
std::underlying_type_t<eRuleProperty> propMask = RULE_PROP_NONE;
std::string effect;
};
// This struct holds props that were dynamically registered. Plugins may read this.
struct {
std::unordered_map<CLayerRuleEffectContainer::storageType, UP<SCustomPropContainer>> props;
} m_otherProps;
#define COMMA , #define COMMA ,
#define DEFINE_PROP(type, name, def) \ #define DEFINE_PROP(type, name, def) \
private: \ private: \

View file

@ -97,7 +97,7 @@ bool CWindowRule::matches(PHLWINDOW w, bool allowEnvLookup) {
return false; return false;
break; break;
case RULE_PROP_CONTENT: case RULE_PROP_CONTENT:
if (!engine->match(w->getContentType())) if (!engine->match(w->getContentType()) && !engine->match(NContentType::toString(w->getContentType())))
return false; return false;
break; break;
case RULE_PROP_XDG_TAG: case RULE_PROP_XDG_TAG:

View file

@ -630,6 +630,8 @@ void CWindowRuleApplicator::propertiesChanged(std::underlying_type_t<eRuleProper
needsRelayout = needsRelayout || RES.needsRelayout; needsRelayout = needsRelayout || RES.needsRelayout;
} }
m_window->updateWindowData();
m_window->updateWindowDecos();
m_window->updateDecorationValues(); m_window->updateDecorationValues();
if (needsRelayout) if (needsRelayout)

View file

@ -120,7 +120,7 @@ void CGroup::add(PHLWINDOW w) {
m_target->recalc(); m_target->recalc();
} }
void CGroup::remove(PHLWINDOW w) { void CGroup::remove(PHLWINDOW w, Math::eDirection dir) {
std::optional<size_t> idx; std::optional<size_t> idx;
for (size_t i = 0; i < m_windows.size(); ++i) { for (size_t i = 0; i < m_windows.size(); ++i) {
if (m_windows.at(i) == w) { if (m_windows.at(i) == w) {
@ -156,8 +156,20 @@ void CGroup::remove(PHLWINDOW w) {
updateWindowVisibility(); updateWindowVisibility();
// do this here: otherwise the new current is hidden and workspace rules get wrong data // do this here: otherwise the new current is hidden and workspace rules get wrong data
if (!REMOVING_GROUP) if (!REMOVING_GROUP) {
w->m_target->assignToSpace(m_target->space()); std::optional<Vector2D> focalPoint;
if (dir != Math::DIRECTION_DEFAULT) {
const auto box = m_target->position();
switch (dir) {
case Math::DIRECTION_RIGHT: focalPoint = Vector2D(box.x + box.w, box.y + box.h / 2.0); break;
case Math::DIRECTION_LEFT: focalPoint = Vector2D(box.x, box.y + box.h / 2.0); break;
case Math::DIRECTION_DOWN: focalPoint = Vector2D(box.x + box.w / 2.0, box.y + box.h); break;
case Math::DIRECTION_UP: focalPoint = Vector2D(box.x + box.w / 2.0, box.y); break;
default: break;
}
}
w->m_target->assignToSpace(m_target->space(), focalPoint);
}
} }
void CGroup::moveCurrent(bool next) { void CGroup::moveCurrent(bool next) {
@ -317,6 +329,7 @@ void CGroup::swapWithNext() {
size_t idx = m_current + 1 >= m_windows.size() ? 0 : m_current + 1; size_t idx = m_current + 1 >= m_windows.size() ? 0 : m_current + 1;
std::iter_swap(m_windows.begin() + m_current, m_windows.begin() + idx); std::iter_swap(m_windows.begin() + m_current, m_windows.begin() + idx);
m_current = idx;
updateWindowVisibility(); updateWindowVisibility();
@ -329,6 +342,7 @@ void CGroup::swapWithLast() {
size_t idx = m_current == 0 ? m_windows.size() - 1 : m_current - 1; size_t idx = m_current == 0 ? m_windows.size() - 1 : m_current - 1;
std::iter_swap(m_windows.begin() + m_current, m_windows.begin() + idx); std::iter_swap(m_windows.begin() + m_current, m_windows.begin() + idx);
m_current = idx;
updateWindowVisibility(); updateWindowVisibility();

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "../DesktopTypes.hpp" #include "../DesktopTypes.hpp"
#include "../../helpers/math/Direction.hpp"
#include <vector> #include <vector>
@ -17,7 +18,7 @@ namespace Desktop::View {
bool has(PHLWINDOW w) const; bool has(PHLWINDOW w) const;
void add(PHLWINDOW w); void add(PHLWINDOW w);
void remove(PHLWINDOW w); void remove(PHLWINDOW w, Math::eDirection dir = Math::DIRECTION_DEFAULT);
void moveCurrent(bool next); void moveCurrent(bool next);
void setCurrent(size_t idx); void setCurrent(size_t idx);
void setCurrent(PHLWINDOW w); void setCurrent(PHLWINDOW w);

View file

@ -277,7 +277,7 @@ CBox CWindow::getWindowIdealBoundingBoxIgnoreReserved() {
// fucker fucking fuck // fucker fucking fuck
const auto WORKAREA = m_workspace->m_space->workArea(); const auto WORKAREA = m_workspace->m_space->workArea();
const auto& RESERVED = PMONITOR->m_reservedArea; const auto& RESERVED = CReservedArea(PMONITOR->logicalBox(), WORKAREA);
if (DELTALESSTHAN(POS.x, WORKAREA.x, 1)) { if (DELTALESSTHAN(POS.x, WORKAREA.x, 1)) {
POS.x -= RESERVED.left(); POS.x -= RESERVED.left();
@ -510,12 +510,6 @@ void CWindow::moveToWorkspace(PHLWORKSPACE pWorkspace) {
setAnimationsToMove(); setAnimationsToMove();
OLDWORKSPACE->updateWindows();
OLDWORKSPACE->updateWindowData();
pWorkspace->updateWindows();
pWorkspace->updateWindowData();
g_pCompositor->updateAllWindowsAnimatedDecorationValues(); g_pCompositor->updateAllWindowsAnimatedDecorationValues();
if (valid(pWorkspace)) { if (valid(pWorkspace)) {
@ -807,9 +801,13 @@ void CWindow::updateWindowData() {
} }
void CWindow::updateWindowData(const SWorkspaceRule& workspaceRule) { void CWindow::updateWindowData(const SWorkspaceRule& workspaceRule) {
if (workspaceRule.noBorder.value_or(false))
m_ruleApplicator->borderSize().matchOptional(std::optional<Hyprlang::INT>(0), Desktop::Types::PRIORITY_WORKSPACE_RULE);
else if (workspaceRule.borderSize)
m_ruleApplicator->borderSize().matchOptional(workspaceRule.borderSize, Desktop::Types::PRIORITY_WORKSPACE_RULE); m_ruleApplicator->borderSize().matchOptional(workspaceRule.borderSize, Desktop::Types::PRIORITY_WORKSPACE_RULE);
else
m_ruleApplicator->borderSize().matchOptional(std::nullopt, Desktop::Types::PRIORITY_WORKSPACE_RULE);
m_ruleApplicator->decorate().matchOptional(workspaceRule.decorate, Desktop::Types::PRIORITY_WORKSPACE_RULE); m_ruleApplicator->decorate().matchOptional(workspaceRule.decorate, Desktop::Types::PRIORITY_WORKSPACE_RULE);
m_ruleApplicator->borderSize().matchOptional(workspaceRule.noBorder ? std::optional<Hyprlang::INT>(0) : std::nullopt, Desktop::Types::PRIORITY_WORKSPACE_RULE);
m_ruleApplicator->rounding().matchOptional(workspaceRule.noRounding.value_or(false) ? std::optional<Hyprlang::INT>(0) : std::nullopt, Desktop::Types::PRIORITY_WORKSPACE_RULE); m_ruleApplicator->rounding().matchOptional(workspaceRule.noRounding.value_or(false) ? std::optional<Hyprlang::INT>(0) : std::nullopt, Desktop::Types::PRIORITY_WORKSPACE_RULE);
m_ruleApplicator->noShadow().matchOptional(workspaceRule.noShadow, Desktop::Types::PRIORITY_WORKSPACE_RULE); m_ruleApplicator->noShadow().matchOptional(workspaceRule.noShadow, Desktop::Types::PRIORITY_WORKSPACE_RULE);
} }
@ -1873,11 +1871,12 @@ void CWindow::mapWindow() {
if (WORKSPACEARGS.contains("silent")) if (WORKSPACEARGS.contains("silent"))
workspaceSilent = true; workspaceSilent = true;
if (WORKSPACEARGS.contains("empty") && PWORKSPACE->getWindows() <= 1) { auto joined = WORKSPACEARGS.join(" ", 0, workspaceSilent ? WORKSPACEARGS.size() - 1 : 0);
if (joined.starts_with("empty") && PWORKSPACE->getWindows() == 0) {
requestedWorkspaceID = PWORKSPACE->m_id; requestedWorkspaceID = PWORKSPACE->m_id;
requestedWorkspaceName = PWORKSPACE->m_name; requestedWorkspaceName = PWORKSPACE->m_name;
} else { } else {
auto result = getWorkspaceIDNameFromString(WORKSPACEARGS.join(" ", 0, workspaceSilent ? WORKSPACEARGS.size() - 1 : 0)); auto result = getWorkspaceIDNameFromString(joined);
requestedWorkspaceID = result.id; requestedWorkspaceID = result.id;
requestedWorkspaceName = result.name; requestedWorkspaceName = result.name;
} }
@ -1953,7 +1952,9 @@ void CWindow::mapWindow() {
&& Desktop::focusState()->window() // focused window exists && Desktop::focusState()->window() // focused window exists
&& canBeGroupedInto(Desktop::focusState()->window()->m_group) // we can group && canBeGroupedInto(Desktop::focusState()->window()->m_group) // we can group
&& Desktop::focusState()->window()->m_workspace == m_workspace // workspaces match, we're not opening on another ws && Desktop::focusState()->window()->m_workspace == m_workspace // workspaces match, we're not opening on another ws
&& !isModal() && !(parent() && m_isFloating) && !isX11OverrideRedirect() // not a modal, floating child or X11 OR && !g_pXWaylandManager->shouldBeFloated(m_self.lock()) && !isX11OverrideRedirect() // not a window that should float or X11
&& !(m_isFloating && !Desktop::focusState()->window()->m_isFloating) // do not auto-group a floated window into a tiled group
&& !isModal() // no modal grouping
) { ) {
// add to group if we are focused on one // add to group if we are focused on one
Desktop::focusState()->window()->m_group->add(m_self.lock()); Desktop::focusState()->window()->m_group->add(m_self.lock());

View file

@ -41,6 +41,7 @@ namespace Event {
Event<PHLWINDOW> openEarly; Event<PHLWINDOW> openEarly;
Event<PHLWINDOW> destroy; Event<PHLWINDOW> destroy;
Event<PHLWINDOW> close; Event<PHLWINDOW> close;
Event<PHLWINDOW> kill;
Event<PHLWINDOW, Desktop::eFocusReason> active; Event<PHLWINDOW, Desktop::eFocusReason> active;
Event<PHLWINDOW> urgent; Event<PHLWINDOW> urgent;
Event<PHLWINDOW> title; Event<PHLWINDOW> title;
@ -54,6 +55,7 @@ namespace Event {
struct { struct {
Event<PHLLS> opened; Event<PHLLS> opened;
Event<PHLLS> closed; Event<PHLLS> closed;
Event<PHLLS> updateRules;
} layer; } layer;
struct { struct {

View file

@ -297,24 +297,12 @@ int NFormatUtils::minStride(const SPixelFormat* const fmt, int32_t width) {
return std::ceil((width * fmt->bytesPerBlock) / pixelsPerBlock(fmt)); return std::ceil((width * fmt->bytesPerBlock) / pixelsPerBlock(fmt));
} }
uint32_t NFormatUtils::drmFormatToGL(DRMFormat drm) {
switch (drm) {
case DRM_FORMAT_XRGB8888:
case DRM_FORMAT_XBGR8888: return GL_RGBA; // doesn't matter, opengl is gucci in this case.
case DRM_FORMAT_XRGB2101010:
case DRM_FORMAT_XBGR2101010: return GL_RGB10_A2;
default: return GL_RGBA;
}
UNREACHABLE();
return GL_RGBA;
}
uint32_t NFormatUtils::glFormatToType(uint32_t gl) {
return gl != GL_RGBA ? GL_UNSIGNED_INT_2_10_10_10_REV : GL_UNSIGNED_BYTE;
}
std::string NFormatUtils::drmFormatName(DRMFormat drm) { std::string NFormatUtils::drmFormatName(DRMFormat drm) {
auto n = drmGetFormatName(drm); auto n = drmGetFormatName(drm);
if (!n)
return "unknown";
std::string name = n; std::string name = n;
free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors) free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors)
return name; return name;
@ -322,6 +310,10 @@ std::string NFormatUtils::drmFormatName(DRMFormat drm) {
std::string NFormatUtils::drmModifierName(uint64_t mod) { std::string NFormatUtils::drmModifierName(uint64_t mod) {
auto n = drmGetFormatModifierName(mod); auto n = drmGetFormatModifierName(mod);
if (!n)
return "unknown";
std::string name = n; std::string name = n;
free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors) free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors)
return name; return name;

View file

@ -53,8 +53,6 @@ namespace NFormatUtils {
bool isFormatOpaque(DRMFormat drm); bool isFormatOpaque(DRMFormat drm);
int pixelsPerBlock(const SPixelFormat* const fmt); int pixelsPerBlock(const SPixelFormat* const fmt);
int minStride(const SPixelFormat* const fmt, int32_t width); int minStride(const SPixelFormat* const fmt, int32_t width);
uint32_t drmFormatToGL(DRMFormat drm);
uint32_t glFormatToType(uint32_t gl);
std::string drmFormatName(DRMFormat drm); std::string drmFormatName(DRMFormat drm);
std::string drmModifierName(uint64_t mod); std::string drmModifierName(uint64_t mod);
DRMFormat alphaFormat(DRMFormat prevFormat); DRMFormat alphaFormat(DRMFormat prevFormat);

View file

@ -30,7 +30,7 @@
#include "../hyprerror/HyprError.hpp" #include "../hyprerror/HyprError.hpp"
#include "../layout/LayoutManager.hpp" #include "../layout/LayoutManager.hpp"
#include "../i18n/Engine.hpp" #include "../i18n/Engine.hpp"
#include "../protocols/types/ColorManagement.hpp" #include "../helpers/cm/ColorManagement.hpp"
#include "sync/SyncTimeline.hpp" #include "sync/SyncTimeline.hpp"
#include "time/Time.hpp" #include "time/Time.hpp"
#include "../desktop/view/LayerSurface.hpp" #include "../desktop/view/LayerSurface.hpp"
@ -82,7 +82,10 @@ void CMonitor::onConnect(bool noRule) {
m_zoomAnimProgress->setValueAndWarp(0.F); m_zoomAnimProgress->setValueAndWarp(0.F);
m_zoomAnimFrameCounter = 0; m_zoomAnimFrameCounter = 0;
g_pEventLoopManager->doLater([] { g_pConfigManager->ensurePersistentWorkspacesPresent(); }); g_pEventLoopManager->doLater([] {
g_pConfigManager->ensurePersistentWorkspacesPresent();
g_pCompositor->ensureWorkspacesOnAssignedMonitors();
});
m_listeners.frame = m_output->events.frame.listen([this] { m_listeners.frame = m_output->events.frame.listen([this] {
if (m_frameScheduler) if (m_frameScheduler)
@ -290,9 +293,15 @@ void CMonitor::onConnect(bool noRule) {
if (!valid(ws)) if (!valid(ws))
continue; continue;
if (ws->m_lastMonitor == m_name || g_pCompositor->m_monitors.size() == 1 /* avoid lost workspaces on recover */) { const auto CURRENTMON = ws->m_monitor.lock();
const bool ORPHANED = !CURRENTMON || std::ranges::none_of(g_pCompositor->m_monitors, [&](const auto& mon) { return mon == CURRENTMON; });
const bool RETURNING = ws->m_lastMonitor == m_name;
const bool RECOVERY = g_pCompositor->m_monitors.size() == 1 && ORPHANED; // temporarily recover orphaned workspaces
if (RETURNING || RECOVERY) {
g_pCompositor->moveWorkspaceToMonitor(ws, m_self.lock()); g_pCompositor->moveWorkspaceToMonitor(ws, m_self.lock());
g_pDesktopAnimationManager->startAnimation(ws, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true); g_pDesktopAnimationManager->startAnimation(ws, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
if (RETURNING)
ws->m_lastMonitor = ""; ws->m_lastMonitor = "";
} }
} }
@ -429,19 +438,24 @@ void CMonitor::onDisconnect(bool destroy) {
m_enabled = false; m_enabled = false;
m_renderingInitPassed = false; m_renderingInitPassed = false;
if (BACKUPMON) {
// snap cursor
g_pCompositor->warpCursorTo(BACKUPMON->m_position + BACKUPMON->m_transformedSize / 2.F, true);
// move workspaces
std::vector<PHLWORKSPACE> wspToMove; std::vector<PHLWORKSPACE> wspToMove;
for (auto const& w : g_pCompositor->getWorkspaces()) { for (auto const& w : g_pCompositor->getWorkspaces()) {
if (w->m_monitor == m_self || !w->m_monitor) if (w->m_monitor == m_self || !w->m_monitor)
wspToMove.emplace_back(w.lock()); wspToMove.emplace_back(w.lock());
} }
// Preserve ownership across cascaded monitor disconnects.
// The first disconnected monitor "owns" where a workspace should return.
for (auto const& w : wspToMove) { for (auto const& w : wspToMove) {
if (w && w->m_lastMonitor.empty())
w->m_lastMonitor = m_name; w->m_lastMonitor = m_name;
}
if (BACKUPMON) {
// snap cursor
g_pCompositor->warpCursorTo(BACKUPMON->m_position + BACKUPMON->m_transformedSize / 2.F, true);
for (auto const& w : wspToMove) {
g_pCompositor->moveWorkspaceToMonitor(w, BACKUPMON); g_pCompositor->moveWorkspaceToMonitor(w, BACKUPMON);
g_pDesktopAnimationManager->startAnimation(w, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true); g_pDesktopAnimationManager->startAnimation(w, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
} }
@ -919,6 +933,9 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
m_supportsWideColor = RULE->supportsHDR; m_supportsWideColor = RULE->supportsHDR;
m_supportsHDR = RULE->supportsHDR; m_supportsHDR = RULE->supportsHDR;
if (RULE->iccFile.empty()) {
// only apply explicit cm settings if we have no icc file
m_cmType = RULE->cmType; m_cmType = RULE->cmType;
switch (m_cmType) { switch (m_cmType) {
case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break; case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break;
@ -941,6 +958,20 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
m_sdrSaturation = RULE->sdrSaturation; m_sdrSaturation = RULE->sdrSaturation;
m_sdrBrightness = RULE->sdrBrightness; m_sdrBrightness = RULE->sdrBrightness;
} else {
auto image = NColorManagement::SImageDescription::fromICC(RULE->iccFile);
if (!image) {
Log::logger->log(Log::ERR, "icc for {} ({}) failed: {}", m_name, RULE->iccFile, image.error());
g_pConfigManager->addParseError(std::format("failed to apply icc {} to {}: {}", RULE->iccFile, m_name, image.error()));
} else {
m_imageDescription = CImageDescription::from(*image);
if (!m_imageDescription) {
Log::logger->log(Log::ERR, "icc for {} ({}) failed 2: {}", m_name, RULE->iccFile, image.error());
g_pConfigManager->addParseError(std::format("failed to apply icc {} to {}: {}", RULE->iccFile, m_name, image.error()));
m_imageDescription = CImageDescription::from(SImageDescription{});
}
}
}
Vector2D logicalSize = m_pixelSize / m_scale; Vector2D logicalSize = m_pixelSize / m_scale;
if (!*PDISABLESCALECHECKS && (logicalSize.x != std::round(logicalSize.x) || logicalSize.y != std::round(logicalSize.y))) { if (!*PDISABLESCALECHECKS && (logicalSize.x != std::round(logicalSize.x) || logicalSize.y != std::round(logicalSize.y))) {
@ -1023,6 +1054,8 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
m_damage.setSize(m_transformedSize); m_damage.setSize(m_transformedSize);
updateVCGTRamps();
// Set scale for all surfaces on this monitor, needed for some clients // Set scale for all surfaces on this monitor, needed for some clients
// but not on unsafe state to avoid crashes // but not on unsafe state to avoid crashes
if (!g_pCompositor->m_unsafeState) { if (!g_pCompositor->m_unsafeState) {
@ -1329,7 +1362,7 @@ void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bo
// move pinned windows // move pinned windows
for (auto const& w : g_pCompositor->m_windows) { for (auto const& w : g_pCompositor->m_windows) {
if (w->m_workspace == POLDWORKSPACE && w->m_pinned) if (w->m_workspace == POLDWORKSPACE && w->m_pinned)
w->moveToWorkspace(pWorkspace); w->layoutTarget()->assignToSpace(pWorkspace->m_space);
} }
if (!noFocus && !Desktop::focusState()->monitor()->m_activeSpecialWorkspace && if (!noFocus && !Desktop::focusState()->monitor()->m_activeSpecialWorkspace &&
@ -1448,6 +1481,7 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
if (const auto PMWSOWNER = pWorkspace->m_monitor.lock(); PMWSOWNER && PMWSOWNER->m_activeSpecialWorkspace == pWorkspace) { if (const auto PMWSOWNER = pWorkspace->m_monitor.lock(); PMWSOWNER && PMWSOWNER->m_activeSpecialWorkspace == pWorkspace) {
PMWSOWNER->m_activeSpecialWorkspace.reset(); PMWSOWNER->m_activeSpecialWorkspace.reset();
g_layoutManager->recalculateMonitor(PMWSOWNER); g_layoutManager->recalculateMonitor(PMWSOWNER);
g_pHyprRenderer->damageMonitor(PMWSOWNER);
g_pEventManager->postEvent(SHyprIPCEvent{"activespecial", "," + PMWSOWNER->m_name}); g_pEventManager->postEvent(SHyprIPCEvent{"activespecial", "," + PMWSOWNER->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"activespecialv2", ",," + PMWSOWNER->m_name}); g_pEventManager->postEvent(SHyprIPCEvent{"activespecialv2", ",," + PMWSOWNER->m_name});
@ -1545,10 +1579,20 @@ Vector2D CMonitor::middle() {
return m_position + m_size / 2.f; return m_position + m_size / 2.f;
} }
const Mat3x3& CMonitor::getTransformMatrix() {
return m_projMatrix;
}
const Mat3x3& CMonitor::getScaleMatrix() {
return m_projOutputMatrix;
}
void CMonitor::updateMatrix() { void CMonitor::updateMatrix() {
m_projMatrix = Mat3x3::identity(); m_projMatrix = Mat3x3::identity();
if (m_transform != WL_OUTPUT_TRANSFORM_NORMAL) if (m_transform != WL_OUTPUT_TRANSFORM_NORMAL)
m_projMatrix.translate(m_pixelSize / 2.0).transform(Math::wlTransformToHyprutils(m_transform)).translate(-m_transformedSize / 2.0); m_projMatrix.translate(m_pixelSize / 2.0).transform(Math::wlTransformToHyprutils(m_transform)).translate(-m_transformedSize / 2.0);
m_projOutputMatrix = Mat3x3::outputProjection(m_pixelSize, HYPRUTILS_TRANSFORM_NORMAL);
} }
WORKSPACEID CMonitor::activeWorkspaceID() { WORKSPACEID CMonitor::activeWorkspaceID() {
@ -1835,7 +1879,7 @@ uint16_t CMonitor::isDSBlocked(bool full) {
// we can't scanout shm buffers. // we can't scanout shm buffers.
const auto params = PSURFACE->m_current.buffer->dmabuf(); const auto params = PSURFACE->m_current.buffer->dmabuf();
if (!params.success || !PSURFACE->m_current.texture->m_eglImage /* dmabuf */) { if (!params.success || !PSURFACE->m_current.texture->isDMA() /* dmabuf */) {
reasons |= DS_BLOCK_DMA; reasons |= DS_BLOCK_DMA;
if (!full) if (!full)
return reasons; return reasons;
@ -2172,8 +2216,8 @@ bool CMonitor::canNoShaderCM() {
const auto SRC_DESC_VALUE = SRC_DESC.value()->value(); const auto SRC_DESC_VALUE = SRC_DESC.value()->value();
if (SRC_DESC_VALUE.icc.fd >= 0 || m_imageDescription->value().icc.fd >= 0) if (m_imageDescription->value().icc.present)
return false; // no ICC support return false;
const auto sdrEOTF = NTransferFunction::fromConfig(); const auto sdrEOTF = NTransferFunction::fromConfig();
// only primaries differ // only primaries differ
@ -2192,6 +2236,71 @@ bool CMonitor::doesNoShaderCM() {
return m_noShaderCTM; return m_noShaderCTM;
} }
static std::vector<uint16_t> resampleInterleavedToKms(const SVCGTTable16& t, size_t gammaSize) {
std::vector<uint16_t> out;
out.resize(gammaSize * 3);
//
auto sample = [&](int c, float x) -> uint16_t {
const float maxX = t.entries - 1;
x = std::clamp(x, 0.F, maxX);
const size_t i0 = (size_t)std::floor(x);
const size_t i1 = std::min(i0 + 1, (size_t)t.entries - 1);
const float f = x - sc<float>(i0);
const float v0 = sc<float>(t.ch[c][i0]);
const float v1 = sc<float>(t.ch[c][i1]);
const float v = v0 + ((v1 - v0) * f);
int64_t vi = std::round(v);
vi = std::clamp(vi, sc<int64_t>(0), sc<int64_t>(65535));
return sc<uint16_t>(vi);
};
for (size_t i = 0; i < gammaSize; ++i) {
float x = sc<float>(i) * sc<float>(t.entries - 1) / sc<float>(gammaSize - 1);
const uint16_t r = sample(0, x);
const uint16_t g = sample(1, x);
const uint16_t b = sample(2, x);
out[i * 3 + 0] = r;
out[i * 3 + 1] = g;
out[i * 3 + 2] = b;
}
return out;
}
void CMonitor::updateVCGTRamps() {
auto gammaSize = m_output->getGammaSize();
if (gammaSize <= 10) {
Log::logger->log(Log::DEBUG, "CMonitor::updateVCGTRamps: skipping, no gamma ramp for output");
return;
}
if (!m_imageDescription->value().icc.vcgt) {
if (m_vcgtRampsSet)
m_output->state->setGammaLut({});
m_vcgtRampsSet = false;
return;
}
// build table
auto table = resampleInterleavedToKms(*m_imageDescription->value().icc.vcgt, gammaSize);
m_output->state->setGammaLut(table);
m_vcgtRampsSet = true;
}
bool CMonitor::gammaRampsInUse() {
return m_vcgtRampsSet;
}
CMonitorState::CMonitorState(CMonitor* owner) : m_owner(owner) { CMonitorState::CMonitorState(CMonitor* owner) : m_owner(owner) {
; ;
} }

View file

@ -16,7 +16,7 @@
#include "math/Math.hpp" #include "math/Math.hpp"
#include "../desktop/reserved/ReservedArea.hpp" #include "../desktop/reserved/ReservedArea.hpp"
#include <optional> #include <optional>
#include "../protocols/types/ColorManagement.hpp" #include "cm/ColorManagement.hpp"
#include "signal/Signal.hpp" #include "signal/Signal.hpp"
#include "DamageRing.hpp" #include "DamageRing.hpp"
#include <aquamarine/output/Output.hpp> #include <aquamarine/output/Output.hpp>
@ -56,6 +56,7 @@ struct SMonitorRule {
float sdrSaturation = 1.0f; // SDR -> HDR float sdrSaturation = 1.0f; // SDR -> HDR
float sdrBrightness = 1.0f; // SDR -> HDR float sdrBrightness = 1.0f; // SDR -> HDR
Desktop::CReservedArea reservedArea; Desktop::CReservedArea reservedArea;
std::string iccFile;
int supportsWideColor = 0; // 0 - auto, 1 - force enable, -1 - force disable int supportsWideColor = 0; // 0 - auto, 1 - force enable, -1 - force disable
int supportsHDR = 0; // 0 - auto, 1 - force enable, -1 - force disable int supportsHDR = 0; // 0 - auto, 1 - force enable, -1 - force disable
@ -126,7 +127,7 @@ class CMonitor {
bool m_scheduledRecalc = false; bool m_scheduledRecalc = false;
wl_output_transform m_transform = WL_OUTPUT_TRANSFORM_NORMAL; wl_output_transform m_transform = WL_OUTPUT_TRANSFORM_NORMAL;
float m_xwaylandScale = 1.f; float m_xwaylandScale = 1.f;
Mat3x3 m_projMatrix;
std::optional<Vector2D> m_forceSize; std::optional<Vector2D> m_forceSize;
SP<Aquamarine::SOutputMode> m_currentMode; SP<Aquamarine::SOutputMode> m_currentMode;
SP<Aquamarine::CSwapchain> m_cursorSwapchain; SP<Aquamarine::CSwapchain> m_cursorSwapchain;
@ -302,7 +303,6 @@ class CMonitor {
void setSpecialWorkspace(const WORKSPACEID& id); void setSpecialWorkspace(const WORKSPACEID& id);
void moveTo(const Vector2D& pos); void moveTo(const Vector2D& pos);
Vector2D middle(); Vector2D middle();
void updateMatrix();
WORKSPACEID activeWorkspaceID(); WORKSPACEID activeWorkspaceID();
WORKSPACEID activeSpecialWorkspaceID(); WORKSPACEID activeSpecialWorkspaceID();
CBox logicalBox(); CBox logicalBox();
@ -332,6 +332,11 @@ class CMonitor {
bool wantsHDR(); bool wantsHDR();
bool inHDR(); bool inHDR();
bool gammaRampsInUse();
//
const Mat3x3& getTransformMatrix();
const Mat3x3& getScaleMatrix();
/// Has an active workspace with a real fullscreen window (includes special workspace) /// Has an active workspace with a real fullscreen window (includes special workspace)
bool inFullscreenMode(); bool inFullscreenMode();
@ -353,7 +358,7 @@ class CMonitor {
PHLWINDOWREF m_previousFSWindow; PHLWINDOWREF m_previousFSWindow;
bool m_needsHDRupdate = false; bool m_needsHDRupdate = false;
NColorManagement::PImageDescription m_imageDescription; NColorManagement::PImageDescription m_imageDescription = NColorManagement::CImageDescription::from(NColorManagement::SImageDescription{});
bool m_noShaderCTM = false; // sets drm CTM, restore needed bool m_noShaderCTM = false; // sets drm CTM, restore needed
// For the list lookup // For the list lookup
@ -362,12 +367,19 @@ class CMonitor {
return m_position == rhs.m_position && m_size == rhs.m_size && m_name == rhs.m_name; return m_position == rhs.m_position && m_size == rhs.m_size && m_name == rhs.m_name;
} }
Mat3x3 m_projMatrix;
private: private:
void updateMatrix();
Mat3x3 m_projOutputMatrix;
void setupDefaultWS(const SMonitorRule&); void setupDefaultWS(const SMonitorRule&);
WORKSPACEID findAvailableDefaultWS(); WORKSPACEID findAvailableDefaultWS();
void commitDPMSState(bool state); void commitDPMSState(bool state);
void updateVCGTRamps();
bool m_doneScheduled = false; bool m_doneScheduled = false;
bool m_vcgtRampsSet = false;
std::stack<WORKSPACEID> m_prevWorkSpaces; std::stack<WORKSPACEID> m_prevWorkSpaces;
struct { struct {

View file

@ -26,7 +26,10 @@ std::string NTransferFunction::toString(eTF tf) {
return ""; return "";
} }
eTF NTransferFunction::fromConfig() { eTF NTransferFunction::fromConfig(bool useICC) {
if (useICC)
return TF_SRGB;
static auto PSDREOTF = CConfigValue<Hyprlang::STRING>("render:cm_sdr_eotf"); static auto PSDREOTF = CConfigValue<Hyprlang::STRING>("render:cm_sdr_eotf");
static auto sdrEOTF = NTransferFunction::fromString(*PSDREOTF); static auto sdrEOTF = NTransferFunction::fromString(*PSDREOTF);
static auto P = Event::bus()->m_events.config.reloaded.listen([]() { sdrEOTF = NTransferFunction::fromString(*PSDREOTF); }); static auto P = Event::bus()->m_events.config.reloaded.listen([]() { sdrEOTF = NTransferFunction::fromString(*PSDREOTF); });

View file

@ -15,5 +15,5 @@ namespace NTransferFunction {
eTF fromString(const std::string tfName); eTF fromString(const std::string tfName);
std::string toString(eTF tf); std::string toString(eTF tf);
eTF fromConfig(); eTF fromConfig(bool useICC = false);
} }

View file

@ -0,0 +1,193 @@
#include "ColorManagement.hpp"
#include "../../macros.hpp"
#include <hyprutils/memory/UniquePtr.hpp>
#include <map>
#include <vector>
using namespace NColorManagement;
namespace NColorManagement {
// expected to be small
static std::vector<UP<const CPrimaries>> knownPrimaries;
static std::vector<UP<const CImageDescription>> knownDescriptions;
static std::map<std::pair<uint, uint>, Hyprgraphics::CMatrix3> primariesConversion;
}
const SPCPRimaries& NColorManagement::getPrimaries(ePrimaries name) {
switch (name) {
case CM_PRIMARIES_SRGB: return NColorPrimaries::BT709;
case CM_PRIMARIES_BT2020: return NColorPrimaries::BT2020;
case CM_PRIMARIES_PAL_M: return NColorPrimaries::PAL_M;
case CM_PRIMARIES_PAL: return NColorPrimaries::PAL;
case CM_PRIMARIES_NTSC: return NColorPrimaries::NTSC;
case CM_PRIMARIES_GENERIC_FILM: return NColorPrimaries::GENERIC_FILM;
case CM_PRIMARIES_CIE1931_XYZ: return NColorPrimaries::CIE1931_XYZ;
case CM_PRIMARIES_DCI_P3: return NColorPrimaries::DCI_P3;
case CM_PRIMARIES_DISPLAY_P3: return NColorPrimaries::DISPLAY_P3;
case CM_PRIMARIES_ADOBE_RGB: return NColorPrimaries::ADOBE_RGB;
default: return NColorPrimaries::DEFAULT_PRIMARIES;
}
}
CPrimaries::CPrimaries(const SPCPRimaries& primaries, const uint32_t primariesId) : m_id(primariesId), m_primaries(primaries) {
m_primaries2XYZ = m_primaries.toXYZ();
}
WP<const CPrimaries> CPrimaries::from(const SPCPRimaries& primaries) {
for (const auto& known : knownPrimaries) {
if (known->value() == primaries)
return known;
}
knownPrimaries.emplace_back(CUniquePointer(new CPrimaries(primaries, knownPrimaries.size() + 1)));
return knownPrimaries.back();
}
WP<const CPrimaries> CPrimaries::from(const ePrimaries name) {
return from(getPrimaries(name));
}
WP<const CPrimaries> CPrimaries::from(const uint32_t primariesId) {
ASSERT(primariesId <= knownPrimaries.size());
return knownPrimaries[primariesId - 1];
}
const SPCPRimaries& CPrimaries::value() const {
return m_primaries;
}
uint CPrimaries::id() const {
return m_id;
}
const Hyprgraphics::CMatrix3& CPrimaries::toXYZ() const {
return m_primaries2XYZ;
}
const Hyprgraphics::CMatrix3& CPrimaries::convertMatrix(const WP<const CPrimaries> dst) const {
const auto cacheKey = std::make_pair(m_id, dst->m_id);
if (!primariesConversion.contains(cacheKey))
primariesConversion.insert(std::make_pair(cacheKey, m_primaries.convertMatrix(dst->m_primaries)));
return primariesConversion[cacheKey];
}
CImageDescription::CImageDescription(const SImageDescription& imageDescription, const uint32_t imageDescriptionId) :
m_id(imageDescriptionId), m_imageDescription(imageDescription) {
m_primariesId = CPrimaries::from(m_imageDescription.getPrimaries())->id();
}
PImageDescription CImageDescription::from(const SImageDescription& imageDescription) {
for (const auto& known : knownDescriptions) {
if (known->value() == imageDescription)
return known;
}
knownDescriptions.emplace_back(UP<CImageDescription>(new CImageDescription(imageDescription, knownDescriptions.size() + 1)));
return knownDescriptions.back();
}
PImageDescription CImageDescription::from(const uint32_t imageDescriptionId) {
ASSERT(imageDescriptionId <= knownDescriptions.size());
return knownDescriptions[imageDescriptionId - 1];
}
PImageDescription CImageDescription::with(const SImageDescription::SPCLuminances& luminances) const {
auto desc = m_imageDescription;
desc.luminances = luminances;
return CImageDescription::from(desc);
}
const SImageDescription& CImageDescription::value() const {
return m_imageDescription;
}
uint CImageDescription::id() const {
return m_id;
}
WP<const CPrimaries> CImageDescription::getPrimaries() const {
return CPrimaries::from(m_primariesId);
}
static Mat3x3 diag3(const std::array<float, 3>& s) {
return Mat3x3{std::array<float, 9>{s[0], 0, 0, 0, s[1], 0, 0, 0, s[2]}};
}
static std::optional<Mat3x3> invertMat3(const Mat3x3& m) {
const auto ARR = m.getMatrix();
const double a = ARR[0], b = ARR[1], c = ARR[2];
const double d = ARR[3], e = ARR[4], f = ARR[5];
const double g = ARR[6], h = ARR[7], i = ARR[8];
const double A = (e * i - f * h);
const double B = -(d * i - f * g);
const double C = (d * h - e * g);
const double D = -(b * i - c * h);
const double E = (a * i - c * g);
const double F = -(a * h - b * g);
const double G = (b * f - c * e);
const double H = -(a * f - c * d);
const double I = (a * e - b * d);
const double det = a * A + b * B + c * C;
if (std::abs(det) < 1e-18)
return std::nullopt;
const double invDet = 1.0 / det;
Mat3x3 inv{std::array<float, 9>{
A * invDet,
D * invDet,
G * invDet, //
B * invDet,
E * invDet,
H * invDet, //
C * invDet,
F * invDet,
I * invDet, //
}};
return inv;
}
static std::array<float, 3> matByVec(const Mat3x3& M, const std::array<float, 3>& v) {
const auto ARR = M.getMatrix();
return {ARR[0] * v[0] + ARR[1] * v[1] + ARR[2] * v[2], ARR[3] * v[0] + ARR[4] * v[1] + ARR[5] * v[2], ARR[6] * v[0] + ARR[7] * v[1] + ARR[8] * v[2]};
}
std::optional<Mat3x3> NColorManagement::rgbToXYZFromPrimaries(SPCPRimaries pr) {
const auto R = Hyprgraphics::xy2xyz(pr.red);
const auto G = Hyprgraphics::xy2xyz(pr.green);
const auto B = Hyprgraphics::xy2xyz(pr.blue);
const auto W = Hyprgraphics::xy2xyz(pr.white);
// P has columns R,G,B
Mat3x3 P{std::array<float, 9>{R.x, G.x, B.x, R.y, G.y, B.y, R.z, G.z, B.z}};
auto invP = invertMat3(P);
if (!invP)
return std::nullopt;
const auto S = matByVec(*invP, {W.x, W.y, W.z});
P.multiply(diag3(S)); // RGB->XYZ
return P;
}
Mat3x3 NColorManagement::adaptBradford(Hyprgraphics::CColor::xy srcW, Hyprgraphics::CColor::xy dstW) {
static const Mat3x3 Bradford{std::array<float, 9>{0.8951f, 0.2664f, -0.1614f, -0.7502f, 1.7135f, 0.0367f, 0.0389f, -0.0685f, 1.0296f}};
static const Mat3x3 BradfordInv = invertMat3(Bradford).value();
const auto srcXYZ = Hyprgraphics::xy2xyz(srcW);
const auto dstXYZ = Hyprgraphics::xy2xyz(dstW);
const auto srcLMS = matByVec(Bradford, {srcXYZ.x, srcXYZ.y, srcXYZ.z});
const auto dstLMS = matByVec(Bradford, {dstXYZ.x, dstXYZ.y, dstXYZ.z});
const std::array<float, 3> scale{dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2]};
Mat3x3 result = BradfordInv;
result.multiply(diag3(scale)).multiply(Bradford);
return result;
}

View file

@ -3,6 +3,11 @@
#include "color-management-v1.hpp" #include "color-management-v1.hpp"
#include <hyprgraphics/color/Color.hpp> #include <hyprgraphics/color/Color.hpp>
#include "../../helpers/memory/Memory.hpp" #include "../../helpers/memory/Memory.hpp"
#include "../../helpers/math/Math.hpp"
#include <filesystem>
#include <vector>
#include <expected>
#define SDR_MIN_LUMINANCE 0.2 #define SDR_MIN_LUMINANCE 0.2
#define SDR_MAX_LUMINANCE 80.0 #define SDR_MAX_LUMINANCE 80.0
@ -12,6 +17,8 @@
#define HDR_REF_LUMINANCE 203.0 #define HDR_REF_LUMINANCE 203.0
#define HLG_MAX_LUMINANCE 1000.0 #define HLG_MAX_LUMINANCE 1000.0
class ITexture;
namespace NColorManagement { namespace NColorManagement {
enum eNoShader : uint8_t { enum eNoShader : uint8_t {
CM_NS_DISABLE = 0, CM_NS_DISABLE = 0,
@ -67,7 +74,6 @@ namespace NColorManagement {
using SPCPRimaries = Hyprgraphics::SPCPRimaries; using SPCPRimaries = Hyprgraphics::SPCPRimaries;
namespace NColorPrimaries { namespace NColorPrimaries {
static const auto DEFAULT_PRIMARIES = SPCPRimaries{};
static const auto BT709 = SPCPRimaries{ static const auto BT709 = SPCPRimaries{
.red = {.x = 0.64, .y = 0.33}, .red = {.x = 0.64, .y = 0.33},
@ -76,6 +82,8 @@ namespace NColorManagement {
.white = {.x = 0.3127, .y = 0.3290}, .white = {.x = 0.3127, .y = 0.3290},
}; };
static const auto DEFAULT_PRIMARIES = BT709;
static const auto PAL_M = SPCPRimaries{ static const auto PAL_M = SPCPRimaries{
.red = {.x = 0.67, .y = 0.33}, .red = {.x = 0.67, .y = 0.33},
.green = {.x = 0.21, .y = 0.71}, .green = {.x = 0.21, .y = 0.71},
@ -140,7 +148,16 @@ namespace NColorManagement {
}; };
} }
struct SVCGTTable16 {
uint16_t channels = 0;
uint16_t entries = 0;
uint16_t entrySize = 0;
std::array<std::vector<uint16_t>, 3> ch;
};
const SPCPRimaries& getPrimaries(ePrimaries name); const SPCPRimaries& getPrimaries(ePrimaries name);
std::optional<Mat3x3> rgbToXYZFromPrimaries(SPCPRimaries pr);
Mat3x3 adaptBradford(Hyprgraphics::CColor::xy srcW, Hyprgraphics::CColor::xy dstW);
class CPrimaries { class CPrimaries {
public: public:
@ -163,19 +180,14 @@ namespace NColorManagement {
}; };
struct SImageDescription { struct SImageDescription {
struct SIccFile { static std::expected<SImageDescription, std::string> fromICC(const std::filesystem::path& file);
int fd = -1;
uint32_t length = 0;
uint32_t offset = 0;
bool operator==(const SIccFile& i2) const {
return fd == i2.fd;
}
} icc;
bool windowsScRGB = false; //
std::vector<uint8_t> rawICC;
eTransferFunction transferFunction = CM_TRANSFER_FUNCTION_GAMMA22; eTransferFunction transferFunction = CM_TRANSFER_FUNCTION_GAMMA22;
float transferFunctionPower = 1.0f; float transferFunctionPower = 1.0f;
bool windowsScRGB = false;
bool primariesNameSet = false; bool primariesNameSet = false;
ePrimaries primariesNamed = CM_PRIMARIES_SRGB; ePrimaries primariesNamed = CM_PRIMARIES_SRGB;
@ -202,11 +214,23 @@ namespace NColorManagement {
} }
} masteringLuminances; } masteringLuminances;
// Matrix data from ICC
struct SICCData {
bool present = false;
size_t lutSize = 33;
std::vector<float> lutDataPacked;
SP<ITexture> lutTexture;
std::optional<SVCGTTable16> vcgt;
} icc;
uint32_t maxCLL = 0; uint32_t maxCLL = 0;
uint32_t maxFALL = 0; uint32_t maxFALL = 0;
bool operator==(const SImageDescription& d2) const { bool operator==(const SImageDescription& d2) const {
return icc == d2.icc && windowsScRGB == d2.windowsScRGB && transferFunction == d2.transferFunction && transferFunctionPower == d2.transferFunctionPower && if (icc.present || d2.icc.present)
return false;
return windowsScRGB == d2.windowsScRGB && transferFunction == d2.transferFunction && transferFunctionPower == d2.transferFunctionPower &&
(primariesNameSet == d2.primariesNameSet && (primariesNameSet ? primariesNamed == d2.primariesNamed : primaries == d2.primaries)) && (primariesNameSet == d2.primariesNameSet && (primariesNameSet ? primariesNamed == d2.primariesNamed : primaries == d2.primaries)) &&
masteringPrimaries == d2.masteringPrimaries && luminances == d2.luminances && masteringLuminances == d2.masteringLuminances && maxCLL == d2.maxCLL && masteringPrimaries == d2.masteringPrimaries && luminances == d2.luminances && masteringLuminances == d2.masteringLuminances && maxCLL == d2.maxCLL &&
maxFALL == d2.maxFALL; maxFALL == d2.maxFALL;
@ -280,44 +304,48 @@ namespace NColorManagement {
class CImageDescription { class CImageDescription {
public: public:
static WP<const CImageDescription> from(const SImageDescription& imageDescription); static WP<const CImageDescription> from(const SImageDescription& imageDescription);
static WP<const CImageDescription> from(const uint imageDescriptionId); static WP<const CImageDescription> from(const uint32_t imageDescriptionId);
WP<const CImageDescription> with(const SImageDescription::SPCLuminances& luminances) const; WP<const CImageDescription> with(const SImageDescription::SPCLuminances& luminances) const;
const SImageDescription& value() const; const SImageDescription& value() const;
uint id() const; uint32_t id() const;
WP<const CPrimaries> getPrimaries() const; WP<const CPrimaries> getPrimaries() const;
private: private:
CImageDescription(const SImageDescription& imageDescription, const uint imageDescriptionId); CImageDescription(const SImageDescription& imageDescription, const uint imageDescriptionId);
uint m_id; uint32_t m_id = 0;
uint m_primariesId; uint32_t m_primariesId = 0;
SImageDescription m_imageDescription; SImageDescription m_imageDescription;
}; };
using PImageDescription = WP<const CImageDescription>; using PImageDescription = WP<const CImageDescription>;
static const auto DEFAULT_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22, static const auto DEFAULT_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{
.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22,
.primariesNameSet = true, .primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_SRGB, .primariesNamed = NColorManagement::CM_PRIMARIES_SRGB,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_SRGB), .primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_SRGB),
.luminances = {.min = SDR_MIN_LUMINANCE, .max = 80, .reference = 80}}); .luminances = {.min = SDR_MIN_LUMINANCE, .max = 80, .reference = 80},
});
static const auto DEFAULT_HDR_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ, static const auto DEFAULT_HDR_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{
.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ,
.primariesNameSet = true, .primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020, .primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020), .primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.luminances = {.min = HDR_MIN_LUMINANCE, .max = 10000, .reference = 203}}); .luminances = {.min = HDR_MIN_LUMINANCE, .max = 10000, .reference = 203},
; });
static const auto SCRGB_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{ static const auto SCRGB_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{
.windowsScRGB = true,
.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_EXT_LINEAR, .transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_EXT_LINEAR,
.windowsScRGB = true,
.primariesNameSet = true, .primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_SRGB, .primariesNamed = NColorManagement::CM_PRIMARIES_SRGB,
.primaries = NColorPrimaries::BT709, .primaries = NColorPrimaries::BT709,
.luminances = {.reference = 203}, .luminances = {.reference = 203},
}); });
;
static const auto LINEAR_IMAGE_DESCRIPTION = SCRGB_IMAGE_DESCRIPTION; // TODO any reason to use something different?
} }

278
src/helpers/cm/ICC.cpp Normal file
View file

@ -0,0 +1,278 @@
#include "ColorManagement.hpp"
#include "../math/Math.hpp"
#include <cstddef>
#include <fstream>
#include "../../debug/log/Logger.hpp"
#include "../../render/Texture.hpp"
#include "../../render/Renderer.hpp"
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::Utils;
#include <lcms2.h>
using namespace NColorManagement;
static std::vector<uint8_t> readBinary(const std::filesystem::path& file) {
std::ifstream ifs(file, std::ios::binary);
if (!ifs.good())
return {};
ifs.seekg(0, std::ios::end);
size_t len = ifs.tellg();
ifs.seekg(0, std::ios::beg);
if (len <= 0)
return {};
std::vector<uint8_t> buf;
buf.resize(len);
ifs.read(reinterpret_cast<char*>(buf.data()), len);
return buf;
}
static uint16_t bigEndianU16(const uint8_t* p) {
return (uint16_t)((uint16_t)p[0] << 8 | (uint16_t)p[1]);
}
static uint32_t bigEndianU32(const uint8_t* p) {
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3];
}
static constexpr cmsTagSignature makeSig(char a, char b, char c, char d) {
return sc<cmsTagSignature>(sc<uint32_t>(a) << 24 | sc<uint32_t>(b) << 16 | sc<uint32_t>(c) << 8 | sc<uint32_t>(d));
}
static constexpr cmsTagSignature VCGT_SIG = makeSig('v', 'c', 'g', 't');
//
static std::expected<std::optional<SVCGTTable16>, std::string> readVCGT16(cmsHPROFILE prof) {
if (!cmsIsTag(prof, VCGT_SIG))
return std::nullopt;
cmsUInt32Number n = cmsReadRawTag(prof, VCGT_SIG, nullptr, 0);
if (n < 8 + 4 + 2 + 2 + 2 + 2) // header + type + table header
return std::unexpected("Malformed vcgt tag");
std::vector<uint8_t> raw(n);
if (cmsReadRawTag(prof, VCGT_SIG, raw.data(), n) != n)
return std::unexpected("Malformed vcgt tag");
// raw layout:
// 0 ... 3: 'vcgt'
// 4 ... 7: reserved
// 8 ... 11: gammaType (0 = table)
uint32_t gammaType = bigEndianU32(raw.data() + 8);
if (gammaType != 0)
return std::unexpected("VCGT formula type is not supported by Hyprland");
SVCGTTable16 table;
table.channels = bigEndianU16(raw.data() + 12);
table.entries = bigEndianU16(raw.data() + 14);
table.entrySize = bigEndianU16(raw.data() + 16);
// raw+18: reserved u16
Log::logger->log(Log::DEBUG, "readVCGT16: table has {} channels, {} entries, and entry size of {}", table.channels, table.entries, table.entrySize);
if (table.channels != 3 || table.entrySize != 2 || table.entries == 0)
return std::unexpected("invalid vcgt table size");
size_t tableBytes = (size_t)table.channels * (size_t)table.entries * (size_t)table.entrySize;
// VCGT is a piece of shit and some absolute fucking mongoloid idiots
// decided it'd be great to have both 18 and 20
// FUCK YOU
size_t tableOff = 20;
auto readTable = [&] -> void {
for (int c = 0; c < 3; ++c) {
table.ch[c].resize(table.entries);
for (uint16_t i = 0; i < table.entries; ++i) {
const uint8_t* p = raw.data() + tableOff + static_cast<ptrdiff_t>((c * table.entries + i) * 2);
table.ch[c][i] = bigEndianU16(p); // 0 ... 65535
}
}
};
if (raw.size() < tableOff + tableBytes) {
tableOff = 18;
if (raw.size() < tableOff + tableBytes) {
Log::logger->log(Log::ERR, "readVCGT16: table is too short, tag is invalid");
return std::unexpected("table is too short");
}
Log::logger->log(Log::DEBUG, "readVCGT16: table is too short, but off = 18 fits. Attempting offset = 18");
readTable();
} else {
readTable();
// if the table's last entry is suspiciously low, we more than likely read an 18 as a 20.
if (table.ch[0][table.entries - 1] < 30000) {
Log::logger->log(Log::DEBUG, "readVCGT16: table is likely offset 18 not 20, re-reading");
tableOff = 18;
readTable();
}
}
if (table.ch[0][table.entries - 1] < 30000) {
Log::logger->log(Log::ERR, "readVCGT16: table is malformed, last value of a gamma ramp can't be {}", table.ch[0][table.entries - 1]);
return std::unexpected("invalid table values");
}
Log::logger->log(Log::DEBUG, "readVCGT16: red channel: [{}, {}, ... {}, {}]", table.ch[0][0], table.ch[0][1], table.ch[0][table.entries - 2], table.ch[0][table.entries - 1]);
Log::logger->log(Log::DEBUG, "readVCGT16: green channel: [{}, {}, ... {}, {}]", table.ch[1][0], table.ch[1][1], table.ch[1][table.entries - 2], table.ch[1][table.entries - 1]);
Log::logger->log(Log::DEBUG, "readVCGT16: blue channel: [{}, {}, ... {}, {}]", table.ch[2][0], table.ch[2][1], table.ch[2][table.entries - 2], table.ch[2][table.entries - 1]);
return table;
}
struct CmsProfileDeleter {
void operator()(cmsHPROFILE p) const {
if (p)
cmsCloseProfile(p);
}
};
struct CmsTransformDeleter {
void operator()(cmsHTRANSFORM t) const {
if (t)
cmsDeleteTransform(t);
}
};
using UniqueProfile = std::unique_ptr<std::remove_pointer_t<cmsHPROFILE>, CmsProfileDeleter>;
using UniqueTransform = std::unique_ptr<std::remove_pointer_t<cmsHTRANSFORM>, CmsTransformDeleter>;
static UniqueProfile createLinearSRGBProfile() {
cmsCIExyYTRIPLE prim{};
// sRGB / Rec.709 primaries
prim.Red.x = 0.6400;
prim.Red.y = 0.3300;
prim.Red.Y = 1.0;
prim.Green.x = 0.3000;
prim.Green.y = 0.6000;
prim.Green.Y = 1.0;
prim.Blue.x = 0.1500;
prim.Blue.y = 0.0600;
prim.Blue.Y = 1.0;
cmsCIExyY wp{};
wp.x = 0.3127;
wp.y = 0.3290;
wp.Y = 1.0; // D65
cmsToneCurve* lin = cmsBuildGamma(nullptr, 1.0);
cmsToneCurve* curves[3] = {lin, lin, lin};
cmsHPROFILE p = cmsCreateRGBProfile(&wp, &prim, curves);
cmsFreeToneCurve(lin);
return UniqueProfile{p};
}
static std::expected<void, std::string> buildIcc3DLut(cmsHPROFILE profile, SImageDescription& image) {
UniqueProfile src = createLinearSRGBProfile();
if (!src)
return std::unexpected("Failed to create linear sRGB profile");
// Rendering intent: RELATIVE_COLORIMETRIC is common for displays; add BPC to be safe.
const int intent = INTENT_RELATIVE_COLORIMETRIC;
const cmsUInt32Number flags = cmsFLAGS_BLACKPOINTCOMPENSATION | cmsFLAGS_HIGHRESPRECALC; // good quality precalc in LCMS
// float->float transform (linear input, encoded output in dst device space)
UniqueTransform xform{cmsCreateTransform(src.get(), TYPE_RGB_FLT, profile, TYPE_RGB_FLT, intent, flags)};
if (!xform)
return std::unexpected("Failed to create ICC transform");
Log::logger->log(Log::DEBUG, "Building a {}³ 3D LUT", image.icc.lutSize);
image.icc.present = true;
image.icc.lutDataPacked.resize(image.icc.lutSize * image.icc.lutSize * image.icc.lutSize * 3);
auto idx = [&image](int r, int g, int b) -> size_t {
//
return ((size_t)b * image.icc.lutSize * image.icc.lutSize + (size_t)g * image.icc.lutSize + (size_t)r) * 3;
};
for (size_t bz = 0; bz < image.icc.lutSize; ++bz) {
for (size_t gy = 0; gy < image.icc.lutSize; ++gy) {
for (size_t rx = 0; rx < image.icc.lutSize; ++rx) {
float in[3] = {
rx / float(image.icc.lutSize - 1),
gy / float(image.icc.lutSize - 1),
bz / float(image.icc.lutSize - 1),
};
float outRGB[3];
cmsDoTransform(xform.get(), in, outRGB, 1);
outRGB[0] = std::clamp(outRGB[0], 0.F, 1.F);
outRGB[1] = std::clamp(outRGB[1], 0.F, 1.F);
outRGB[2] = std::clamp(outRGB[2], 0.F, 1.F);
const size_t o = idx(rx, gy, bz);
image.icc.lutDataPacked[o + 0] = outRGB[0];
image.icc.lutDataPacked[o + 1] = outRGB[1];
image.icc.lutDataPacked[o + 2] = outRGB[2];
}
}
}
Log::logger->log(Log::DEBUG, "3D LUT constructed, size {}", image.icc.lutDataPacked.size());
// upload
image.icc.lutTexture = g_pHyprRenderer->createTexture(image.icc.lutDataPacked, image.icc.lutSize);
return {};
}
std::expected<SImageDescription, std::string> SImageDescription::fromICC(const std::filesystem::path& file) {
static auto PVCGTENABLED = CConfigValue<Hyprlang::INT>("render:icc_vcgt_enabled");
std::error_code ec;
if (!std::filesystem::exists(file, ec) || ec)
return std::unexpected("Invalid file");
SImageDescription image;
image.rawICC = readBinary(file);
if (image.rawICC.empty())
return std::unexpected("Failed to read file");
cmsHPROFILE prof = cmsOpenProfileFromFile(file.string().c_str(), "r");
if (!prof)
return std::unexpected("CMS failed to open icc file");
CScopeGuard x([&prof] { cmsCloseProfile(prof); });
// only handle RGB (typical display profiles)
if (cmsGetColorSpace(prof) != cmsSigRgbData)
return std::unexpected("Only RGB display profiles are supported");
Log::logger->log(Log::DEBUG, "============= Begin ICC load =============");
Log::logger->log(Log::DEBUG, "ICC size: {} bytes", image.rawICC.size());
if (const auto RET = buildIcc3DLut(prof, image); !RET)
return std::unexpected(RET.error());
if (*PVCGTENABLED) {
auto vcgtRes = readVCGT16(prof);
if (!vcgtRes)
return std::unexpected(vcgtRes.error());
image.icc.vcgt = *vcgtRes;
if (!*vcgtRes)
Log::logger->log(Log::DEBUG, "ICC profile has no VCGT data");
} else
Log::logger->log(Log::DEBUG, "Skipping VCGT load, disabled by config");
Log::logger->log(Log::DEBUG, "============= End ICC load =============");
return image;
}

View file

@ -30,8 +30,6 @@ CHyprError::CHyprError() {
if (m_fadeOpacity->isBeingAnimated() || m_monitorChanged) if (m_fadeOpacity->isBeingAnimated() || m_monitorChanged)
g_pHyprRenderer->damageBox(m_damageBox); g_pHyprRenderer->damageBox(m_damageBox);
}); });
m_texture = makeShared<CTexture>();
} }
void CHyprError::queueCreate(std::string message, const CHyprColor& color) { void CHyprError::queueCreate(std::string message, const CHyprColor& color) {
@ -40,8 +38,8 @@ void CHyprError::queueCreate(std::string message, const CHyprColor& color) {
} }
void CHyprError::createQueued() { void CHyprError::createQueued() {
if (m_isCreated) if (m_isCreated && m_texture)
m_texture->destroyTexture(); m_texture.reset();
m_fadeOpacity->setConfig(g_pConfigManager->getAnimationPropertyConfig("fadeIn")); m_fadeOpacity->setConfig(g_pConfigManager->getAnimationPropertyConfig("fadeIn"));
@ -145,12 +143,13 @@ void CHyprError::createQueued() {
// copy the data to an OpenGL texture we have // copy the data to an OpenGL texture we have
const auto DATA = cairo_image_surface_get_data(CAIROSURFACE); const auto DATA = cairo_image_surface_get_data(CAIROSURFACE);
m_texture->allocate(); auto tex = texture();
m_texture->bind(); tex->allocate(PMONITOR->m_pixelSize);
m_texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST); tex->bind();
m_texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST); tex->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_R, GL_BLUE); tex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_B, GL_RED); tex->setTexParameter(GL_TEXTURE_SWIZZLE_R, GL_BLUE);
tex->setTexParameter(GL_TEXTURE_SWIZZLE_B, GL_RED);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
@ -187,7 +186,8 @@ void CHyprError::draw() {
if (!m_fadeOpacity->isBeingAnimated()) { if (!m_fadeOpacity->isBeingAnimated()) {
if (m_fadeOpacity->value() == 0.f) { if (m_fadeOpacity->value() == 0.f) {
m_queuedDestroy = false; m_queuedDestroy = false;
m_texture->destroyTexture(); if (m_texture)
m_texture.reset();
m_isCreated = false; m_isCreated = false;
m_queued = ""; m_queued = "";
@ -218,7 +218,7 @@ void CHyprError::draw() {
m_monitorChanged = false; m_monitorChanged = false;
CTexPassElement::SRenderData data; CTexPassElement::SRenderData data;
data.tex = m_texture; data.tex = texture();
data.box = monbox; data.box = monbox;
data.a = m_fadeOpacity->value(); data.a = m_fadeOpacity->value();
@ -239,3 +239,9 @@ bool CHyprError::active() {
float CHyprError::height() { float CHyprError::height() {
return m_lastHeight; return m_lastHeight;
} }
SP<ITexture> CHyprError::texture() {
if (!m_texture)
m_texture = g_pHyprRenderer->createTexture();
return m_texture;
}

View file

@ -18,13 +18,16 @@ class CHyprError {
bool active(); bool active();
float height(); // logical float height(); // logical
//
SP<ITexture> texture();
private: private:
void createQueued(); void createQueued();
std::string m_queued = ""; std::string m_queued = "";
CHyprColor m_queuedColor; CHyprColor m_queuedColor;
bool m_queuedDestroy = false; bool m_queuedDestroy = false;
bool m_isCreated = false; bool m_isCreated = false;
SP<CTexture> m_texture; SP<ITexture> m_texture;
PHLANIMVAR<float> m_fadeOpacity; PHLANIMVAR<float> m_fadeOpacity;
CBox m_damageBox = {0, 0, 0, 0}; CBox m_damageBox = {0, 0, 0, 0};
float m_lastHeight = 0.F; float m_lastHeight = 0.F;

View file

@ -1605,6 +1605,7 @@ I18n::CI18nEngine::CI18nEngine() {
huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_UNKNOWN, "Ứng dụng <b>{app}</b> đang yêu cầu một quyền không xác định."); huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_UNKNOWN, "Ứng dụng <b>{app}</b> đang yêu cầu một quyền không xác định.");
huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_SCREENCOPY, "Ứng dụng <b>{app}</b> đang cố gắng ghi hình màn hình của bạn.\n\nBạn muốn cho phép không?"); huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_SCREENCOPY, "Ứng dụng <b>{app}</b> đang cố gắng ghi hình màn hình của bạn.\n\nBạn muốn cho phép không?");
huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_CURSOR_POS, "Ứng dụng <b>{app}</b> đang cố gắng đọc vị trí chuột của bạn.\n\nBạn muốn cho phép không?");
huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_PLUGIN, "Ứng dụng <b>{app}</b> đang cố gắng tải plugin: <b>{plugin}</b>.\n\nBạn muốn cho phép không?"); huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_PLUGIN, "Ứng dụng <b>{app}</b> đang cố gắng tải plugin: <b>{plugin}</b>.\n\nBạn muốn cho phép không?");
huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_KEYBOARD, "Phát hiện bàn phím mới: <b>{keyboard}</b>.\n\nBạn muốn cho phép bàn phím này hoạt động không?"); huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_REQUEST_KEYBOARD, "Phát hiện bàn phím mới: <b>{keyboard}</b>.\n\nBạn muốn cho phép bàn phím này hoạt động không?");
huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_UNKNOWN_NAME, "(không xác định)"); huEngine->registerEntry("vi_VN", TXT_KEY_PERMISSION_UNKNOWN_NAME, "(không xác định)");

View file

@ -11,13 +11,7 @@
using namespace Layout; using namespace Layout;
CLayoutManager::CLayoutManager() { CLayoutManager::CLayoutManager() = default;
static auto P = Event::bus()->m_events.monitor.layoutChanged.listen([] {
for (const auto& ws : g_pCompositor->getWorkspaces()) {
ws->m_space->recheckWorkArea();
}
});
}
void CLayoutManager::newTarget(SP<ITarget> target, SP<CSpace> space) { void CLayoutManager::newTarget(SP<ITarget> target, SP<CSpace> space) {
// on a new target: remember desired pos for float, if available // on a new target: remember desired pos for float, if available
@ -67,6 +61,13 @@ void CLayoutManager::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectC
target->space()->resizeTarget(Δ, target, corner); target->space()->resizeTarget(Δ, target, corner);
} }
void CLayoutManager::setTargetGeom(const CBox& box, SP<ITarget> target) {
if (!target->floating())
return;
target->space()->setTargetGeom(box, target);
}
std::expected<void, std::string> CLayoutManager::layoutMsg(const std::string_view& sv) { std::expected<void, std::string> CLayoutManager::layoutMsg(const std::string_view& sv) {
const auto MONITOR = Desktop::focusState()->monitor(); const auto MONITOR = Desktop::focusState()->monitor();

View file

@ -53,6 +53,7 @@ namespace Layout {
void moveMouse(const Vector2D& mousePos); void moveMouse(const Vector2D& mousePos);
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE); void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
void moveTarget(const Vector2D& Δ, SP<ITarget> target); void moveTarget(const Vector2D& Δ, SP<ITarget> target);
void setTargetGeom(const CBox& box, SP<ITarget> target); // floats only
void endDragTarget(); void endDragTarget();
std::expected<void, std::string> layoutMsg(const std::string_view& sv); std::expected<void, std::string> layoutMsg(const std::string_view& sv);

View file

@ -42,16 +42,16 @@ void CAlgorithm::removeTarget(SP<ITarget> target) {
const bool IS_FLOATING = std::ranges::contains(m_floatingTargets, target); const bool IS_FLOATING = std::ranges::contains(m_floatingTargets, target);
if (IS_FLOATING) { if (IS_FLOATING) {
m_floating->removeTarget(target);
std::erase(m_floatingTargets, target); std::erase(m_floatingTargets, target);
m_floating->removeTarget(target);
return; return;
} }
const bool IS_TILED = std::ranges::contains(m_tiledTargets, target); const bool IS_TILED = std::ranges::contains(m_tiledTargets, target);
if (IS_TILED) { if (IS_TILED) {
m_tiled->removeTarget(target);
std::erase(m_tiledTargets, target); std::erase(m_tiledTargets, target);
m_tiled->removeTarget(target);
return; return;
} }
@ -262,3 +262,10 @@ SP<ITarget> CAlgorithm::getNextCandidate(SP<ITarget> old) {
// god damn it, maybe empty? // god damn it, maybe empty?
return nullptr; return nullptr;
} }
void CAlgorithm::setTargetGeom(const CBox& box, SP<ITarget> target) {
if (!target->floating() || !std::ranges::contains(m_floatingTargets, target))
return;
m_floating->setTargetGeom(box, target);
}

View file

@ -40,6 +40,8 @@ namespace Layout {
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE); void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
void moveTarget(const Vector2D& Δ, SP<ITarget> target); void moveTarget(const Vector2D& Δ, SP<ITarget> target);
void setTargetGeom(const CBox& box, SP<ITarget> target); // only for float
void updateFloatingAlgo(UP<IFloatingAlgorithm>&& algo); void updateFloatingAlgo(UP<IFloatingAlgorithm>&& algo);
void updateTiledAlgo(UP<ITiledAlgorithm>&& algo); void updateTiledAlgo(UP<ITiledAlgorithm>&& algo);

View file

@ -17,6 +17,9 @@ namespace Layout {
// a target is being moved by a delta // a target is being moved by a delta
virtual void moveTarget(const Vector2D& Δ, SP<ITarget> target) = 0; virtual void moveTarget(const Vector2D& Δ, SP<ITarget> target) = 0;
// a target is moved to a pos x size
virtual void setTargetGeom(const CBox& geom, SP<ITarget> target) = 0;
virtual void recenter(SP<ITarget> t); virtual void recenter(SP<ITarget> t);
virtual void recalculate(); virtual void recalculate();

View file

@ -1,5 +1,10 @@
#include "ModeAlgorithm.hpp" #include "ModeAlgorithm.hpp"
#include "../space/Space.hpp"
#include "Algorithm.hpp"
#include "../../helpers/Monitor.hpp"
#include "../../desktop/view/Window.hpp"
using namespace Layout; using namespace Layout;
std::expected<void, std::string> IModeAlgorithm::layoutMsg(const std::string_view& sv) { std::expected<void, std::string> IModeAlgorithm::layoutMsg(const std::string_view& sv) {
@ -9,3 +14,20 @@ std::expected<void, std::string> IModeAlgorithm::layoutMsg(const std::string_vie
std::optional<Vector2D> IModeAlgorithm::predictSizeForNewTarget() { std::optional<Vector2D> IModeAlgorithm::predictSizeForNewTarget() {
return std::nullopt; return std::nullopt;
} }
std::optional<Vector2D> IModeAlgorithm::focalPointForDir(SP<ITarget> t, Math::eDirection dir) {
Vector2D focalPoint;
const auto WINDOWIDEALBB =
t->fullscreenMode() != FSMODE_NONE ? m_parent->space()->workspace()->m_monitor->logicalBox() : t->window()->getWindowIdealBoundingBoxIgnoreReserved();
switch (dir) {
case Math::DIRECTION_UP: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, -1.0}; break;
case Math::DIRECTION_DOWN: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, WINDOWIDEALBB.size().y + 1.0}; break;
case Math::DIRECTION_LEFT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{-1.0, WINDOWIDEALBB.size().y / 2.0}; break;
case Math::DIRECTION_RIGHT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x + 1.0, WINDOWIDEALBB.size().y / 2.0}; break;
default: return std::nullopt;
}
return focalPoint;
}

View file

@ -44,6 +44,9 @@ namespace Layout {
// optional: predict new window's size // optional: predict new window's size
virtual std::optional<Vector2D> predictSizeForNewTarget(); virtual std::optional<Vector2D> predictSizeForNewTarget();
// Impl'd here: focal point for dir
virtual std::optional<Vector2D> focalPointForDir(SP<ITarget> t, Math::eDirection dir);
protected: protected:
IModeAlgorithm() = default; IModeAlgorithm() = default;

View file

@ -116,6 +116,8 @@ void CDefaultFloatingAlgorithm::newTarget(SP<ITarget> target) {
PWINDOW->m_reportedSize = PWINDOW->m_pendingReportedSize; PWINDOW->m_reportedSize = PWINDOW->m_pendingReportedSize;
} }
} }
updateTarget(target);
} }
void CDefaultFloatingAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) { void CDefaultFloatingAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
@ -152,6 +154,8 @@ void CDefaultFloatingAlgorithm::movedTarget(SP<ITarget> target, std::optional<Ve
// put around the current center, fit in workArea // put around the current center, fit in workArea
target->setPositionGlobal(fitBoxInWorkArea(CBox{NEW_POS, LAST_SIZE}, target)); target->setPositionGlobal(fitBoxInWorkArea(CBox{NEW_POS, LAST_SIZE}, target));
} }
updateTarget(target);
} }
CBox CDefaultFloatingAlgorithm::fitBoxInWorkArea(const CBox& box, SP<ITarget> t) { CBox CDefaultFloatingAlgorithm::fitBoxInWorkArea(const CBox& box, SP<ITarget> t) {
@ -173,6 +177,7 @@ CBox CDefaultFloatingAlgorithm::fitBoxInWorkArea(const CBox& box, SP<ITarget> t)
void CDefaultFloatingAlgorithm::removeTarget(SP<ITarget> target) { void CDefaultFloatingAlgorithm::removeTarget(SP<ITarget> target) {
target->rememberFloatingSize(target->position().size()); target->rememberFloatingSize(target->position().size());
m_datas.erase(target);
} }
void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) { void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
@ -184,6 +189,8 @@ void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> tar
if (g_layoutManager->dragController()->target() == target) if (g_layoutManager->dragController()->target() == target)
target->warpPositionSize(); target->warpPositionSize();
updateTarget(target);
} }
void CDefaultFloatingAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> target) { void CDefaultFloatingAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> target) {
@ -193,12 +200,17 @@ void CDefaultFloatingAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> targe
if (g_layoutManager->dragController()->target() == target) if (g_layoutManager->dragController()->target() == target)
target->warpPositionSize(); target->warpPositionSize();
updateTarget(target);
} }
void CDefaultFloatingAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) { void CDefaultFloatingAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
auto posABackup = a->position(); auto posABackup = a->position();
a->setPositionGlobal(b->position()); a->setPositionGlobal(b->position());
b->setPositionGlobal(posABackup); b->setPositionGlobal(posABackup);
updateTarget(a);
updateTarget(b);
} }
void CDefaultFloatingAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) { void CDefaultFloatingAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
@ -216,4 +228,25 @@ void CDefaultFloatingAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDire
} }
t->setPositionGlobal(pos); t->setPositionGlobal(pos);
updateTarget(t);
}
void CDefaultFloatingAlgorithm::recenter(SP<ITarget> t) {
if (!m_datas.contains(t)) {
IFloatingAlgorithm::recenter(t);
return;
}
t->setPositionGlobal(m_datas.at(t).lastBox);
}
void CDefaultFloatingAlgorithm::setTargetGeom(const CBox& geom, SP<ITarget> target) {
target->setPositionGlobal(geom);
updateTarget(target);
}
void CDefaultFloatingAlgorithm::updateTarget(SP<ITarget> t) {
m_datas[t] = {.lastBox = t->position()};
} }

View file

@ -1,5 +1,7 @@
#include "../../FloatingAlgorithm.hpp" #include "../../FloatingAlgorithm.hpp"
#include <map>
namespace Layout { namespace Layout {
class CAlgorithm; class CAlgorithm;
} }
@ -17,10 +19,22 @@ namespace Layout::Floating {
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE); virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void moveTarget(const Vector2D& Δ, SP<ITarget> target); virtual void moveTarget(const Vector2D& Δ, SP<ITarget> target);
virtual void setTargetGeom(const CBox& geom, SP<ITarget> target);
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b); virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent); virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
virtual void recenter(SP<ITarget> t);
private: private:
CBox fitBoxInWorkArea(const CBox& box, SP<ITarget> t); CBox fitBoxInWorkArea(const CBox& box, SP<ITarget> t);
void updateTarget(SP<ITarget>);
struct SWindowData {
CBox lastBox;
};
std::map<WP<ITarget>, SWindowData> m_datas;
}; };
}; };

View file

@ -99,10 +99,12 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, ACTIVE_MON)) if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, ACTIVE_MON))
OPENINGON = getClosestNode(MOUSECOORDS); OPENINGON = getClosestNode(MOUSECOORDS);
} else if (*PUSEACTIVE) { } else if (*PUSEACTIVE || m_overrideFocalPoint) {
const auto ACTIVE_WINDOW = Desktop::focusState()->window(); const auto ACTIVE_WINDOW = Desktop::focusState()->window();
if (!m_overrideFocalPoint && ACTIVE_WINDOW && !ACTIVE_WINDOW->m_isFloating && ACTIVE_WINDOW != target->window() && ACTIVE_WINDOW->m_workspace == PWORKSPACE && if (m_overrideFocalPoint)
OPENINGON = getClosestNode(*m_overrideFocalPoint);
else if (!m_overrideFocalPoint && ACTIVE_WINDOW && !ACTIVE_WINDOW->m_isFloating && ACTIVE_WINDOW != target->window() && ACTIVE_WINDOW->m_workspace == PWORKSPACE &&
ACTIVE_WINDOW->m_isMapped) ACTIVE_WINDOW->m_isMapped)
OPENINGON = getNodeFromWindow(ACTIVE_WINDOW); OPENINGON = getNodeFromWindow(ACTIVE_WINDOW);
@ -182,7 +184,7 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
// whether or not the override persists after opening one window // whether or not the override persists after opening one window
if (*PERMANENTDIRECTIONOVERRIDE == 0) if (*PERMANENTDIRECTIONOVERRIDE == 0)
m_overrideDirection = Math::DIRECTION_DEFAULT; m_overrideDirection = Math::DIRECTION_DEFAULT;
} else if (*PSMARTSPLIT == 1) { } else if (*PSMARTSPLIT == 1 || m_overrideFocalPoint) {
const auto PARENT_CENTER = NEWPARENT->box.pos() + NEWPARENT->box.size() / 2; const auto PARENT_CENTER = NEWPARENT->box.pos() + NEWPARENT->box.size() / 2;
const auto PARENT_PROPORTIONS = NEWPARENT->box.h / NEWPARENT->box.w; const auto PARENT_PROPORTIONS = NEWPARENT->box.h / NEWPARENT->box.w;
const auto DELTA = MOUSECOORDS - PARENT_CENTER; const auto DELTA = MOUSECOORDS - PARENT_CENTER;
@ -214,10 +216,7 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
} }
} }
} else if (*PFORCESPLIT == 0 || !newTarget) { } else if (*PFORCESPLIT == 0 || !newTarget) {
if ((SIDEBYSIDE && if ((SIDEBYSIDE && MOUSECOORDS.x < NEWPARENT->box.x + (NEWPARENT->box.w / 2.F)) || (!SIDEBYSIDE && MOUSECOORDS.y < NEWPARENT->box.y + (NEWPARENT->box.h / 2.F))) {
VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w / 2.f, NEWPARENT->box.y + NEWPARENT->box.h)) ||
(!SIDEBYSIDE &&
VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w, NEWPARENT->box.y + NEWPARENT->box.h / 2.f))) {
// we are hovering over the first node, make PNODE first. // we are hovering over the first node, make PNODE first.
NEWPARENT->children[1] = OPENINGON; NEWPARENT->children[1] = OPENINGON;
NEWPARENT->children[0] = PNODE; NEWPARENT->children[0] = PNODE;
@ -242,12 +241,11 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
// and update the previous parent if it exists // and update the previous parent if it exists
if (OPENINGON->pParent) { if (OPENINGON->pParent) {
if (OPENINGON->pParent->children[0] == OPENINGON) { if (OPENINGON->pParent->children[0] == OPENINGON)
OPENINGON->pParent->children[0] = NEWPARENT; OPENINGON->pParent->children[0] = NEWPARENT;
} else { else
OPENINGON->pParent->children[1] = NEWPARENT; OPENINGON->pParent->children[1] = NEWPARENT;
} }
}
// Update the children // Update the children
if (!verticalOverride && (NEWPARENT->box.w * *PWIDTHMULTIPLIER > NEWPARENT->box.h || horizontalOverride)) { if (!verticalOverride && (NEWPARENT->box.w * *PWIDTHMULTIPLIER > NEWPARENT->box.h || horizontalOverride)) {
@ -551,41 +549,35 @@ std::optional<Vector2D> CDwindleAlgorithm::predictSizeForNewTarget() {
} }
void CDwindleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) { void CDwindleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
const auto PNODE = getNodeFromTarget(t); const auto PNODE = getNodeFromTarget(t);
const Vector2D originalPos = t->position().middle(); const Vector2D originalPos = t->position().middle();
if (!PNODE || !t->window()) if (!PNODE || !t->window())
return; return;
Vector2D focalPoint; const auto FOCAL_POINT = focalPointForDir(t, dir);
const auto WINDOWIDEALBB = const auto PMONITORFOCAL = g_pCompositor->getMonitorFromVector(FOCAL_POINT.value_or(t->position().middle()));
t->fullscreenMode() != FSMODE_NONE ? m_parent->space()->workspace()->m_monitor->logicalBox() : t->window()->getWindowIdealBoundingBoxIgnoreReserved();
switch (dir) { if (PMONITORFOCAL != m_parent->space()->workspace()->m_monitor && !*PMONITORFALLBACK)
case Math::DIRECTION_UP: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, -1.0}; break; return; // noop
case Math::DIRECTION_DOWN: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, WINDOWIDEALBB.size().y + 1.0}; break;
case Math::DIRECTION_LEFT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{-1.0, WINDOWIDEALBB.size().y / 2.0}; break;
case Math::DIRECTION_RIGHT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x + 1.0, WINDOWIDEALBB.size().y / 2.0}; break;
default: return;
}
t->window()->setAnimationsToMove(); t->window()->setAnimationsToMove();
removeTarget(t); removeTarget(t);
const auto PMONITORFOCAL = g_pCompositor->getMonitorFromVector(focalPoint);
if (PMONITORFOCAL != m_parent->space()->workspace()->m_monitor) { if (PMONITORFOCAL != m_parent->space()->workspace()->m_monitor) {
// move with a focal point // move with a focal point
if (PMONITORFOCAL->m_activeWorkspace) if (PMONITORFOCAL->m_activeWorkspace)
t->assignToSpace(PMONITORFOCAL->m_activeWorkspace->m_space); t->assignToSpace(PMONITORFOCAL->m_activeWorkspace->m_space, FOCAL_POINT);
return; return;
} }
movedTarget(t, focalPoint); movedTarget(t, FOCAL_POINT);
// restore focus to the previous position // restore focus to the previous position
if (silent) { if (silent) {
@ -665,11 +657,28 @@ std::expected<void, std::string> CDwindleAlgorithm::layoutMsg(const std::string_
const auto CURRENT_NODE = getNodeFromWindow(Desktop::focusState()->window()); const auto CURRENT_NODE = getNodeFromWindow(Desktop::focusState()->window());
if (ARGS[0] == "togglesplit") { if (ARGS[0] == "togglesplit") {
if (CURRENT_NODE) if (CURRENT_NODE) {
toggleSplit(CURRENT_NODE); if (!toggleSplit(CURRENT_NODE))
return std::unexpected("can't togglesplit in the current workspace");
}
} else if (ARGS[0] == "swapsplit") { } else if (ARGS[0] == "swapsplit") {
if (CURRENT_NODE) if (CURRENT_NODE) {
swapSplit(CURRENT_NODE); if (!swapSplit(CURRENT_NODE))
return std::unexpected("can't swapsplit in the current workspace");
}
} else if (ARGS[0] == "rotatesplit") {
if (CURRENT_NODE) {
int angle = 90;
if (!ARGS[1].empty()) {
try {
angle = std::stoi(std::string{ARGS[1]});
} catch (const std::exception& e) {
Log::logger->log(Log::WARN, "Invalid angle argument for rotatesplit: {}", ARGS[1]);
return std::unexpected("Invalid angle argument");
}
}
rotateSplit(CURRENT_NODE, angle);
}
} else if (ARGS[0] == "movetoroot") { } else if (ARGS[0] == "movetoroot") {
auto node = CURRENT_NODE; auto node = CURRENT_NODE;
if (!ARGS[1].empty()) { if (!ARGS[1].empty()) {
@ -679,7 +688,8 @@ std::expected<void, std::string> CDwindleAlgorithm::layoutMsg(const std::string_
} }
const auto STABLE = ARGS[2].empty() || ARGS[2] != "unstable"; const auto STABLE = ARGS[2].empty() || ARGS[2] != "unstable";
moveToRoot(node, STABLE); if (!moveToRoot(node, STABLE))
return std::unexpected("can't movetoroot in the current workspace");
} else if (ARGS[0] == "preselect") { } else if (ARGS[0] == "preselect") {
auto direction = ARGS[1]; auto direction = ARGS[1];
@ -714,42 +724,102 @@ std::expected<void, std::string> CDwindleAlgorithm::layoutMsg(const std::string_
break; break;
} }
} }
} else if (ARGS[0] == "splitratio") {
auto ratio = ARGS[1];
bool exact = ARGS[2].starts_with("exact");
if (ratio.empty())
return std::unexpected("splitratio requires an arg");
auto delta = getPlusMinusKeywordResult(std::string{ratio}, 0.F);
if (!CURRENT_NODE || !CURRENT_NODE->pParent)
return std::unexpected("cannot alter split ratio on no / single node");
if (!delta)
return std::unexpected(std::format("failed to parse \"{}\" as a delta", ratio));
const float newRatio = exact ? *delta : CURRENT_NODE->pParent->splitRatio + *delta;
CURRENT_NODE->pParent->splitRatio = std::clamp(newRatio, 0.1F, 1.9F);
CURRENT_NODE->pParent->recalcSizePosRecursive();
} }
return {}; return {};
} }
void CDwindleAlgorithm::toggleSplit(SP<SDwindleNodeData> x) { bool CDwindleAlgorithm::toggleSplit(SP<SDwindleNodeData> x) {
if (!x || !x->pParent) if (!x || !x->pParent)
return; return false;
if (x->pTarget->fullscreenMode() != FSMODE_NONE) if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return; return false;
x->pParent->splitTop = !x->pParent->splitTop; x->pParent->splitTop = !x->pParent->splitTop;
x->pParent->recalcSizePosRecursive(); x->pParent->recalcSizePosRecursive();
return true;
} }
void CDwindleAlgorithm::swapSplit(SP<SDwindleNodeData> x) { bool CDwindleAlgorithm::swapSplit(SP<SDwindleNodeData> x) {
if (x->pTarget->fullscreenMode() != FSMODE_NONE) if (x->pTarget->fullscreenMode() != FSMODE_NONE || !x->pParent)
return; return false;
std::swap(x->pParent->children[0], x->pParent->children[1]); std::swap(x->pParent->children[0], x->pParent->children[1]);
x->pParent->recalcSizePosRecursive(); x->pParent->recalcSizePosRecursive();
return true;
} }
void CDwindleAlgorithm::moveToRoot(SP<SDwindleNodeData> x, bool stable) { void CDwindleAlgorithm::rotateSplit(SP<SDwindleNodeData> x, int angle) {
if (!x || !x->pParent) if (!x || !x->pParent)
return; return;
if (x->pTarget->fullscreenMode() != FSMODE_NONE) if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return; return;
// normalize the angle to multiples of 90 degrees
int normalizedAngle = ((sc<int>(angle / 90) % 4) + 4) % 4; // ensures positive modulo
auto pParent = x->pParent;
bool shouldSwap = false;
switch (normalizedAngle) {
case 0: // 0 degrees - no change
break;
case 1:
if (pParent->splitTop)
shouldSwap = true;
pParent->splitTop = !pParent->splitTop;
break;
case 2: shouldSwap = true; break;
case 3:
if (!pParent->splitTop)
shouldSwap = true;
pParent->splitTop = !pParent->splitTop;
break;
default: break; // should never happen
}
if (shouldSwap)
std::swap(pParent->children[0], pParent->children[1]);
pParent->recalcSizePosRecursive();
}
bool CDwindleAlgorithm::moveToRoot(SP<SDwindleNodeData> x, bool stable) {
if (!x || !x->pParent)
return false;
if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return false;
// already at root // already at root
if (!x->pParent->pParent) if (!x->pParent->pParent)
return; return false;
auto& pNode = x->pParent->children[0] == x ? x->pParent->children[0] : x->pParent->children[1]; auto& pNode = x->pParent->children[0] == x ? x->pParent->children[0] : x->pParent->children[1];
@ -770,4 +840,6 @@ void CDwindleAlgorithm::moveToRoot(SP<SDwindleNodeData> x, bool stable) {
std::swap(pRoot->children[0], pRoot->children[1]); std::swap(pRoot->children[0], pRoot->children[1]);
pRoot->recalcSizePosRecursive(); pRoot->recalcSizePosRecursive();
return true;
} }

View file

@ -48,9 +48,10 @@ namespace Layout::Tiled {
SP<SDwindleNodeData> getClosestNode(const Vector2D&, SP<ITarget> skip = nullptr); SP<SDwindleNodeData> getClosestNode(const Vector2D&, SP<ITarget> skip = nullptr);
SP<SDwindleNodeData> getMasterNode(); SP<SDwindleNodeData> getMasterNode();
void toggleSplit(SP<SDwindleNodeData>); bool toggleSplit(SP<SDwindleNodeData>);
void swapSplit(SP<SDwindleNodeData>); bool swapSplit(SP<SDwindleNodeData>);
void moveToRoot(SP<SDwindleNodeData>, bool stable = true); void rotateSplit(SP<SDwindleNodeData>, int angle = 90);
bool moveToRoot(SP<SDwindleNodeData>, bool stable = true);
Math::eDirection m_overrideDirection = Math::DIRECTION_DEFAULT; Math::eDirection m_overrideDirection = Math::DIRECTION_DEFAULT;
}; };

View file

@ -403,6 +403,8 @@ void CMasterAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
} }
void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) { void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
const auto PWINDOW2 = g_pCompositor->getWindowInDirection(t->window(), dir); const auto PWINDOW2 = g_pCompositor->getWindowInDirection(t->window(), dir);
if (!t->window()) if (!t->window())
@ -424,7 +426,10 @@ void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir
t->window()->setAnimationsToMove(); t->window()->setAnimationsToMove();
if (t->window()->m_workspace != targetWs) { if (t->window()->m_workspace != targetWs) {
t->assignToSpace(targetWs->m_space); if (!*PMONITORFALLBACK)
return; // noop
t->assignToSpace(targetWs->m_space, focalPointForDir(t, dir));
} else if (PWINDOW2) { } else if (PWINDOW2) {
// if same monitor, switch windows // if same monitor, switch windows
g_layoutManager->switchTargets(t, PWINDOW2->layoutTarget()); g_layoutManager->switchTargets(t, PWINDOW2->layoutTarget());
@ -720,7 +725,7 @@ std::expected<void, std::string> CMasterAlgorithm::layoutMsg(const std::string_v
for (auto& nd : m_masterNodesData) { for (auto& nd : m_masterNodesData) {
if (!nd->isMaster) { if (!nd->isMaster) {
const auto newMaster = nd; const auto& newMaster = nd;
newMaster->isMaster = true; newMaster->isMaster = true;
auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster);
@ -756,7 +761,7 @@ std::expected<void, std::string> CMasterAlgorithm::layoutMsg(const std::string_v
for (auto& nd : m_masterNodesData | std::views::reverse) { for (auto& nd : m_masterNodesData | std::views::reverse) {
if (!nd->isMaster) { if (!nd->isMaster) {
const auto newMaster = nd; const auto& newMaster = nd;
newMaster->isMaster = true; newMaster->isMaster = true;
auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster);
@ -956,7 +961,9 @@ void CMasterAlgorithm::calculateWorkspace() {
const auto STACKWINDOWS = WINDOWS - MASTERS; const auto STACKWINDOWS = WINDOWS - MASTERS;
const auto WORKAREA = m_parent->space()->workArea(); const auto WORKAREA = m_parent->space()->workArea();
const auto PMONITOR = m_parent->space()->workspace()->m_monitor; const auto PMONITOR = m_parent->space()->workspace()->m_monitor;
const auto UNRESERVED_WIDTH = WORKAREA.width + PMONITOR->m_reservedArea.left() + PMONITOR->m_reservedArea.right(); const auto reservedLeft = PMONITOR ? PMONITOR->m_reservedArea.left() : 0;
const auto reservedRight = PMONITOR ? PMONITOR->m_reservedArea.right() : 0;
const auto UNRESERVED_WIDTH = WORKAREA.width + reservedLeft + reservedRight;
if (orientation == ORIENTATION_CENTER) { if (orientation == ORIENTATION_CENTER) {
if (STACKWINDOWS >= *SLAVECOUNTFORCENTER) if (STACKWINDOWS >= *SLAVECOUNTFORCENTER)
@ -1074,7 +1081,7 @@ void CMasterAlgorithm::calculateWorkspace() {
} }
nd->size = Vector2D(WIDTH, HEIGHT); nd->size = Vector2D(WIDTH, HEIGHT);
nd->position = (*PIGNORERESERVED && centerMasterWindow ? WORKAREA.pos() - Vector2D(PMONITOR->m_reservedArea.left(), 0.0) : WORKAREA.pos()) + Vector2D(nextX, nextY); nd->position = (*PIGNORERESERVED && centerMasterWindow ? WORKAREA.pos() - Vector2D(reservedLeft, 0.0) : WORKAREA.pos()) + Vector2D(nextX, nextY);
nd->pTarget->setPositionGlobal({nd->position, nd->size}); nd->pTarget->setPositionGlobal({nd->position, nd->size});
mastersLeft--; mastersLeft--;
@ -1192,7 +1199,7 @@ void CMasterAlgorithm::calculateWorkspace() {
continue; continue;
if (onRight) { if (onRight) {
nextX = WIDTH + PMASTERNODE->size.x - (*PIGNORERESERVED ? PMONITOR->m_reservedArea.left() : 0); nextX = WIDTH + PMASTERNODE->size.x - (*PIGNORERESERVED ? reservedLeft : 0);
nextY = nextYR; nextY = nextYR;
heightLeft = heightLeftR; heightLeft = heightLeftR;
slavesLeft = slavesLeftR; slavesLeft = slavesLeftR;
@ -1217,7 +1224,7 @@ void CMasterAlgorithm::calculateWorkspace() {
} }
} }
nd->size = Vector2D(*PIGNORERESERVED ? (WIDTH - (onRight ? PMONITOR->m_reservedArea.right() : PMONITOR->m_reservedArea.left())) : WIDTH, HEIGHT); nd->size = Vector2D(*PIGNORERESERVED ? (WIDTH - (onRight ? reservedRight : reservedLeft)) : WIDTH, HEIGHT);
nd->position = WORKAREA.pos() + Vector2D(nextX, nextY); nd->position = WORKAREA.pos() + Vector2D(nextX, nextY);
nd->pTarget->setPositionGlobal({nd->position, nd->size}); nd->pTarget->setPositionGlobal({nd->position, nd->size});

View file

@ -202,6 +202,11 @@ void CMonocleAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
} }
void CMonocleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) { void CMonocleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
if (!*PMONITORFALLBACK)
return; // noop
// try to find a monitor in the specified direction, thats the logical thing // try to find a monitor in the specified direction, thats the logical thing
if (!t || !t->space() || !t->space()->workspace()) if (!t || !t->space() || !t->space()->workspace())
return; return;
@ -215,7 +220,7 @@ void CMonocleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection di
if (t->window()) if (t->window())
t->window()->setAnimationsToMove(); t->window()->setAnimationsToMove();
t->assignToSpace(TARGETWS->m_space); t->assignToSpace(TARGETWS->m_space, focalPointForDir(t, dir));
} }
} }

View file

@ -55,12 +55,14 @@ size_t CScrollTapeController::addStrip(float size) {
return m_strips.size() - 1; return m_strips.size() - 1;
} }
void CScrollTapeController::insertStrip(size_t afterIndex, float size) { void CScrollTapeController::insertStrip(ssize_t afterIndex, float size) {
if (afterIndex >= m_strips.size()) { if (afterIndex >= sc<ssize_t>(m_strips.size())) {
addStrip(size); addStrip(size);
return; return;
} }
afterIndex = std::clamp(afterIndex, sc<ssize_t>(-1L), sc<ssize_t>(INT32_MAX));
SStripData newStrip; SStripData newStrip;
newStrip.size = size; newStrip.size = size;
m_strips.insert(m_strips.begin() + afterIndex + 1, newStrip); m_strips.insert(m_strips.begin() + afterIndex + 1, newStrip);

View file

@ -40,7 +40,7 @@ namespace Layout::Tiled {
bool isReversed() const; bool isReversed() const;
size_t addStrip(float size = 1.0F); size_t addStrip(float size = 1.0F);
void insertStrip(size_t afterIndex, float size = 1.0F); void insertStrip(ssize_t afterIndex, float size = 1.0F);
void removeStrip(size_t index); void removeStrip(size_t index);
size_t stripCount() const; size_t stripCount() const;
SStripData& getStrip(size_t index); SStripData& getStrip(size_t index);

View file

@ -190,10 +190,12 @@ size_t SColumnData::idx(SP<ITarget> t) {
} }
size_t SColumnData::idxForHeight(float y) { size_t SColumnData::idxForHeight(float y) {
if (targetDatas.empty())
return 0;
for (size_t i = 0; i < targetDatas.size(); ++i) { for (size_t i = 0; i < targetDatas.size(); ++i) {
if (targetDatas[i]->target->position().y < y) if (targetDatas[i]->target->position().y < y)
continue; continue;
return i - 1; return i == 0 ? 0 : i - 1;
} }
return targetDatas.size() - 1; return targetDatas.size() - 1;
} }
@ -245,24 +247,28 @@ void SColumnData::remove(SP<ITarget> t) {
scrollingData->remove(self.lock()); scrollingData->remove(self.lock());
} }
void SColumnData::up(SP<SScrollingTargetData> w) { bool SColumnData::up(SP<SScrollingTargetData> w) {
for (size_t i = 1; i < targetDatas.size(); ++i) { for (size_t i = 1; i < targetDatas.size(); ++i) {
if (targetDatas[i] != w) if (targetDatas[i] != w)
continue; continue;
std::swap(targetDatas[i], targetDatas[i - 1]); std::swap(targetDatas[i], targetDatas[i - 1]);
break; return true;
}
} }
void SColumnData::down(SP<SScrollingTargetData> w) { return false;
}
bool SColumnData::down(SP<SScrollingTargetData> w) {
for (size_t i = 0; i < targetDatas.size() - 1; ++i) { for (size_t i = 0; i < targetDatas.size() - 1; ++i) {
if (targetDatas[i] != w) if (targetDatas[i] != w)
continue; continue;
std::swap(targetDatas[i], targetDatas[i + 1]); std::swap(targetDatas[i], targetDatas[i + 1]);
break; return true;
} }
return false;
} }
SP<SScrollingTargetData> SColumnData::next(SP<SScrollingTargetData> w) { SP<SScrollingTargetData> SColumnData::next(SP<SScrollingTargetData> w) {
@ -296,23 +302,21 @@ SScrollingData::SScrollingData(CScrollingAlgorithm* algo) : algorithm(algo) {
} }
SP<SColumnData> SScrollingData::add() { SP<SColumnData> SScrollingData::add() {
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
auto col = columns.emplace_back(makeShared<SColumnData>(self.lock())); auto col = columns.emplace_back(makeShared<SColumnData>(self.lock()));
col->self = col; col->self = col;
size_t stripIdx = controller->addStrip(*PCOLWIDTH); size_t stripIdx = controller->addStrip(algorithm->defaultColumnWidth());
controller->getStrip(stripIdx).userData = col; controller->getStrip(stripIdx).userData = col;
return col; return col;
} }
SP<SColumnData> SScrollingData::add(int after) { SP<SColumnData> SScrollingData::add(int after) {
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
auto col = makeShared<SColumnData>(self.lock()); auto col = makeShared<SColumnData>(self.lock());
col->self = col; col->self = col;
columns.insert(columns.begin() + after + 1, col); columns.insert(columns.begin() + after + 1, col);
controller->insertStrip(after, *PCOLWIDTH); controller->insertStrip(after, algorithm->defaultColumnWidth());
controller->getStrip(after + 1).userData = col; controller->getStrip(after + 1).userData = col;
return col; return col;
@ -466,6 +470,21 @@ CScrollingAlgorithm::CScrollingAlgorithm() {
m_scrollingData = makeShared<SScrollingData>(this); m_scrollingData = makeShared<SScrollingData>(this);
m_scrollingData->self = m_scrollingData; m_scrollingData->self = m_scrollingData;
// Helper to parse explicit_column_widths string
auto parseColumnWidths = [](const std::string& dir) -> std::vector<float> {
auto widthVec = std::vector<float>();
CConstVarList widths(dir, 0, ',');
for (auto& w : widths) {
try {
widthVec.emplace_back(std::clamp(std::stof(std::string{w}), MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH));
} catch (...) { Log::logger->log(Log::ERR, "scrolling: Failed to parse width {} as float", w); }
}
if (widthVec.empty())
widthVec = {0.333, 0.5, 0.667, 1.0}; // default
return widthVec;
};
// Helper to parse direction string // Helper to parse direction string
auto parseDirection = [](const std::string& dir) -> eScrollDirection { auto parseDirection = [](const std::string& dir) -> eScrollDirection {
if (dir == "left") if (dir == "left")
@ -478,19 +497,11 @@ CScrollingAlgorithm::CScrollingAlgorithm() {
return SCROLL_DIR_RIGHT; // default return SCROLL_DIR_RIGHT; // default
}; };
m_configCallback = Event::bus()->m_events.config.reloaded.listen([this, parseDirection] { m_configCallback = Event::bus()->m_events.config.reloaded.listen([this, parseColumnWidths, parseDirection] {
static const auto PCONFDIRECTION = CConfigValue<Hyprlang::STRING>("scrolling:direction"); static const auto PCONFDIRECTION = CConfigValue<Hyprlang::STRING>("scrolling:direction");
m_config.configuredWidths.clear(); m_config.configuredWidths.clear();
m_config.configuredWidths = parseColumnWidths(*PCONFWIDTHS);
CConstVarList widths(*PCONFWIDTHS, 0, ',');
for (auto& w : widths) {
try {
m_config.configuredWidths.emplace_back(std::stof(std::string{w}));
} catch (...) { Log::logger->log(Log::ERR, "scrolling: Failed to parse width {} as float", w); }
}
if (m_config.configuredWidths.empty())
m_config.configuredWidths = {0.333, 0.5, 0.667, 1.0};
// Update scroll direction // Update scroll direction
m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION)); m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION));
@ -523,7 +534,7 @@ CScrollingAlgorithm::CScrollingAlgorithm() {
}); });
// Initialize default widths and direction // Initialize default widths and direction
m_config.configuredWidths = {0.333, 0.5, 0.667, 1.0}; m_config.configuredWidths = parseColumnWidths(*PCONFWIDTHS);
m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION)); m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION));
} }
@ -617,7 +628,9 @@ void CScrollingAlgorithm::removeTarget(SP<ITarget> target) {
if (!m_scrollingData->next(DATA->column.lock()) && DATA->column->targetDatas.size() <= 1) { if (!m_scrollingData->next(DATA->column.lock()) && DATA->column->targetDatas.size() <= 1) {
// move the view if this is the last column // move the view if this is the last column
const auto USABLE = usableArea(); const auto USABLE = usableArea();
m_scrollingData->controller->adjustOffset(-(USABLE.w * DATA->column->getColumnWidth())); const bool isPrimaryHoriz = m_scrollingData->controller->isPrimaryHorizontal();
const double usablePrimary = isPrimaryHoriz ? USABLE.w : USABLE.h;
m_scrollingData->controller->adjustOffset(-(usablePrimary * DATA->column->getColumnWidth()));
} }
DATA->column->remove(target); DATA->column->remove(target);
@ -625,7 +638,9 @@ void CScrollingAlgorithm::removeTarget(SP<ITarget> target) {
if (!DATA->column) { if (!DATA->column) {
// column got removed, let's ensure we don't leave any cringe extra space // column got removed, let's ensure we don't leave any cringe extra space
const auto USABLE = usableArea(); const auto USABLE = usableArea();
double newOffset = std::clamp(m_scrollingData->controller->getOffset(), 0.0, std::max(m_scrollingData->maxWidth() - USABLE.w, 1.0)); const bool isPrimaryHoriz = m_scrollingData->controller->isPrimaryHorizontal();
const double usablePrimary = isPrimaryHoriz ? USABLE.w : USABLE.h;
const double newOffset = std::clamp(m_scrollingData->controller->getOffset(), 0.0, std::max(m_scrollingData->maxWidth() - usablePrimary, 1.0));
m_scrollingData->controller->setOffset(newOffset); m_scrollingData->controller->setOffset(newOffset);
} }
@ -833,21 +848,63 @@ void CScrollingAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection
} }
void CScrollingAlgorithm::moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool silent) { void CScrollingAlgorithm::moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool silent) {
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
const auto DATA = dataFor(t); const auto DATA = dataFor(t);
if (!DATA) if (!DATA)
return; return;
const auto TAPE_DIR = getDynamicDirection();
const auto CURRENT_COL = DATA->column.lock(); const auto CURRENT_COL = DATA->column.lock();
const auto current_idx = m_scrollingData->idx(CURRENT_COL); const auto current_idx = m_scrollingData->idx(CURRENT_COL);
if (dir == Math::DIRECTION_LEFT) { auto rotateDir = [this](Math::eDirection dir) -> Math::eDirection {
switch (m_scrollingData->controller->getDirection()) {
case SCROLL_DIR_RIGHT: return dir;
case SCROLL_DIR_LEFT: {
if (dir == Math::DIRECTION_LEFT)
return Math::DIRECTION_RIGHT;
if (dir == Math::DIRECTION_RIGHT)
return Math::DIRECTION_LEFT;
return dir;
}
case SCROLL_DIR_UP: {
switch (dir) {
case Math::DIRECTION_UP: return Math::DIRECTION_RIGHT;
case Math::DIRECTION_DOWN: return Math::DIRECTION_LEFT;
case Math::DIRECTION_LEFT: return Math::DIRECTION_DOWN;
case Math::DIRECTION_RIGHT: return Math::DIRECTION_UP;
default: break;
}
return dir;
}
case SCROLL_DIR_DOWN: {
switch (dir) {
case Math::DIRECTION_UP: return Math::DIRECTION_LEFT;
case Math::DIRECTION_DOWN: return Math::DIRECTION_RIGHT;
case Math::DIRECTION_LEFT: return Math::DIRECTION_DOWN;
case Math::DIRECTION_RIGHT: return Math::DIRECTION_UP;
default: break;
}
return dir;
}
default: break;
}
return dir;
};
const auto ROTATED_DIR = rotateDir(dir);
auto commenceDir = [&]() -> bool {
if (ROTATED_DIR == Math::DIRECTION_LEFT) {
const auto COL = m_scrollingData->prev(DATA->column.lock()); const auto COL = m_scrollingData->prev(DATA->column.lock());
// ignore moves to the "origin" when on first column and moving opposite to tape direction // ignore moves to the origin if we are alone
if (!COL && current_idx == 0 && (TAPE_DIR == SCROLL_DIR_RIGHT || TAPE_DIR == SCROLL_DIR_DOWN)) if (!COL && current_idx == 0 && DATA->column->targetDatas.size() == 1)
return; return false;
DATA->column->remove(t); DATA->column->remove(t);
@ -862,12 +919,14 @@ void CScrollingAlgorithm::moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool
COL->add(DATA); COL->add(DATA);
m_scrollingData->centerOrFitCol(COL); m_scrollingData->centerOrFitCol(COL);
} }
} else if (dir == Math::DIRECTION_RIGHT) {
return true;
} else if (ROTATED_DIR == Math::DIRECTION_RIGHT) {
const auto COL = m_scrollingData->next(DATA->column.lock()); const auto COL = m_scrollingData->next(DATA->column.lock());
// ignore moves to the "origin" when on last column and moving opposite to tape direction // ignore move to the right when there is no next column and we're alone
if (!COL && current_idx == (int64_t)m_scrollingData->columns.size() - 1 && (TAPE_DIR == SCROLL_DIR_LEFT || TAPE_DIR == SCROLL_DIR_UP)) if (!COL && current_idx == (int64_t)m_scrollingData->columns.size() - 1 && DATA->column->targetDatas.size() == 1)
return; return false;
DATA->column->remove(t); DATA->column->remove(t);
@ -884,15 +943,34 @@ void CScrollingAlgorithm::moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool
m_scrollingData->centerOrFitCol(COL); m_scrollingData->centerOrFitCol(COL);
} }
} else if (dir == Math::DIRECTION_UP) return true;
DATA->column->up(DATA); } else if (ROTATED_DIR == Math::DIRECTION_UP)
else if (dir == Math::DIRECTION_DOWN) return DATA->column->up(DATA);
DATA->column->down(DATA); else if (ROTATED_DIR == Math::DIRECTION_DOWN)
return DATA->column->down(DATA);
return false;
};
if (!commenceDir()) {
// dir wasn't commenced, move to a workspace if possible
// with the original dir
if (!*PMONITORFALLBACK)
return; // noop
const auto MONINDIR = g_pCompositor->getMonitorInDirection(m_parent->space()->workspace()->m_monitor.lock(), dir);
if (MONINDIR && MONINDIR != m_parent->space()->workspace()->m_monitor && MONINDIR->m_activeWorkspace) {
t->assignToSpace(MONINDIR->m_activeWorkspace->m_space, focalPointForDir(t, dir));
m_scrollingData->recalculate();
return;
}
}
m_scrollingData->recalculate(); m_scrollingData->recalculate();
focusTargetUpdate(t); focusTargetUpdate(t);
if (t->window())
g_pCompositor->warpCursorTo(t->window()->middle());
} }
std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::string_view& sv) { std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::string_view& sv) {
@ -1179,6 +1257,7 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
} else if (ARGS[0] == "focus") { } else if (ARGS[0] == "focus") {
const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr);
static const auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback"); static const auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback");
static const auto PCONFWRAPFOCUS = CConfigValue<Hyprlang::INT>("scrolling:wrap_focus");
if (!TDATA || ARGS[1].empty()) if (!TDATA || ARGS[1].empty())
return std::unexpected("no window to focus"); return std::unexpected("no window to focus");
@ -1234,7 +1313,7 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
g_pCompositor->warpCursorTo(TDATA->target->window()->middle()); g_pCompositor->warpCursorTo(TDATA->target->window()->middle());
return {}; return {};
} else } else
PREV = m_scrollingData->columns.back(); PREV = (*PCONFWRAPFOCUS == 1) ? m_scrollingData->columns.back() : m_scrollingData->columns.front();
} }
auto pTargetData = findBestNeighbor(TDATA, PREV); auto pTargetData = findBestNeighbor(TDATA, PREV);
@ -1256,7 +1335,7 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
g_pCompositor->warpCursorTo(TDATA->target->window()->middle()); g_pCompositor->warpCursorTo(TDATA->target->window()->middle());
return {}; return {};
} else } else
NEXT = m_scrollingData->columns.front(); NEXT = (*PCONFWRAPFOCUS == 1) ? m_scrollingData->columns.front() : m_scrollingData->columns.back();
} }
auto pTargetData = findBestNeighbor(TDATA, NEXT); auto pTargetData = findBestNeighbor(TDATA, NEXT);
@ -1283,6 +1362,8 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
m_scrollingData->recalculate(); m_scrollingData->recalculate();
} else if (ARGS[0] == "swapcol") { } else if (ARGS[0] == "swapcol") {
static const auto PCONFWRAPSWAPCOL = CConfigValue<Hyprlang::INT>("scrolling:wrap_swapcol");
if (ARGS.size() < 2) if (ARGS.size() < 2)
return std::unexpected("not enough args"); return std::unexpected("not enough args");
@ -1308,9 +1389,15 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
// wrap around swaps // wrap around swaps
if (direction == "l") if (direction == "l")
if (*PCONFWRAPSWAPCOL == 1)
targetIdx = (currentIdx == 0) ? (colCount - 1) : (currentIdx - 1); targetIdx = (currentIdx == 0) ? (colCount - 1) : (currentIdx - 1);
else
targetIdx = (currentIdx == 0) ? 0 : (currentIdx - 1);
else if (direction == "r") else if (direction == "r")
if (*PCONFWRAPSWAPCOL == 1)
targetIdx = (currentIdx == (int64_t)colCount - 1) ? 0 : (currentIdx + 1); targetIdx = (currentIdx == (int64_t)colCount - 1) ? 0 : (currentIdx + 1);
else
targetIdx = (currentIdx == (int64_t)colCount - 1) ? (colCount - 1) : (currentIdx + 1);
else else
return std::unexpected("no target (invalid direction?)"); return std::unexpected("no target (invalid direction?)");
; ;
@ -1418,9 +1505,14 @@ CBox CScrollingAlgorithm::usableArea() {
CBox box = m_parent->space()->workArea(); CBox box = m_parent->space()->workArea();
// doesn't matter, this happens when this algo is about to be destroyed // doesn't matter, this happens when this algo is about to be destroyed
if (!m_parent->space()->workspace()) if (!m_parent->space()->workspace() || !m_parent->space()->workspace()->m_monitor)
return box; return box;
box.translate(-m_parent->space()->workspace()->m_monitor->m_position); box.translate(-m_parent->space()->workspace()->m_monitor->m_position);
return box; return box;
} }
float CScrollingAlgorithm::defaultColumnWidth() {
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
return std::clamp(*PCOLWIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
}

View file

@ -40,8 +40,8 @@ namespace Layout::Tiled {
// index of lowest target that is above y. // index of lowest target that is above y.
size_t idxForHeight(float y); size_t idxForHeight(float y);
void up(SP<SScrollingTargetData> w); bool up(SP<SScrollingTargetData> w);
void down(SP<SScrollingTargetData> w); bool down(SP<SScrollingTargetData> w);
SP<SScrollingTargetData> next(SP<SScrollingTargetData> w); SP<SScrollingTargetData> next(SP<SScrollingTargetData> w);
SP<SScrollingTargetData> prev(SP<SScrollingTargetData> w); SP<SScrollingTargetData> prev(SP<SScrollingTargetData> w);
@ -138,6 +138,8 @@ namespace Layout::Tiled {
void moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool silent); void moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool silent);
void focusOnInput(SP<ITarget> target, eInputMode input); void focusOnInput(SP<ITarget> target, eInputMode input);
float defaultColumnWidth();
friend struct SScrollingData; friend struct SScrollingData;
}; };
}; };

View file

@ -6,6 +6,7 @@
#include "../../debug/log/Logger.hpp" #include "../../debug/log/Logger.hpp"
#include "../../desktop/Workspace.hpp" #include "../../desktop/Workspace.hpp"
#include "../../config/ConfigManager.hpp" #include "../../config/ConfigManager.hpp"
#include "../../event/EventBus.hpp"
using namespace Layout; using namespace Layout;
@ -17,6 +18,12 @@ SP<CSpace> CSpace::create(PHLWORKSPACE w) {
CSpace::CSpace(PHLWORKSPACE parent) : m_parent(parent) { CSpace::CSpace(PHLWORKSPACE parent) : m_parent(parent) {
recheckWorkArea(); recheckWorkArea();
// NOLINTNEXTLINE
m_geomUpdateCallback = Event::bus()->m_events.monitor.layoutChanged.listen([this] {
recheckWorkArea();
m_algorithm->recalculate();
});
} }
void CSpace::add(SP<ITarget> t) { void CSpace::add(SP<ITarget> t) {
@ -176,6 +183,11 @@ void CSpace::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool sil
m_algorithm->moveTargetInDirection(t, dir, silent); m_algorithm->moveTargetInDirection(t, dir, silent);
} }
void CSpace::setTargetGeom(const CBox& box, SP<ITarget> target) {
if (m_algorithm)
m_algorithm->setTargetGeom(box, target);
}
SP<ITarget> CSpace::getNextCandidate(SP<ITarget> old) { SP<ITarget> CSpace::getNextCandidate(SP<ITarget> old) {
return !m_algorithm ? nullptr : m_algorithm->getNextCandidate(old); return !m_algorithm ? nullptr : m_algorithm->getNextCandidate(old);
} }

View file

@ -47,6 +47,7 @@ namespace Layout {
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE); void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
void moveTarget(const Vector2D& Δ, SP<ITarget> target); void moveTarget(const Vector2D& Δ, SP<ITarget> target);
void setTargetGeom(const CBox& box, SP<ITarget> target); // only for float
SP<CAlgorithm> algorithm() const; SP<CAlgorithm> algorithm() const;
@ -63,5 +64,8 @@ namespace Layout {
// work area is in global coords // work area is in global coords
CBox m_workArea, m_floatingWorkArea; CBox m_workArea, m_floatingWorkArea;
// for recalc
CHyprSignalListener m_geomUpdateCallback;
}; };
}; };

View file

@ -239,6 +239,8 @@ void CDragStateController::dragEnd() {
draggingTarget->damageEntire(); draggingTarget->damageEntire();
g_layoutManager->setTargetGeom(draggingTarget->position(), draggingTarget);
Desktop::focusState()->fullWindowFocus(draggingTarget->window(), Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE); Desktop::focusState()->fullWindowFocus(draggingTarget->window(), Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE);
m_wasDraggingWindow = false; m_wasDraggingWindow = false;

View file

@ -10,6 +10,9 @@
#include "../../Compositor.hpp" #include "../../Compositor.hpp"
#include "../../render/Renderer.hpp" #include "../../render/Renderer.hpp"
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::Utils;
using namespace Layout; using namespace Layout;
SP<ITarget> CWindowTarget::create(PHLWINDOW w) { SP<ITarget> CWindowTarget::create(PHLWINDOW w) {
@ -34,6 +37,9 @@ void CWindowTarget::setPositionGlobal(const CBox& box) {
void CWindowTarget::updatePos() { void CWindowTarget::updatePos() {
g_pHyprRenderer->damageWindow(m_window.lock());
CScopeGuard x([this] { g_pHyprRenderer->damageWindow(m_window.lock()); });
if (!m_space) if (!m_space)
return; return;
@ -55,6 +61,11 @@ void CWindowTarget::updatePos() {
// Tiled is more complicated. // Tiled is more complicated.
// if we are in maximized, force the box to be max work area.
// TODO: this shouldn't be here.
if (fullscreenMode() == FSMODE_MAXIMIZED)
ITarget::setPositionGlobal(m_space->workArea(floating()));
const auto PMONITOR = m_space->workspace()->m_monitor; const auto PMONITOR = m_space->workspace()->m_monitor;
const auto PWORKSPACE = m_space->workspace(); const auto PWORKSPACE = m_space->workspace();
@ -104,7 +115,7 @@ void CWindowTarget::updatePos() {
Vector2D ratioPadding; Vector2D ratioPadding;
if ((*REQUESTEDRATIO).y != 0 && m_space->algorithm()->tiledTargets() <= 1) { if ((*REQUESTEDRATIO).y != 0 && m_space->algorithm()->tiledTargets() <= 1 && fullscreenMode() == FSMODE_NONE) {
const Vector2D originalSize = MONITOR_WORKAREA.size(); const Vector2D originalSize = MONITOR_WORKAREA.size();
const double requestedRatio = (*REQUESTEDRATIO).x / (*REQUESTEDRATIO).y; const double requestedRatio = (*REQUESTEDRATIO).x / (*REQUESTEDRATIO).y;
@ -131,7 +142,7 @@ void CWindowTarget::updatePos() {
calcPos = calcPos + GAPOFFSETTOPLEFT + ratioPadding / 2; calcPos = calcPos + GAPOFFSETTOPLEFT + ratioPadding / 2;
calcSize = calcSize - GAPOFFSETTOPLEFT - GAPOFFSETBOTTOMRIGHT - ratioPadding; calcSize = calcSize - GAPOFFSETTOPLEFT - GAPOFFSETBOTTOMRIGHT - ratioPadding;
if (isPseudo()) { if (isPseudo() && fullscreenMode() == FSMODE_NONE) {
// Calculate pseudo // Calculate pseudo
float scale = 1; float scale = 1;
@ -162,18 +173,14 @@ void CWindowTarget::updatePos() {
static auto PCLAMP_TILED = CConfigValue<Hyprlang::INT>("misc:size_limits_tiled"); static auto PCLAMP_TILED = CConfigValue<Hyprlang::INT>("misc:size_limits_tiled");
if (*PCLAMP_TILED) { if (*PCLAMP_TILED) {
const auto borderSize = m_window->getRealBorderSize(); Vector2D minSize = m_window->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE});
Vector2D monitorAvailable = MONITOR_WORKAREA.size() - Vector2D{2.0 * borderSize, 2.0 * borderSize}; Vector2D maxSize = m_window->isFullscreen() ? Vector2D{INFINITY, INFINITY} : m_window->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY});
Vector2D minSize = m_window->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}).clamp(Vector2D{0, 0}, monitorAvailable);
Vector2D maxSize = m_window->isFullscreen() ? Vector2D{INFINITY, INFINITY} :
m_window->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY}).clamp(Vector2D{0, 0}, monitorAvailable);
calcSize = calcSize.clamp(minSize, maxSize); calcSize = calcSize.clamp(minSize, maxSize);
calcPos += (availableSpace - calcSize) / 2.0; calcPos += (availableSpace - calcSize) / 2.0;
calcPos.x = std::clamp(calcPos.x, MONITOR_WORKAREA.x + borderSize, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w - calcSize.x - borderSize); calcPos.x = std::clamp(calcPos.x, MONITOR_WORKAREA.x, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w - calcSize.x);
calcPos.y = std::clamp(calcPos.y, MONITOR_WORKAREA.y + borderSize, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h - calcSize.y - borderSize); calcPos.y = std::clamp(calcPos.y, MONITOR_WORKAREA.y, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h - calcSize.y);
} }
if (m_window->onSpecialWorkspace() && !m_window->isFullscreen()) { if (m_window->onSpecialWorkspace() && !m_window->isFullscreen()) {
@ -370,5 +377,6 @@ void CWindowTarget::onUpdateSpace() {
m_window->m_monitor = space()->workspace()->m_monitor; m_window->m_monitor = space()->workspace()->m_monitor;
m_window->moveToWorkspace(space()->workspace()); m_window->moveToWorkspace(space()->workspace());
m_window->updateToplevel(); m_window->updateToplevel();
m_window->updateWindowData();
m_window->updateWindowDecos(); m_window->updateWindowDecos();
} }

View file

@ -109,9 +109,6 @@ CKeybindManager::CKeybindManager() {
m_dispatchers["togglegroup"] = toggleGroup; m_dispatchers["togglegroup"] = toggleGroup;
m_dispatchers["changegroupactive"] = changeGroupActive; m_dispatchers["changegroupactive"] = changeGroupActive;
m_dispatchers["movegroupwindow"] = moveGroupWindow; m_dispatchers["movegroupwindow"] = moveGroupWindow;
m_dispatchers["togglesplit"] = toggleSplit;
m_dispatchers["swapsplit"] = swapSplit;
m_dispatchers["splitratio"] = alterSplitRatio;
m_dispatchers["focusmonitor"] = focusMonitor; m_dispatchers["focusmonitor"] = focusMonitor;
m_dispatchers["movecursortocorner"] = moveCursorToCorner; m_dispatchers["movecursortocorner"] = moveCursorToCorner;
m_dispatchers["movecursor"] = moveCursor; m_dispatchers["movecursor"] = moveCursor;
@ -1470,6 +1467,7 @@ SDispatchResult CKeybindManager::moveActiveToWorkspaceSilent(std::string args) {
SDispatchResult CKeybindManager::moveFocusTo(std::string args) { SDispatchResult CKeybindManager::moveFocusTo(std::string args) {
static auto PFULLCYCLE = CConfigValue<Hyprlang::INT>("binds:movefocus_cycles_fullscreen"); static auto PFULLCYCLE = CConfigValue<Hyprlang::INT>("binds:movefocus_cycles_fullscreen");
static auto PGROUPCYCLE = CConfigValue<Hyprlang::INT>("binds:movefocus_cycles_groupfirst"); static auto PGROUPCYCLE = CConfigValue<Hyprlang::INT>("binds:movefocus_cycles_groupfirst");
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
Math::eDirection dir = Math::fromChar(args[0]); Math::eDirection dir = Math::fromChar(args[0]);
if (dir == Math::DIRECTION_DEFAULT) { if (dir == Math::DIRECTION_DEFAULT) {
@ -1479,6 +1477,7 @@ SDispatchResult CKeybindManager::moveFocusTo(std::string args) {
const auto PLASTWINDOW = Desktop::focusState()->window(); const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW || !PLASTWINDOW->aliveAndVisible()) { if (!PLASTWINDOW || !PLASTWINDOW->aliveAndVisible()) {
if (*PMONITORFALLBACK)
tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir)); tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir));
return {}; return {};
} }
@ -1509,7 +1508,7 @@ SDispatchResult CKeybindManager::moveFocusTo(std::string args) {
Log::logger->log(Log::DEBUG, "No window found in direction {}, looking for a monitor", Math::toString(dir)); Log::logger->log(Log::DEBUG, "No window found in direction {}, looking for a monitor", Math::toString(dir));
if (tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir))) if (*PMONITORFALLBACK && tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir)))
return {}; return {};
static auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback"); static auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback");
@ -1690,7 +1689,10 @@ SDispatchResult CKeybindManager::changeGroupActive(std::string args) {
// index starts from '1'; '0' means last window // index starts from '1'; '0' means last window
try { try {
const int INDEX = std::stoi(args); const int INDEX = std::stoi(args);
PWINDOW->m_group->setCurrent(INDEX); if (INDEX <= 0)
PWINDOW->m_group->setCurrent(PWINDOW->m_group->size() - 1);
else
PWINDOW->m_group->setCurrent(INDEX - 1);
} catch (...) { return {.success = false, .error = "invalid idx"}; } } catch (...) { return {.success = false, .error = "invalid idx"}; }
return {}; return {};
@ -1704,18 +1706,6 @@ SDispatchResult CKeybindManager::changeGroupActive(std::string args) {
return {}; return {};
} }
SDispatchResult CKeybindManager::toggleSplit(std::string args) {
return {.success = false, .error = "removed - use layoutmsg"};
}
SDispatchResult CKeybindManager::swapSplit(std::string args) {
return {.success = false, .error = "removed - use layoutmsg"};
}
SDispatchResult CKeybindManager::alterSplitRatio(std::string args) {
return {.success = false, .error = "removed - use layoutmsg"};
}
SDispatchResult CKeybindManager::focusMonitor(std::string arg) { SDispatchResult CKeybindManager::focusMonitor(std::string arg) {
const auto PMONITOR = g_pCompositor->getMonitorFromString(arg); const auto PMONITOR = g_pCompositor->getMonitorFromString(arg);
tryMoveFocusToMonitor(PMONITOR); tryMoveFocusToMonitor(PMONITOR);
@ -2747,7 +2737,9 @@ void CKeybindManager::moveWindowOutOfGroup(PHLWINDOW pWindow, const std::string&
WP<Desktop::View::CGroup> group = pWindow->m_group; WP<Desktop::View::CGroup> group = pWindow->m_group;
pWindow->m_group->remove(pWindow); const auto direction = !dir.empty() ? Math::fromChar(dir[0]) : Math::DIRECTION_DEFAULT;
pWindow->m_group->remove(pWindow, direction);
if (*BFOCUSREMOVEDWINDOW || !group) { if (*BFOCUSREMOVEDWINDOW || !group) {
Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND); Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND);

View file

@ -194,10 +194,7 @@ class CKeybindManager {
static SDispatchResult swapActive(std::string); static SDispatchResult swapActive(std::string);
static SDispatchResult toggleGroup(std::string); static SDispatchResult toggleGroup(std::string);
static SDispatchResult changeGroupActive(std::string); static SDispatchResult changeGroupActive(std::string);
static SDispatchResult alterSplitRatio(std::string);
static SDispatchResult focusMonitor(std::string); static SDispatchResult focusMonitor(std::string);
static SDispatchResult toggleSplit(std::string);
static SDispatchResult swapSplit(std::string);
static SDispatchResult moveCursorToCorner(std::string); static SDispatchResult moveCursorToCorner(std::string);
static SDispatchResult moveCursor(std::string); static SDispatchResult moveCursor(std::string);
static SDispatchResult workspaceOpt(std::string); static SDispatchResult workspaceOpt(std::string);

View file

@ -8,6 +8,7 @@
#include "../protocols/IdleNotify.hpp" #include "../protocols/IdleNotify.hpp"
#include "../protocols/core/Compositor.hpp" #include "../protocols/core/Compositor.hpp"
#include "../protocols/core/Seat.hpp" #include "../protocols/core/Seat.hpp"
#include "debug/log/Logger.hpp"
#include "eventLoop/EventLoopManager.hpp" #include "eventLoop/EventLoopManager.hpp"
#include "../render/pass/TexPassElement.hpp" #include "../render/pass/TexPassElement.hpp"
#include "../managers/input/InputManager.hpp" #include "../managers/input/InputManager.hpp"
@ -18,9 +19,12 @@
#include "../helpers/time/Time.hpp" #include "../helpers/time/Time.hpp"
#include "../helpers/Drm.hpp" #include "../helpers/Drm.hpp"
#include "../event/EventBus.hpp" #include "../event/EventBus.hpp"
#include <climits>
#include <cstring> #include <cstring>
#include <gbm.h> #include <gbm.h>
#include <cairo/cairo.h> #include <cairo/cairo.h>
#include <hyprutils/math/Region.hpp>
#include <hyprutils/math/Vector2D.hpp>
#include <hyprutils/utils/ScopeGuard.hpp> #include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::Utils; using namespace Hyprutils::Utils;
@ -406,7 +410,7 @@ bool CPointerManager::setHWCursorBuffer(SP<SMonitorPointerState> state, SP<Aquam
return true; return true;
} }
SP<Aquamarine::IBuffer> CPointerManager::renderHWCursorBuffer(SP<CPointerManager::SMonitorPointerState> state, SP<CTexture> texture) { SP<Aquamarine::IBuffer> CPointerManager::renderHWCursorBuffer(SP<CPointerManager::SMonitorPointerState> state, SP<ITexture> texture) {
auto maxSize = state->monitor->m_output->cursorPlaneSize(); auto maxSize = state->monitor->m_output->cursorPlaneSize();
auto const& cursorSize = m_currentCursorImage.size; auto const& cursorSize = m_currentCursorImage.size;
@ -539,24 +543,23 @@ SP<Aquamarine::IBuffer> CPointerManager::renderHWCursorBuffer(SP<CPointerManager
// we need to scale the cursor to the right size, because it might not be (esp with XCursor) // we need to scale the cursor to the right size, because it might not be (esp with XCursor)
const auto SCALE = texture->m_size / (m_currentCursorImage.size / m_currentCursorImage.scale * state->monitor->m_scale); const auto SCALE = texture->m_size / (m_currentCursorImage.size / m_currentCursorImage.scale * state->monitor->m_scale);
cairo_matrix_scale(&matrixPre, SCALE.x, SCALE.y); const auto SX = SCALE.x, SY = SCALE.y;
const auto BW = sc<double>(DMABUF.size.x), BH = sc<double>(DMABUF.size.y);
if (TR) { // Cairo pattern matrix maps destination coords to source coords (inverse of visual transform).
cairo_matrix_rotate(&matrixPre, M_PI_2 * sc<double>(TR)); // x_src = xx * x_dst + xy * y_dst + x0
// y_src = yx * x_dst + yy * y_dst + y0
// FIXME: this is wrong, and doesn't work for 5, 6 and 7. (flipped + rot) // cairo_matrix_init(&m, xx, yx, xy, yy, x0, y0)
// cba to do it rn, does anyone fucking use that?? switch (TR) {
if (TR >= WL_OUTPUT_TRANSFORM_FLIPPED) { case WL_OUTPUT_TRANSFORM_NORMAL:
cairo_matrix_scale(&matrixPre, -1, 1); default: cairo_matrix_init(&matrixPre, SX, 0, 0, SY, 0, 0); break;
cairo_matrix_translate(&matrixPre, -DMABUF.size.x, 0); case WL_OUTPUT_TRANSFORM_90: cairo_matrix_init(&matrixPre, 0, SY, -SX, 0, SX * BW, 0); break;
} case WL_OUTPUT_TRANSFORM_180: cairo_matrix_init(&matrixPre, -SX, 0, 0, -SY, SX * BW, SY * BH); break;
case WL_OUTPUT_TRANSFORM_270: cairo_matrix_init(&matrixPre, 0, -SY, SX, 0, 0, SY * BH); break;
if (TR == 3 || TR == 7) case WL_OUTPUT_TRANSFORM_FLIPPED: cairo_matrix_init(&matrixPre, -SX, 0, 0, SY, SX * BW, 0); break;
cairo_matrix_translate(&matrixPre, -DMABUF.size.x, 0); case WL_OUTPUT_TRANSFORM_FLIPPED_90: cairo_matrix_init(&matrixPre, 0, SY, SX, 0, 0, 0); break;
else if (TR == 2 || TR == 6) case WL_OUTPUT_TRANSFORM_FLIPPED_180: cairo_matrix_init(&matrixPre, SX, 0, 0, -SY, 0, SY * BH); break;
cairo_matrix_translate(&matrixPre, -DMABUF.size.x, -DMABUF.size.y); case WL_OUTPUT_TRANSFORM_FLIPPED_270: cairo_matrix_init(&matrixPre, 0, -SY, -SX, 0, SX * BW, SY * BH); break;
else if (TR == 1 || TR == 5)
cairo_matrix_translate(&matrixPre, 0, -DMABUF.size.y);
} }
cairo_pattern_set_matrix(PATTERNPRE, &matrixPre); cairo_pattern_set_matrix(PATTERNPRE, &matrixPre);
@ -589,15 +592,14 @@ SP<Aquamarine::IBuffer> CPointerManager::renderHWCursorBuffer(SP<CPointerManager
RBO->bind(); RBO->bind();
const auto& damageSize = state->monitor->m_output->cursorPlaneSize(); g_pHyprOpenGL->beginSimple(state->monitor.lock(), {0, 0, INT_MAX, INT_MAX}, RBO);
g_pHyprOpenGL->beginSimple(state->monitor.lock(), {0, 0, damageSize.x, damageSize.y}, RBO);
g_pHyprOpenGL->clear(CHyprColor{0.F, 0.F, 0.F, 0.F}); // ensure the RBO is zero initialized. g_pHyprOpenGL->clear(CHyprColor{0.F, 0.F, 0.F, 0.F}); // ensure the RBO is zero initialized.
CBox xbox = {{}, Vector2D{m_currentCursorImage.size / m_currentCursorImage.scale * state->monitor->m_scale}.round()}; CBox xbox = {{}, Vector2D{m_currentCursorImage.size / m_currentCursorImage.scale * state->monitor->m_scale}.round()};
Log::logger->log(Log::TRACE, "[pointer] monitor: {}, size: {}, hw buf: {}, scale: {:.2f}, monscale: {:.2f}, xbox: {}", state->monitor->m_name, m_currentCursorImage.size, Log::logger->log(Log::TRACE, "[pointer] monitor: {}, size: {}, hw buf: {}, scale: {:.2f}, monscale: {:.2f}, xbox: {}", state->monitor->m_name, m_currentCursorImage.size,
cursorSize, m_currentCursorImage.scale, state->monitor->m_scale, xbox.size()); cursorSize, m_currentCursorImage.scale, state->monitor->m_scale, xbox.size());
g_pHyprOpenGL->renderTexture(texture, xbox, {}); g_pHyprOpenGL->renderTexture(texture, xbox, {.noCM = true});
g_pHyprOpenGL->end(); g_pHyprOpenGL->end();
g_pHyprOpenGL->m_renderData.pMonitor.reset(); g_pHyprOpenGL->m_renderData.pMonitor.reset();
@ -901,13 +903,13 @@ const CPointerManager::SCursorImage& CPointerManager::currentCursorImage() {
return m_currentCursorImage; return m_currentCursorImage;
} }
SP<CTexture> CPointerManager::getCurrentCursorTexture() { SP<ITexture> CPointerManager::getCurrentCursorTexture() {
if (!m_currentCursorImage.pBuffer && (!m_currentCursorImage.surface || !m_currentCursorImage.surface->resource()->m_current.texture)) if (!m_currentCursorImage.pBuffer && (!m_currentCursorImage.surface || !m_currentCursorImage.surface->resource()->m_current.texture))
return nullptr; return nullptr;
if (m_currentCursorImage.pBuffer) { if (m_currentCursorImage.pBuffer) {
if (!m_currentCursorImage.bufferTex) if (!m_currentCursorImage.bufferTex)
m_currentCursorImage.bufferTex = makeShared<CTexture>(m_currentCursorImage.pBuffer, true); m_currentCursorImage.bufferTex = g_pHyprRenderer->createTexture(m_currentCursorImage.pBuffer, true);
return m_currentCursorImage.bufferTex; return m_currentCursorImage.bufferTex;
} }

View file

@ -12,7 +12,7 @@
class CMonitor; class CMonitor;
class IHID; class IHID;
class CTexture; class ITexture;
AQUAMARINE_FORWARD(IBuffer); AQUAMARINE_FORWARD(IBuffer);
@ -71,7 +71,7 @@ class CPointerManager {
struct SCursorImage { struct SCursorImage {
SP<Aquamarine::IBuffer> pBuffer; SP<Aquamarine::IBuffer> pBuffer;
SP<CTexture> bufferTex; SP<ITexture> bufferTex;
WP<Desktop::View::CWLSurface> surface; WP<Desktop::View::CWLSurface> surface;
Vector2D hotspot; Vector2D hotspot;
@ -83,7 +83,7 @@ class CPointerManager {
}; };
const SCursorImage& currentCursorImage(); const SCursorImage& currentCursorImage();
SP<CTexture> getCurrentCursorTexture(); SP<ITexture> getCurrentCursorTexture();
struct { struct {
CSignalT<> cursorChanged; CSignalT<> cursorChanged;
@ -181,7 +181,7 @@ class CPointerManager {
std::vector<SP<SMonitorPointerState>> m_monitorStates; std::vector<SP<SMonitorPointerState>> m_monitorStates;
SP<SMonitorPointerState> stateFor(PHLMONITOR mon); SP<SMonitorPointerState> stateFor(PHLMONITOR mon);
bool attemptHardwareCursor(SP<SMonitorPointerState> state); bool attemptHardwareCursor(SP<SMonitorPointerState> state);
SP<Aquamarine::IBuffer> renderHWCursorBuffer(SP<SMonitorPointerState> state, SP<CTexture> texture); SP<Aquamarine::IBuffer> renderHWCursorBuffer(SP<SMonitorPointerState> state, SP<ITexture> texture);
bool setHWCursorBuffer(SP<SMonitorPointerState> state, SP<Aquamarine::IBuffer> buf); bool setHWCursorBuffer(SP<SMonitorPointerState> state, SP<Aquamarine::IBuffer> buf);
struct { struct {

View file

@ -847,6 +847,9 @@ void CInputManager::processMouseDownKill(const IPointer::SButtonEvent& e) {
break; break;
} }
g_pEventManager->postEvent(SHyprIPCEvent({.event = "kill", .data = std::format("{:x}", rc<uintptr_t>(PWINDOW.m_data))}));
Event::bus()->m_events.window.kill.emit(PWINDOW);
// kill the mf // kill the mf
kill(PWINDOW->getPID(), SIGKILL); kill(PWINDOW->getPID(), SIGKILL);
break; break;

View file

@ -169,10 +169,10 @@ bool CCursorshareSession::copy() {
return false; return false;
} }
CFramebuffer outFB; auto outFB = g_pHyprRenderer->createFB();
outFB.alloc(m_bufferSize.x, m_bufferSize.y, m_format); outFB->alloc(m_bufferSize.x, m_bufferSize.y, m_format);
if (!g_pHyprRenderer->beginRender(m_pendingFrame.monitor, fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, &outFB, true)) { if (!g_pHyprRenderer->beginRender(m_pendingFrame.monitor, fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, outFB, true)) {
LOGM(Log::ERR, "Can't copy: failed to begin rendering to shm"); LOGM(Log::ERR, "Can't copy: failed to begin rendering to shm");
return false; return false;
} }
@ -182,8 +182,8 @@ bool CCursorshareSession::copy() {
g_pHyprRenderer->endRender(); g_pHyprRenderer->endRender();
g_pHyprOpenGL->m_renderData.pMonitor = m_pendingFrame.monitor; g_pHyprOpenGL->m_renderData.pMonitor = m_pendingFrame.monitor;
outFB.bind(); outFB->bind();
glBindFramebuffer(GL_READ_FRAMEBUFFER, outFB.getFBID()); glBindFramebuffer(GL_READ_FRAMEBUFFER, GLFB(outFB)->getFBID());
glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_PACK_ALIGNMENT, 1);
@ -212,7 +212,7 @@ bool CCursorshareSession::copy() {
g_pHyprOpenGL->m_renderData.pMonitor.reset(); g_pHyprOpenGL->m_renderData.pMonitor.reset();
m_pendingFrame.buffer->endDataPtr(); m_pendingFrame.buffer->endDataPtr();
outFB.unbind(); GLFB(outFB)->unbind();
glPixelStorei(GL_PACK_ALIGNMENT, 4); glPixelStorei(GL_PACK_ALIGNMENT, 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);

View file

@ -10,6 +10,7 @@
#include "../../helpers/Monitor.hpp" #include "../../helpers/Monitor.hpp"
#include "../../desktop/view/Window.hpp" #include "../../desktop/view/Window.hpp"
#include "../../desktop/state/FocusState.hpp" #include "../../desktop/state/FocusState.hpp"
#include <hyprutils/math/Region.hpp>
using namespace Screenshare; using namespace Screenshare;
@ -133,7 +134,7 @@ void CScreenshareFrame::copy() {
// store a snapshot before the permission popup so we don't break screenshots // store a snapshot before the permission popup so we don't break screenshots
const auto PERM = g_pDynamicPermissionManager->clientPermissionMode(m_session->m_client, PERMISSION_TYPE_SCREENCOPY); const auto PERM = g_pDynamicPermissionManager->clientPermissionMode(m_session->m_client, PERMISSION_TYPE_SCREENCOPY);
if (PERM == PERMISSION_RULE_ALLOW_MODE_PENDING) { if (PERM == PERMISSION_RULE_ALLOW_MODE_PENDING) {
if (!m_session->m_tempFB.isAllocated()) if (!m_session->m_tempFB || !m_session->m_tempFB->isAllocated())
storeTempFB(); storeTempFB();
// don't copy a frame while allow is pending because screenshot tools will only take the first frame we give, which is empty // don't copy a frame while allow is pending because screenshot tools will only take the first frame we give, which is empty
@ -159,7 +160,7 @@ void CScreenshareFrame::renderMonitor() {
const auto PMONITOR = m_session->monitor(); const auto PMONITOR = m_session->monitor();
auto TEXTURE = makeShared<CTexture>(PMONITOR->m_output->state->state().buffer); auto TEXTURE = g_pHyprRenderer->createTexture(PMONITOR->m_output->state->state().buffer);
const bool IS_CM_AWARE = PROTO::colorManagement && PROTO::colorManagement->isClientCMAware(m_session->m_client); const bool IS_CM_AWARE = PROTO::colorManagement && PROTO::colorManagement->isClientCMAware(m_session->m_client);
g_pHyprOpenGL->m_renderData.transformDamage = false; g_pHyprOpenGL->m_renderData.transformDamage = false;
@ -295,8 +296,11 @@ void CScreenshareFrame::renderWindow() {
return; return;
auto pointerSurface = Desktop::View::CWLSurface::fromResource(pointerSurfaceResource); auto pointerSurface = Desktop::View::CWLSurface::fromResource(pointerSurfaceResource);
if (!pointerSurface)
return;
if (!pointerSurface || pointerSurface->getSurfaceBoxGlobal()->intersection(m_session->m_window->getFullWindowBoundingBox()).empty()) auto box = pointerSurface->getSurfaceBoxGlobal();
if (!box.has_value() || box->intersection(m_session->m_window->getFullWindowBoundingBox()).empty())
return; return;
if (Desktop::focusState()->window() != m_session->m_window) if (Desktop::focusState()->window() != m_session->m_window)
@ -322,10 +326,10 @@ void CScreenshareFrame::render() {
return; return;
} }
if (m_session->m_tempFB.isAllocated()) { if (m_session->m_tempFB && m_session->m_tempFB->isAllocated()) {
CBox texbox = {{}, m_bufferSize}; CBox texbox = {{}, m_bufferSize};
g_pHyprOpenGL->renderTexture(m_session->m_tempFB.getTexture(), texbox, {}); g_pHyprOpenGL->renderTexture(m_session->m_tempFB->getTexture(), texbox, {});
m_session->m_tempFB.release(); m_session->m_tempFB->release();
return; return;
} }
@ -380,10 +384,10 @@ bool CScreenshareFrame::copyShm() {
const auto PMONITOR = m_session->monitor(); const auto PMONITOR = m_session->monitor();
CFramebuffer outFB; auto outFB = g_pHyprRenderer->createFB();
outFB.alloc(m_bufferSize.x, m_bufferSize.y, shm.format); outFB->alloc(m_bufferSize.x, m_bufferSize.y, shm.format);
if (!g_pHyprRenderer->beginRender(PMONITOR, m_damage, RENDER_MODE_FULL_FAKE, nullptr, &outFB, true)) { if (!g_pHyprRenderer->beginRender(PMONITOR, m_damage, RENDER_MODE_FULL_FAKE, nullptr, outFB, true)) {
LOGM(Log::ERR, "Can't copy: failed to begin rendering"); LOGM(Log::ERR, "Can't copy: failed to begin rendering");
return false; return false;
} }
@ -395,8 +399,8 @@ bool CScreenshareFrame::copyShm() {
g_pHyprRenderer->endRender(); g_pHyprRenderer->endRender();
g_pHyprOpenGL->m_renderData.pMonitor = PMONITOR; g_pHyprOpenGL->m_renderData.pMonitor = PMONITOR;
outFB.bind(); outFB->bind();
glBindFramebuffer(GL_READ_FRAMEBUFFER, outFB.getFBID()); glBindFramebuffer(GL_READ_FRAMEBUFFER, GLFB(outFB)->getFBID());
glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_PACK_ALIGNMENT, 1);
@ -438,7 +442,7 @@ bool CScreenshareFrame::copyShm() {
}); });
} }
outFB.unbind(); GLFB(outFB)->unbind();
glPixelStorei(GL_PACK_ALIGNMENT, 4); glPixelStorei(GL_PACK_ALIGNMENT, 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
@ -453,13 +457,13 @@ bool CScreenshareFrame::copyShm() {
} }
void CScreenshareFrame::storeTempFB() { void CScreenshareFrame::storeTempFB() {
g_pHyprRenderer->makeEGLCurrent(); if (!m_session->m_tempFB)
m_session->m_tempFB = g_pHyprRenderer->createFB();
m_session->m_tempFB.alloc(m_bufferSize.x, m_bufferSize.y); m_session->m_tempFB->alloc(m_bufferSize.x, m_bufferSize.y);
CRegion fakeDamage = {0, 0, INT16_MAX, INT16_MAX}; CRegion fakeDamage = {0, 0, INT16_MAX, INT16_MAX};
if (!g_pHyprRenderer->beginRender(m_session->monitor(), fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, &m_session->m_tempFB, true)) { if (!g_pHyprRenderer->beginRender(m_session->monitor(), fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, m_session->m_tempFB, true)) {
LOGM(Log::ERR, "Can't copy: failed to begin rendering to temp fb"); LOGM(Log::ERR, "Can't copy: failed to begin rendering to temp fb");
return; return;
} }

View file

@ -145,15 +145,19 @@ WP<CScreenshareSession> CScreenshareManager::getManagedSession(eScreenshareType
auto& session = *it; auto& session = *it;
session->stoppedListener = session->m_session->m_events.stopped.listen([session = WP<SManagedSession>(session)]() { session->stoppedListener = session->m_session->m_events.stopped.listen([session = WP<SManagedSession>(session)]() {
std::erase_if(Screenshare::mgr()->m_managedSessions, [&](const auto& s) { return !s || session.expired() || s->m_session == session->m_session; }); if (!session.expired())
std::erase_if(Screenshare::mgr()->m_managedSessions, [&](const auto& s) { return s && s->m_session.get() == session->m_session.get(); });
}); });
return session->m_session; return session->m_session;
} }
void CScreenshareManager::destroyClientSessions(wl_client* client) { bool CScreenshareManager::isOutputBeingSSd(PHLMONITOR monitor) {
LOGM(Log::TRACE, "Destroy client sessions for {:x}", (uintptr_t)client); return std::ranges::any_of(m_sessions, [monitor](const auto& s) {
std::erase_if(m_managedSessions, [&](const auto& session) { return !session || session->m_session->m_client == client; }); if (!s)
return false;
return (s->m_type == SHARE_MONITOR || s->m_type == SHARE_REGION) && s->m_monitor == monitor;
});
} }
CScreenshareManager::SManagedSession::SManagedSession(UP<CScreenshareSession>&& session) : m_session(std::move(session)) { CScreenshareManager::SManagedSession::SManagedSession(UP<CScreenshareSession>&& session) : m_session(std::move(session)) {

View file

@ -75,7 +75,7 @@ namespace Screenshare {
std::vector<DRMFormat> m_formats; std::vector<DRMFormat> m_formats;
Vector2D m_bufferSize = Vector2D(0, 0); Vector2D m_bufferSize = Vector2D(0, 0);
CFramebuffer m_tempFB; SP<IFramebuffer> m_tempFB;
SP<CEventLoopTimer> m_shareStopTimer; SP<CEventLoopTimer> m_shareStopTimer;
bool m_sharing = false; bool m_sharing = false;
@ -206,9 +206,8 @@ namespace Screenshare {
UP<CCursorshareSession> newCursorSession(wl_client* client, WP<CWLPointerResource> pointer); UP<CCursorshareSession> newCursorSession(wl_client* client, WP<CWLPointerResource> pointer);
void destroyClientSessions(wl_client* client);
void onOutputCommit(PHLMONITOR monitor); void onOutputCommit(PHLMONITOR monitor);
bool isOutputBeingSSd(PHLMONITOR monitor);
private: private:
std::vector<WP<CScreenshareSession>> m_sessions; std::vector<WP<CScreenshareSession>> m_sessions;

View file

@ -39,7 +39,8 @@ CScreenshareSession::CScreenshareSession(PHLMONITOR monitor, CBox captureRegion,
CScreenshareSession::~CScreenshareSession() { CScreenshareSession::~CScreenshareSession() {
stop(); stop();
LOGM(Log::TRACE, "Destroyed screenshare session for ({}): {}", m_type, m_name); uintptr_t ptr = m_type == SHARE_WINDOW && !m_window.expired() ? (uintptr_t)m_window.get() : (m_monitor.expired() ? (uintptr_t)nullptr : (uintptr_t)m_monitor.get());
LOGM(Log::TRACE, "Destroyed screenshare session for ({}): {}, {:x}", m_type, m_name, ptr);
} }
void CScreenshareSession::stop() { void CScreenshareSession::stop() {
@ -52,6 +53,9 @@ void CScreenshareSession::stop() {
} }
void CScreenshareSession::init() { void CScreenshareSession::init() {
uintptr_t ptr = m_type == SHARE_WINDOW && !m_window.expired() ? (uintptr_t)m_window.get() : (m_monitor.expired() ? (uintptr_t)nullptr : (uintptr_t)m_monitor.get());
LOGM(Log::TRACE, "Created screenshare session for ({}): {}, {:x}", m_type, m_name, ptr);
m_shareStopTimer = makeShared<CEventLoopTimer>( m_shareStopTimer = makeShared<CEventLoopTimer>(
std::chrono::milliseconds(500), std::chrono::milliseconds(500),
[this](SP<CEventLoopTimer> self, void* data) { [this](SP<CEventLoopTimer> self, void* data) {
@ -99,7 +103,7 @@ void CScreenshareSession::calculateConstraints() {
m_name = PMONITOR->m_name; m_name = PMONITOR->m_name;
break; break;
case SHARE_WINDOW: case SHARE_WINDOW:
m_bufferSize = m_window->m_realSize->value().round(); m_bufferSize = (m_window->m_realSize->value() * PMONITOR->m_scale).round();
m_name = m_window->m_title; m_name = m_window->m_title;
break; break;
case SHARE_REGION: case SHARE_REGION:
@ -121,7 +125,7 @@ void CScreenshareSession::screenshareEvents(bool startSharing) {
m_sharing = true; m_sharing = true;
g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencast", .data = std::format("1,{}", m_type)}); g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencast", .data = std::format("1,{}", m_type)});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencastv2", .data = std::format("1,{},{}", m_type, m_name)}); g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencastv2", .data = std::format("1,{},{}", m_type, m_name)});
LOGM(Log::INFO, "New screenshare session for ({}): {}", m_type, m_name); LOGM(Log::INFO, "Started screenshare session for ({}): {}", m_type, m_name);
Event::bus()->m_events.screenshare.state.emit(true, m_type, m_name); Event::bus()->m_events.screenshare.state.emit(true, m_type, m_name);
} else if (!startSharing && m_sharing) { } else if (!startSharing && m_sharing) {

View file

@ -3,7 +3,7 @@
#include "color-management-v1.hpp" #include "color-management-v1.hpp"
#include "../helpers/Monitor.hpp" #include "../helpers/Monitor.hpp"
#include "core/Output.hpp" #include "core/Output.hpp"
#include "types/ColorManagement.hpp" #include "../helpers/cm/ColorManagement.hpp"
#include <cstdint> #include <cstdint>
using namespace NColorManagement; using namespace NColorManagement;
@ -388,12 +388,6 @@ CColorManagementFeedbackSurface::CColorManagementFeedbackSurface(SP<CWpColorMana
RESOURCE->m_settings = m_surface->getPreferredImageDescription(); RESOURCE->m_settings = m_surface->getPreferredImageDescription();
m_currentPreferredId = RESOURCE->m_settings->id(); m_currentPreferredId = RESOURCE->m_settings->id();
if (!PROTO::colorManagement->m_debug && RESOURCE->m_settings->value().icc.fd >= 0) {
LOGM(Log::ERR, "FIXME: parse icc profile");
r->error(WP_COLOR_MANAGER_V1_ERROR_UNSUPPORTED_FEATURE, "ICC profiles are not supported");
return;
}
RESOURCE->resource()->sendReady(m_currentPreferredId); RESOURCE->resource()->sendReady(m_currentPreferredId);
}); });
@ -429,7 +423,7 @@ CColorManagementIccCreator::CColorManagementIccCreator(SP<CWpImageDescriptionCre
LOGM(Log::TRACE, "Create image description from icc for id {}", id); LOGM(Log::TRACE, "Create image description from icc for id {}", id);
// FIXME actually check completeness // FIXME actually check completeness
if (m_settings.icc.fd < 0 || !m_settings.icc.length) { if (m_icc.fd < 0 || !m_icc.length) {
r->error(WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INCOMPLETE_SET, "Missing required settings"); r->error(WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INCOMPLETE_SET, "Missing required settings");
return; return;
} }
@ -443,10 +437,10 @@ CColorManagementIccCreator::CColorManagementIccCreator(SP<CWpImageDescriptionCre
return; return;
} }
LOGM(Log::ERR, "FIXME: Parse icc file {}({},{}) for id {}", m_settings.icc.fd, m_settings.icc.offset, m_settings.icc.length, id); LOGM(Log::ERR, "FIXME: Parse icc file {}({},{}) for id {}", m_icc.fd, m_icc.offset, m_icc.length, id);
// FIXME actually check support // FIXME actually check support
if (m_settings.icc.fd < 0 || !m_settings.icc.length) { if (m_icc.fd < 0 || !m_icc.length) {
RESOURCE->resource()->sendFailed(WP_IMAGE_DESCRIPTION_V1_CAUSE_UNSUPPORTED, "unsupported"); RESOURCE->resource()->sendFailed(WP_IMAGE_DESCRIPTION_V1_CAUSE_UNSUPPORTED, "unsupported");
return; return;
} }
@ -459,9 +453,9 @@ CColorManagementIccCreator::CColorManagementIccCreator(SP<CWpImageDescriptionCre
}); });
m_resource->setSetIccFile([this](CWpImageDescriptionCreatorIccV1* r, int fd, uint32_t offset, uint32_t length) { m_resource->setSetIccFile([this](CWpImageDescriptionCreatorIccV1* r, int fd, uint32_t offset, uint32_t length) {
m_settings.icc.fd = fd; m_icc.fd = fd;
m_settings.icc.offset = offset; m_icc.offset = offset;
m_settings.icc.length = length; m_icc.length = length;
}); });
} }
@ -731,8 +725,9 @@ CColorManagementImageDescriptionInfo::CColorManagementImageDescriptionInfo(SP<CW
const auto toProto = [](float value) { return sc<int32_t>(std::round(value * PRIMARIES_SCALE)); }; const auto toProto = [](float value) { return sc<int32_t>(std::round(value * PRIMARIES_SCALE)); };
if (m_settings.icc.fd >= 0) // FIXME:
m_resource->sendIccFile(m_settings.icc.fd, m_settings.icc.length); // if (m_icc.fd >= 0)
// m_resource->sendIccFile(m_icc.fd, m_icc.length);
// send preferred client paramateres // send preferred client paramateres
m_resource->sendPrimaries(toProto(m_settings.primaries.red.x), toProto(m_settings.primaries.red.y), toProto(m_settings.primaries.green.x), m_resource->sendPrimaries(toProto(m_settings.primaries.red.x), toProto(m_settings.primaries.red.y), toProto(m_settings.primaries.green.x),

View file

@ -7,7 +7,7 @@
#include "../helpers/Monitor.hpp" #include "../helpers/Monitor.hpp"
#include "core/Compositor.hpp" #include "core/Compositor.hpp"
#include "color-management-v1.hpp" #include "color-management-v1.hpp"
#include "types/ColorManagement.hpp" #include "../helpers/cm/ColorManagement.hpp"
class CColorManager; class CColorManager;
class CColorManagementOutput; class CColorManagementOutput;
@ -109,6 +109,14 @@ class CColorManagementIccCreator {
WP<CColorManagementIccCreator> m_self; WP<CColorManagementIccCreator> m_self;
NColorManagement::SImageDescription m_settings; NColorManagement::SImageDescription m_settings;
struct SIccFile {
int fd = -1;
uint32_t length = 0;
uint32_t offset = 0;
bool operator==(const SIccFile& i2) const {
return fd == i2.fd;
}
} m_icc;
private: private:
SP<CWpImageDescriptionCreatorIccV1> m_resource; SP<CWpImageDescriptionCreatorIccV1> m_resource;

View file

@ -56,6 +56,12 @@ CGammaControl::CGammaControl(SP<CZwlrGammaControlV1> resource_, wl_resource* out
LOGM(Log::DEBUG, "setGamma for {}", m_monitor->m_name); LOGM(Log::DEBUG, "setGamma for {}", m_monitor->m_name);
if UNLIKELY (m_monitor->gammaRampsInUse()) {
LOGM(Log::ERR, "Monitor has gamma ramps in use (ICC?)");
m_resource->sendFailed();
return;
}
// TODO: make CFileDescriptor getflags use F_GETFL // TODO: make CFileDescriptor getflags use F_GETFL
int fdFlags = fcntl(gammaFd.get(), F_GETFL, 0); int fdFlags = fcntl(gammaFd.get(), F_GETFL, 0);
if UNLIKELY (fdFlags < 0) { if UNLIKELY (fdFlags < 0) {

View file

@ -13,10 +13,7 @@ CScreencopyClient::CScreencopyClient(SP<CZwlrScreencopyManagerV1> resource_) : m
return; return;
m_resource->setDestroy([this](CZwlrScreencopyManagerV1* pMgr) { PROTO::screencopy->destroyResource(this); }); m_resource->setDestroy([this](CZwlrScreencopyManagerV1* pMgr) { PROTO::screencopy->destroyResource(this); });
m_resource->setOnDestroy([this](CZwlrScreencopyManagerV1* pMgr) { m_resource->setOnDestroy([this](CZwlrScreencopyManagerV1* pMgr) { PROTO::screencopy->destroyResource(this); });
Screenshare::mgr()->destroyClientSessions(m_savedClient);
PROTO::screencopy->destroyResource(this);
});
m_resource->setCaptureOutput( m_resource->setCaptureOutput(
[this](CZwlrScreencopyManagerV1* pMgr, uint32_t frame, int32_t overlayCursor, wl_resource* output) { captureOutput(frame, overlayCursor, output, {}); }); [this](CZwlrScreencopyManagerV1* pMgr, uint32_t frame, int32_t overlayCursor, wl_resource* output) { captureOutput(frame, overlayCursor, output, {}); });
m_resource->setCaptureOutputRegion([this](CZwlrScreencopyManagerV1* pMgr, uint32_t frame, int32_t overlayCursor, wl_resource* output, int32_t x, int32_t y, int32_t w, m_resource->setCaptureOutputRegion([this](CZwlrScreencopyManagerV1* pMgr, uint32_t frame, int32_t overlayCursor, wl_resource* output, int32_t x, int32_t y, int32_t w,
@ -25,10 +22,6 @@ CScreencopyClient::CScreencopyClient(SP<CZwlrScreencopyManagerV1> resource_) : m
m_savedClient = m_resource->client(); m_savedClient = m_resource->client();
} }
CScreencopyClient::~CScreencopyClient() {
Screenshare::mgr()->destroyClientSessions(m_savedClient);
}
void CScreencopyClient::captureOutput(uint32_t frame, int32_t overlayCursor_, wl_resource* output, CBox box) { void CScreencopyClient::captureOutput(uint32_t frame, int32_t overlayCursor_, wl_resource* output, CBox box) {
const auto PMONITORRES = CWLOutputResource::fromResource(output); const auto PMONITORRES = CWLOutputResource::fromResource(output);
if (!PMONITORRES || !PMONITORRES->m_monitor) { if (!PMONITORRES || !PMONITORRES->m_monitor) {
@ -69,11 +62,6 @@ CScreencopyFrame::CScreencopyFrame(SP<CZwlrScreencopyFrameV1> resource_, WP<CScr
m_resource->setCopy([this](CZwlrScreencopyFrameV1* pFrame, wl_resource* res) { shareFrame(pFrame, res, false); }); m_resource->setCopy([this](CZwlrScreencopyFrameV1* pFrame, wl_resource* res) { shareFrame(pFrame, res, false); });
m_resource->setCopyWithDamage([this](CZwlrScreencopyFrameV1* pFrame, wl_resource* res) { shareFrame(pFrame, res, true); }); m_resource->setCopyWithDamage([this](CZwlrScreencopyFrameV1* pFrame, wl_resource* res) { shareFrame(pFrame, res, true); });
m_listeners.stopped = m_session->m_events.stopped.listen([this]() {
if (good())
m_resource->sendFailed();
});
m_frame = m_session->nextFrame(overlayCursor); m_frame = m_session->nextFrame(overlayCursor);
auto formats = m_session->allowedFormats(); auto formats = m_session->allowedFormats();
@ -87,6 +75,13 @@ CScreencopyFrame::CScreencopyFrame(SP<CZwlrScreencopyFrameV1> resource_, WP<CScr
auto bufSize = m_frame->bufferSize(); auto bufSize = m_frame->bufferSize();
const auto PSHMINFO = NFormatUtils::getPixelFormatFromDRM(format); const auto PSHMINFO = NFormatUtils::getPixelFormatFromDRM(format);
if (!PSHMINFO) {
LOGM(Log::ERR, "No pixel format for drm format");
m_resource->sendFailed();
return;
}
const auto stride = NFormatUtils::minStride(PSHMINFO, bufSize.x); const auto stride = NFormatUtils::minStride(PSHMINFO, bufSize.x);
m_resource->sendBuffer(NFormatUtils::drmToShm(format), bufSize.x, bufSize.y, stride); m_resource->sendBuffer(NFormatUtils::drmToShm(format), bufSize.x, bufSize.y, stride);
@ -104,6 +99,12 @@ void CScreencopyFrame::shareFrame(CZwlrScreencopyFrameV1* pFrame, wl_resource* b
return; return;
} }
if UNLIKELY (m_session.expired() || !m_session->monitor()) {
LOGM(Log::ERR, "Session stopped for frame {:x}", (uintptr_t)this);
m_resource->sendFailed();
return;
}
if UNLIKELY (m_buffer) { if UNLIKELY (m_buffer) {
LOGM(Log::ERR, "Buffer used in {:x}", (uintptr_t)this); LOGM(Log::ERR, "Buffer used in {:x}", (uintptr_t)this);
m_resource->error(ZWLR_SCREENCOPY_FRAME_V1_ERROR_ALREADY_USED, "frame already used"); m_resource->error(ZWLR_SCREENCOPY_FRAME_V1_ERROR_ALREADY_USED, "frame already used");

View file

@ -19,7 +19,6 @@ namespace Screenshare {
class CScreencopyClient { class CScreencopyClient {
public: public:
CScreencopyClient(SP<CZwlrScreencopyManagerV1> resource_); CScreencopyClient(SP<CZwlrScreencopyManagerV1> resource_);
~CScreencopyClient();
bool good(); bool good();
@ -52,10 +51,7 @@ class CScreencopyFrame {
Time::steady_tp m_timestamp; Time::steady_tp m_timestamp;
bool m_overlayCursor = true; bool m_overlayCursor = true;
struct { //
CHyprSignalListener stopped;
} m_listeners;
void shareFrame(CZwlrScreencopyFrameV1* pFrame, wl_resource* buffer, bool withDamage); void shareFrame(CZwlrScreencopyFrameV1* pFrame, wl_resource* buffer, bool withDamage);
friend class CScreencopyProtocol; friend class CScreencopyProtocol;

View file

@ -12,11 +12,11 @@ CSinglePixelBuffer::CSinglePixelBuffer(uint32_t id, wl_client* client, CHyprColo
m_opaque = col_.a >= 1.F; m_opaque = col_.a >= 1.F;
m_texture = makeShared<CTexture>(DRM_FORMAT_ARGB8888, rc<uint8_t*>(&m_color), 4, Vector2D{1, 1}); m_texture = g_pHyprRenderer->createTexture(DRM_FORMAT_ARGB8888, rc<uint8_t*>(&m_color), 4, Vector2D{1, 1});
m_resource = CWLBufferResource::create(makeShared<CWlBuffer>(client, 1, id)); m_resource = CWLBufferResource::create(makeShared<CWlBuffer>(client, 1, id));
m_success = m_texture->m_texID; m_success = m_texture->ok();
size = {1, 1}; size = {1, 1};

View file

@ -13,10 +13,7 @@ CToplevelExportClient::CToplevelExportClient(SP<CHyprlandToplevelExportManagerV1
if UNLIKELY (!good()) if UNLIKELY (!good())
return; return;
m_resource->setOnDestroy([this](CHyprlandToplevelExportManagerV1* pMgr) { m_resource->setOnDestroy([this](CHyprlandToplevelExportManagerV1* pMgr) { PROTO::toplevelExport->destroyResource(this); });
Screenshare::mgr()->destroyClientSessions(m_savedClient);
PROTO::toplevelExport->destroyResource(this);
});
m_resource->setDestroy([this](CHyprlandToplevelExportManagerV1* pMgr) { PROTO::toplevelExport->destroyResource(this); }); m_resource->setDestroy([this](CHyprlandToplevelExportManagerV1* pMgr) { PROTO::toplevelExport->destroyResource(this); });
m_resource->setCaptureToplevel([this](CHyprlandToplevelExportManagerV1* pMgr, uint32_t frame, int32_t overlayCursor, uint32_t handle) { m_resource->setCaptureToplevel([this](CHyprlandToplevelExportManagerV1* pMgr, uint32_t frame, int32_t overlayCursor, uint32_t handle) {
captureToplevel(frame, overlayCursor, g_pCompositor->getWindowFromHandle(handle)); captureToplevel(frame, overlayCursor, g_pCompositor->getWindowFromHandle(handle));
@ -28,10 +25,6 @@ CToplevelExportClient::CToplevelExportClient(SP<CHyprlandToplevelExportManagerV1
m_savedClient = m_resource->client(); m_savedClient = m_resource->client();
} }
CToplevelExportClient::~CToplevelExportClient() {
Screenshare::mgr()->destroyClientSessions(m_savedClient);
}
void CToplevelExportClient::captureToplevel(uint32_t frame, int32_t overlayCursor_, PHLWINDOW handle) { void CToplevelExportClient::captureToplevel(uint32_t frame, int32_t overlayCursor_, PHLWINDOW handle) {
auto session = Screenshare::mgr()->getManagedSession(m_resource->client(), handle); auto session = Screenshare::mgr()->getManagedSession(m_resource->client(), handle);
@ -63,11 +56,6 @@ CToplevelExportFrame::CToplevelExportFrame(SP<CHyprlandToplevelExportFrameV1> re
m_resource->setDestroy([this](CHyprlandToplevelExportFrameV1* pFrame) { PROTO::toplevelExport->destroyResource(this); }); m_resource->setDestroy([this](CHyprlandToplevelExportFrameV1* pFrame) { PROTO::toplevelExport->destroyResource(this); });
m_resource->setCopy([this](CHyprlandToplevelExportFrameV1* pFrame, wl_resource* res, int32_t ignoreDamage) { shareFrame(res, !!ignoreDamage); }); m_resource->setCopy([this](CHyprlandToplevelExportFrameV1* pFrame, wl_resource* res, int32_t ignoreDamage) { shareFrame(res, !!ignoreDamage); });
m_listeners.stopped = m_session->m_events.stopped.listen([this]() {
if (good())
m_resource->sendFailed();
});
m_frame = m_session->nextFrame(overlayCursor); m_frame = m_session->nextFrame(overlayCursor);
auto formats = m_session->allowedFormats(); auto formats = m_session->allowedFormats();
@ -100,6 +88,12 @@ void CToplevelExportFrame::shareFrame(wl_resource* buffer, bool ignoreDamage) {
return; return;
} }
if UNLIKELY (m_session.expired() || !m_session->monitor()) {
LOGM(Log::ERR, "Session stopped for frame {:x}", (uintptr_t)this);
m_resource->sendFailed();
return;
}
if UNLIKELY (m_buffer) { if UNLIKELY (m_buffer) {
LOGM(Log::ERR, "Buffer used in {:x}", (uintptr_t)this); LOGM(Log::ERR, "Buffer used in {:x}", (uintptr_t)this);
m_resource->error(HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_ALREADY_USED, "frame already used"); m_resource->error(HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_ALREADY_USED, "frame already used");

View file

@ -18,7 +18,6 @@ namespace Screenshare {
class CToplevelExportClient { class CToplevelExportClient {
public: public:
CToplevelExportClient(SP<CHyprlandToplevelExportManagerV1> resource_); CToplevelExportClient(SP<CHyprlandToplevelExportManagerV1> resource_);
~CToplevelExportClient();
bool good(); bool good();
@ -50,10 +49,7 @@ class CToplevelExportFrame {
CHLBufferReference m_buffer; CHLBufferReference m_buffer;
Time::steady_tp m_timestamp; Time::steady_tp m_timestamp;
struct { //
CHyprSignalListener stopped;
} m_listeners;
void shareFrame(wl_resource* buffer, bool ignoreDamage); void shareFrame(wl_resource* buffer, bool ignoreDamage);
friend class CToplevelExportProtocol; friend class CToplevelExportProtocol;

View file

@ -20,7 +20,7 @@
#include "../../helpers/math/Math.hpp" #include "../../helpers/math/Math.hpp"
#include "../../helpers/time/Time.hpp" #include "../../helpers/time/Time.hpp"
#include "../types/Buffer.hpp" #include "../types/Buffer.hpp"
#include "../types/ColorManagement.hpp" #include "../../helpers/cm/ColorManagement.hpp"
#include "../types/SurfaceRole.hpp" #include "../types/SurfaceRole.hpp"
#include "../types/SurfaceState.hpp" #include "../types/SurfaceState.hpp"

Some files were not shown because too many files have changed in this diff Show more