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")
find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
find_package(glslang CONFIG REQUIRED)
set(AQUAMARINE_MINIMUM_VERSION 0.9.3)
set(HYPRLANG_MINIMUM_VERSION 0.6.7)
@ -266,7 +267,8 @@ pkg_check_modules(
gbm
gio-2.0
re2
muparser)
muparser
lcms2)
find_package(hyprwayland-scanner 0.3.10 REQUIRED)
@ -478,9 +480,9 @@ function(protocolWayland)
endfunction()
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()
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()
pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4)

View file

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

18
flake.lock generated
View file

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

206
flake.nix
View file

@ -88,108 +88,122 @@
};
};
outputs = inputs @ {
self,
nixpkgs,
systems,
...
}: let
inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
});
pkgsCrossFor = eachSystem (system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
});
pkgsDebugFor = eachSystem (system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-debug
];
});
pkgsDebugCrossFor = eachSystem (system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-debug
];
});
in {
overlays = import ./nix/overlays.nix {inherit self lib inputs;};
outputs =
inputs@{
self,
nixpkgs,
systems,
...
}:
let
inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (
system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
}
);
pkgsCrossFor = eachSystem (
system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
}
);
pkgsDebugFor = eachSystem (
system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-debug
];
}
);
pkgsDebugCrossFor = eachSystem (
system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-debug
];
}
);
in
{
overlays = import ./nix/overlays.nix { inherit self lib inputs; };
checks = eachSystem (system:
(lib.filterAttrs
(n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n))
self.packages.${system})
// {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
hyprland-treewide-formatter = {
enable = true;
entry = "${self.formatter.${system}}/bin/hyprland-treewide-formatter";
pass_filenames = false;
excludes = ["subprojects"];
always_run = true;
checks = eachSystem (
system:
(lib.filterAttrs (
n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n)
) self.packages.${system})
// {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
hyprland-treewide-formatter = {
enable = true;
entry = "${self.formatter.${system}}/bin/hyprland-treewide-formatter";
pass_filenames = false;
excludes = [ "subprojects" ];
always_run = true;
};
};
};
};
}
// (import ./nix/tests inputs pkgsFor.${system}));
}
// (import ./nix/tests inputs pkgsFor.${system})
);
packages = eachSystem (system: {
default = self.packages.${system}.hyprland;
inherit
(pkgsFor.${system})
# hyprland-packages
hyprland
hyprland-unwrapped
hyprland-with-tests
# hyprland-extras
xdg-desktop-portal-hyprland
;
inherit (pkgsDebugFor.${system}) hyprland-debug;
hyprland-cross = (pkgsCrossFor.${system} "aarch64-linux").hyprland;
hyprland-debug-cross = (pkgsDebugCrossFor.${system} "aarch64-linux").hyprland-debug;
});
packages = eachSystem (system: {
default = self.packages.${system}.hyprland;
inherit (pkgsFor.${system})
# hyprland-packages
hyprland
hyprland-unwrapped
hyprland-with-tests
# hyprland-extras
xdg-desktop-portal-hyprland
;
inherit (pkgsDebugFor.${system}) hyprland-debug;
hyprland-cross = (pkgsCrossFor.${system} "aarch64-linux").hyprland;
hyprland-debug-cross = (pkgsDebugCrossFor.${system} "aarch64-linux").hyprland-debug;
});
devShells = eachSystem (system: {
default =
pkgsFor.${system}.mkShell.override {
inherit (self.packages.${system}.default) stdenv;
} {
name = "hyprland-shell";
hardeningDisable = ["fortify"];
inputsFrom = [pkgsFor.${system}.hyprland];
packages = [pkgsFor.${system}.clang-tools];
inherit (self.checks.${system}.pre-commit-check) shellHook;
};
});
devShells = eachSystem (system: {
default =
pkgsFor.${system}.mkShell.override
{
inherit (self.packages.${system}.default) stdenv;
}
{
name = "hyprland-shell";
hardeningDisable = [ "fortify" ];
inputsFrom = [ pkgsFor.${system}.hyprland ];
packages = [ pkgsFor.${system}.clang-tools ];
inherit (self.checks.${system}.pre-commit-check) shellHook;
};
});
formatter = eachSystem (system: pkgsFor.${system}.callPackage ./nix/formatter.nix {});
formatter = eachSystem (system: pkgsFor.${system}.callPackage ./nix/formatter.nix { });
nixosModules.default = import ./nix/module.nix inputs;
homeManagerModules.default = import ./nix/hm-module.nix self;
nixosModules.default = import ./nix/module.nix inputs;
homeManagerModules.default = import ./nix/hm-module.nix self;
# Hydra build jobs
# Recent versions of Hydra can aggregate jobsets from 'hydraJobs' instead of a release.nix
# or similar. Remember to filter large or incompatible attributes here. More eval jobs can
# be added by merging, e.g., self.packages // self.devShells.
hydraJobs = self.packages;
};
# Hydra build jobs
# Recent versions of Hydra can aggregate jobsets from 'hydraJobs' instead of a release.nix
# or similar. Remember to filter large or incompatible attributes here. More eval jobs can
# be added by merging, e.g., self.packages // self.devShells.
hydraJobs = self.packages;
};
}

View file

@ -228,23 +228,23 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE] = {0};
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) {
if (errno == EWOULDBLOCK)
log("Hyprland IPC didn't respond in time\n");
log("Couldn't read (6)");
return 6;
}
reply += std::string(buffer, sizeWritten);
while (sizeWritten == BUFFER_SIZE) {
// 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);
if (sizeWritten < 0) {
if (errno == EWOULDBLOCK)
log("Hyprland IPC didn't respond in time\n");
log("Couldn't read (6)");
return 6;
}
if (sizeWritten == 0) {
// server closed connection, we're done
break;
}
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)
find_package(glaze 7.0.0 QUIET)
find_package(glaze 6.0.1 QUIET)
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")
include(FetchContent)
FetchContent_Declare(

View file

@ -131,9 +131,18 @@ bool CPluginManager::createSafeDirectory(const std::string& path) {
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) {
const auto HLVER = getHyprlandVersion();
if (!validArg(url) || !validArg(rev)) {
std::println(stderr, "\n{}", failureString("url or rev invalid"));
return false;
}
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"));
return false;
@ -198,7 +207,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
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")) {
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));
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)) {
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")) {
@ -648,7 +657,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
const auto HLVER = getHyprlandVersion(false);
CProgressBar progress;
progress.m_iMaxSteps = REPOS.size() * 2 + 2;
progress.m_iMaxSteps = (REPOS.size() * 2) + 2;
progress.m_iSteps = 0;
progress.m_szCurrentMessage = "Updating repositories";
progress.print();
@ -669,7 +678,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
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")) {
std::println("{}", failureString("could not clone repo: shell returned: {}", ret));
@ -679,7 +688,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
if (!repo.rev.empty()) {
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) {
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:
std::string headerError(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);

View file

@ -10,7 +10,9 @@
#include <src/managers/PointerManager.hpp>
#include <src/managers/input/trackpad/TrackpadGestures.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/view/LayerSurface.hpp>
#include <src/Compositor.hpp>
#include <src/desktop/state/FocusState.hpp>
#include <src/layout/LayoutManager.hpp>
@ -270,32 +272,67 @@ static SDispatchResult keybind(std::string in) {
return {};
}
static Desktop::Rule::CWindowRuleEffectContainer::storageType ruleIDX = 0;
static Desktop::Rule::CWindowRuleEffectContainer::storageType windowRuleIDX = 0;
//
static SDispatchResult addRule(std::string in) {
ruleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
static SDispatchResult addWindowRule(std::string in) {
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 {};
}
static SDispatchResult checkRule(std::string in) {
static SDispatchResult checkWindowRule(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW)
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"};
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 {};
}
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) {
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:click", ::click);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_rule", ::addRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_rule", ::checkRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_window_rule", ::addWindowRule);
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);
// init mouse

View file

@ -39,6 +39,16 @@ namespace Colors {
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) \
do { \
const auto& RESULT = expr; \

View file

@ -118,6 +118,33 @@ static bool test() {
Tests::killAllWindows();
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;
}

View file

@ -64,16 +64,16 @@ static void test13349() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,547");
EXPECT_CONTAINS(str, "size: 931,511");
EXPECT_CONTAINS(str, "at: 497,22");
EXPECT_CONTAINS(str, "size: 456,1036");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 967,547");
EXPECT_CONTAINS(str, "size: 931,511");
EXPECT_CONTAINS(str, "at: 967,22");
EXPECT_CONTAINS(str, "size: 456,1036");
}
// clean up
@ -81,6 +81,152 @@ static void test13349() {
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() {
NLog::log("{}Testing Dwindle layout", Colors::GREEN);
@ -91,6 +237,12 @@ static bool test() {
NLog::log("{}Testing #13349", Colors::GREEN);
test13349();
NLog::log("{}Testing splits", Colors::GREEN);
testSplit();
NLog::log("{}Testing rotatesplit", Colors::GREEN);
testRotatesplit();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
getFromSocket("/dispatch workspace 1");

View file

@ -127,6 +127,34 @@ static bool test() {
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);
OK(getFromSocket("/keyword group:auto_group false"));
@ -173,6 +201,99 @@ static bool test() {
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
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;
}

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();
}
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() {
NLog::log("{}Testing Master layout", Colors::GREEN);
@ -108,6 +169,9 @@ static bool test() {
NLog::log("{}Testing `focusmaster previous` layoutmsg", Colors::GREEN);
focusMasterPrevious();
NLog::log("{}Testing fs behavior", Colors::GREEN);
testFsBehavior();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
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");
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"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
@ -566,6 +566,98 @@ static bool testWindowRuleFocusOnActivate() {
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() {
NLog::log("{}Testing windows", Colors::GREEN);
@ -994,7 +1086,7 @@ static bool test() {
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect"));
@ -1002,12 +1094,12 @@ static bool test() {
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty"));
@ -1016,7 +1108,7 @@ static bool test() {
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
@ -1028,6 +1120,8 @@ static bool test() {
testGroupFallbackFocus();
testInitialFloatSize();
testWindowRuleFocusOnActivate();
testPinnedWorkspacesValid();
testWindowRuleWorkspaceEmpty();
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));

View file

@ -255,6 +255,144 @@ static void testMultimonBAF() {
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() {
NLog::log("{}Testing workspaces", Colors::GREEN);
@ -594,13 +732,14 @@ static bool test() {
Tests::killAllWindows();
testMultimonBAF();
testMultimonFocus();
// destroy the headless output
OK(getFromSocket("/output remove HEADLESS-3"));
testSpecialWorkspaceFullscreen();
testAsymmetricGaps();
testDynamicWsEffects();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);

View file

@ -3,6 +3,7 @@
#include <cerrno>
#include <thread>
#include <print>
#include <fstream>
#include "../shared.hpp"
#include "../hyprctlCompat.hpp"
@ -39,6 +40,38 @@ CUniquePointer<CProcess> Tests::spawnKitty(const std::string& class_, const std:
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) {
errno = 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) {
CProcess proc("/bin/sh", {"-c", cmd});
@ -105,3 +170,14 @@ std::string Tests::execAndGet(const std::string& cmd) {
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
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> spawnLayerKitty(const std::string& namespace_ = "", const std::vector<std::string> args = {});
bool processAlive(pid_t pid);
int windowCount();
int countOccurrences(const std::string& in, const std::string& what);
bool killAllWindows();
void waitUntilWindowsN(int n);
int layerCount();
bool killAllLayers();
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
}
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
misc {
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, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, togglesplit, # dwindle
bind = $mainMod, J, layoutmsg, togglesplit, # dwindle
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l

View file

@ -12,6 +12,7 @@
epoll-shim,
git,
glaze-hyprland,
glslang,
gtest,
hyprcursor,
hyprgraphics,
@ -21,6 +22,7 @@
hyprutils,
hyprwayland-scanner,
hyprwire,
lcms2,
libGL,
libdrm,
libexecinfo,
@ -64,8 +66,20 @@
inherit (builtins) foldl' readFile;
inherit (lib.asserts) assertMsg;
inherit (lib.attrsets) mapAttrsToList;
inherit (lib.lists) flatten concatLists optional optionals;
inherit (lib.strings) makeBinPath optionalString cmakeBool trim;
inherit
(lib.lists)
flatten
concatLists
optional
optionals
;
inherit
(lib.strings)
makeBinPath
optionalString
cmakeBool
trim
;
fs = lib.fileset;
adapters = flatten [
@ -77,7 +91,8 @@
in
assert assertMsg (!nvidiaPatches) "The option `nvidiaPatches` 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 (!withHyprtester) "The option `withHyprtester` has been removed. Hyprtester is always built now.";
customStdenv.mkDerivation (finalAttrs: {
@ -90,24 +105,29 @@ in
fs.intersection
# allows non-flake builds to only include files tracked by git
(fs.gitTracked ../.)
(fs.unions (flatten [
../assets/hyprland-portals.conf
../assets/install
../hyprctl
../hyprland.pc.in
../hyprpm
../LICENSE
../protocols
../src
../start
../systemd
../VERSION
(fs.fileFilter (file: file.hasExt "1") ../docs)
(fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example)
(fs.fileFilter (file: file.hasExt "sh") ../scripts)
(fs.fileFilter (file: file.name == "CMakeLists.txt") ../.)
(optional withTests [../tests ../hyprtester])
]));
(
fs.unions (flatten [
../assets/hyprland-portals.conf
../assets/install
../hyprctl
../hyprland.pc.in
../hyprpm
../LICENSE
../protocols
../src
../start
../systemd
../VERSION
(fs.fileFilter (file: file.hasExt "1") ../docs)
(fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example)
(fs.fileFilter (file: file.hasExt "sh") ../scripts)
(fs.fileFilter (file: file.name == "CMakeLists.txt") ../.)
(optional withTests [
../tests
../hyprtester
])
])
);
};
postPatch = ''
@ -123,7 +143,10 @@ in
GIT_COMMITS = revCount;
GIT_COMMIT_DATE = date;
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")}";
};
@ -151,6 +174,7 @@ in
cairo
git
glaze-hyprland
glslang
gtest
hyprcursor
hyprgraphics
@ -158,6 +182,7 @@ in
hyprlang
hyprutils
hyprwire
lcms2
libdrm
libgbm
libGL
@ -218,12 +243,14 @@ in
postInstall = ''
${optionalString wrapRuntimeDeps ''
wrapProgram $out/bin/Hyprland \
--suffix PATH : ${makeBinPath [
binutils
hyprland-guiutils
pciutils
pkgconf
]}
--suffix PATH : ${
makeBinPath [
binutils
hyprland-guiutils
pciutils
pkgconf
]
}
''}
${optionalString withTests ''

View file

@ -2,7 +2,7 @@
writeShellApplication,
deadnix,
statix,
alejandra,
nixfmt,
llvmPackages_19,
fd,
}:
@ -11,7 +11,7 @@ writeShellApplication {
runtimeInputs = [
deadnix
statix
alejandra
nixfmt
llvmPackages_19.clang-tools
fd
];
@ -24,14 +24,14 @@ writeShellApplication {
nix_format() {
if [ "$*" = 0 ]; then
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
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
statix fix -- "$1"
deadnix -e "$1"
alejandra "$1"
nixfmt "$1"
fi
}

View file

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

View file

@ -1,4 +1,5 @@
lib: let
lib:
let
inherit (lib)
attrNames
filterAttrs
@ -17,7 +18,7 @@ lib: let
This function takes a nested attribute set and converts it into Hyprland-compatible
configuration syntax, supporting top, bottom, and regular command sections.
Commands are flattened using the `flattenAttrs` function, and attributes are formatted as
`key = value` pairs. Lists are expanded as duplicate keys to match Hyprland's expected format.
@ -81,44 +82,51 @@ lib: let
:::
*/
toHyprlang = {
topCommandsPrefixes ? ["$" "bezier"],
bottomCommandsPrefixes ? [],
}: attrs: let
toHyprlang' = attrs: let
# Specially configured `toKeyValue` generator with support for duplicate keys
# and a legible key-value separator.
mkCommands = generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault {} " = ";
listsAsDuplicateKeys = true;
indent = ""; # No indent, since we don't have nesting
};
toHyprlang =
{
topCommandsPrefixes ? [
"$"
"bezier"
],
bottomCommandsPrefixes ? [ ],
}:
attrs:
let
toHyprlang' =
attrs:
let
# Specially configured `toKeyValue` generator with support for duplicate keys
# and a legible key-value separator.
mkCommands = generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault { } " = ";
listsAsDuplicateKeys = true;
indent = ""; # No indent, since we don't have nesting
};
# Flatten the attrset, combining keys in a "path" like `"a:b:c" = "x"`.
# Uses `flattenAttrs` with a colon separator.
commands = flattenAttrs (p: k: "${p}:${k}") attrs;
# Flatten the attrset, combining keys in a "path" like `"a:b:c" = "x"`.
# Uses `flattenAttrs` with a colon separator.
commands = flattenAttrs (p: k: "${p}:${k}") attrs;
# General filtering function to check if a key starts with any prefix in a given list.
filterCommands = list: n:
foldl (acc: prefix: acc || hasPrefix prefix n) false list;
# General filtering function to check if a key starts with any prefix in a given list.
filterCommands = list: n: foldl (acc: prefix: acc || hasPrefix prefix n) false list;
# Partition keys into top commands and the rest
result = partition (filterCommands topCommandsPrefixes) (attrNames commands);
topCommands = filterAttrs (n: _: builtins.elem n result.right) commands;
remainingCommands = removeAttrs commands result.right;
# Partition keys into top commands and the rest
result = partition (filterCommands topCommandsPrefixes) (attrNames commands);
topCommands = filterAttrs (n: _: builtins.elem n result.right) commands;
remainingCommands = removeAttrs commands result.right;
# Partition remaining commands into bottom commands and regular commands
result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong;
bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands;
regularCommands = removeAttrs remainingCommands result2.right;
# Partition remaining commands into bottom commands and regular commands
result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong;
bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands;
regularCommands = removeAttrs remainingCommands result2.right;
in
# Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands.
concatMapStrings mkCommands [
topCommands
regularCommands
bottomCommands
];
in
# Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands.
concatMapStrings mkCommands [
topCommands
regularCommands
bottomCommands
];
in
toHyprlang' attrs;
/**
@ -131,7 +139,7 @@ lib: let
Configuration:
* `pred` - A function `(string -> string -> string)` defining how keys should be concatenated.
# Inputs
Structured function argument:
@ -139,7 +147,7 @@ lib: let
: pred (required)
: A function that determines how parent and child keys should be combined into a single key.
It takes a `prefix` (parent key) and `key` (current key) and returns the joined key.
Value:
: The nested attribute set to be flattened.
@ -174,26 +182,21 @@ lib: let
```
:::
*/
flattenAttrs = pred: attrs: let
flattenAttrs' = prefix: attrs:
builtins.foldl' (
acc: key: let
value = attrs.${key};
newKey =
if prefix == ""
then key
else pred prefix key;
in
acc
// (
if builtins.isAttrs value
then flattenAttrs' newKey value
else {"${newKey}" = value;}
)
) {} (builtins.attrNames attrs);
in
flattenAttrs =
pred: attrs:
let
flattenAttrs' =
prefix: attrs:
builtins.foldl' (
acc: key:
let
value = attrs.${key};
newKey = if prefix == "" then key else pred prefix key;
in
acc // (if builtins.isAttrs value then flattenAttrs' newKey value else { "${newKey}" = value; })
) { } (builtins.attrNames attrs);
in
flattenAttrs' "" attrs;
in
{

View file

@ -1,18 +1,21 @@
inputs: {
inputs:
{
config,
lib,
pkgs,
...
}: let
}:
let
inherit (pkgs.stdenv.hostPlatform) system;
selflib = import ./lib.nix lib;
cfg = config.programs.hyprland;
in {
in
{
options = {
programs.hyprland = {
plugins = lib.mkOption {
type = with lib.types; listOf (either package path);
default = [];
default = [ ];
description = ''
List of Hyprland plugins to use. Can either be packages or
absolute plugin paths.
@ -20,23 +23,25 @@ in {
};
settings = lib.mkOption {
type = with lib.types; let
valueType =
nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "Hyprland configuration value";
};
in
type =
with lib.types;
let
valueType =
nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "Hyprland configuration value";
};
in
valueType;
default = {};
default = { };
description = ''
Hyprland configuration written in Nix. Entries with the same key
should be written as lists. Variables' and colors' names should be
@ -92,8 +97,15 @@ in {
topPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = ["$" "bezier"];
example = ["$" "bezier" "source"];
default = [
"$"
"bezier"
];
example = [
"$"
"bezier"
"source"
];
description = ''
List of prefix of attributes to put at the top of the config.
'';
@ -101,8 +113,8 @@ in {
bottomPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = [];
example = ["source"];
default = [ ];
example = [ "source" ];
description = ''
List of prefix of attributes to put at the bottom of the config.
'';
@ -117,35 +129,36 @@ in {
};
}
(lib.mkIf cfg.enable {
environment.etc."xdg/hypr/hyprland.conf" = let
shouldGenerate = cfg.extraConfig != "" || cfg.settings != {} || cfg.plugins != [];
environment.etc."xdg/hypr/hyprland.conf" =
let
shouldGenerate = cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ];
pluginsToHyprlang = plugins:
selflib.toHyprlang {
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
}
{
"exec-once" = let
mkEntry = entry:
if lib.types.package.check entry
then "${entry}/lib/lib${entry.pname}.so"
else entry;
hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl";
in
map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins;
};
in
lib.mkIf shouldGenerate {
text =
lib.optionalString (cfg.plugins != [])
(pluginsToHyprlang cfg.plugins)
+ lib.optionalString (cfg.settings != {})
(selflib.toHyprlang {
pluginsToHyprlang =
_plugins:
selflib.toHyprlang
{
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
}
cfg.settings)
{
"exec-once" =
let
mkEntry =
entry: if lib.types.package.check entry then "${entry}/lib/lib${entry.pname}.so" else entry;
hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl";
in
map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins;
};
in
lib.mkIf shouldGenerate {
text =
lib.optionalString (cfg.plugins != [ ]) (pluginsToHyprlang cfg.plugins)
+ lib.optionalString (cfg.settings != { }) (
selflib.toHyprlang {
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
} cfg.settings
)
+ lib.optionalString (cfg.extraConfig != "") cfg.extraConfig;
};
})

View file

@ -2,20 +2,27 @@
self,
lib,
inputs,
}: let
mkDate = longDate: (lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
}:
let
mkDate =
longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION);
in {
in
{
# Contains what a user is most likely to care about:
# Hyprland itself, XDPH and the Share Picker.
default = lib.composeManyExtensions (with self.overlays; [
hyprland-packages
hyprland-extras
]);
default = lib.composeManyExtensions (
with self.overlays;
[
hyprland-packages
hyprland-extras
]
);
# Packages for variations of Hyprland, dependencies included.
hyprland-packages = lib.composeManyExtensions [
@ -33,49 +40,45 @@ in {
self.overlays.glaze
# Hyprland packages themselves
(final: _prev: let
date = mkDate (self.lastModifiedDate or "19700101");
version = "${ver}+date=${date}_${self.shortRev or "dirty"}";
in {
hyprland = final.callPackage ./default.nix {
stdenv = final.gcc15Stdenv;
commit = self.rev or "";
revCount = self.sourceInfo.revCount or "";
inherit date version;
};
hyprland-unwrapped = final.hyprland.override {wrapRuntimeDeps = false;};
(
final: _prev:
let
date = mkDate (self.lastModifiedDate or "19700101");
version = "${ver}+date=${date}_${self.shortRev or "dirty"}";
in
{
hyprland = final.callPackage ./default.nix {
stdenv = final.gcc15Stdenv;
commit = self.rev or "";
revCount = self.sourceInfo.revCount or "";
inherit date version;
};
hyprland-unwrapped = final.hyprland.override { wrapRuntimeDeps = false; };
hyprland-with-tests = final.hyprland.override {withTests = true;};
hyprland-with-tests = final.hyprland.override { withTests = true; };
hyprland-with-hyprtester =
builtins.trace ''
hyprland-with-hyprtester = builtins.trace ''
hyprland-with-hyprtester was removed. Please use the hyprland package.
Hyprtester is always built now.
''
final.hyprland;
'' final.hyprland;
# deprecated packages
hyprland-legacy-renderer =
builtins.trace ''
# deprecated packages
hyprland-legacy-renderer = builtins.trace ''
hyprland-legacy-renderer was removed. Please use the hyprland package.
Legacy renderer is no longer supported.
''
final.hyprland;
'' final.hyprland;
hyprland-nvidia =
builtins.trace ''
hyprland-nvidia = builtins.trace ''
hyprland-nvidia was removed. Please use the hyprland package.
Nvidia patches are no longer needed.
''
final.hyprland;
'' final.hyprland;
hyprland-hidpi =
builtins.trace ''
hyprland-hidpi = builtins.trace ''
hyprland-hidpi was removed. Please use the hyprland package.
For more information, refer to https://wiki.hypr.land/Configuring/XWayland.
''
final.hyprland;
})
'' final.hyprland;
}
)
];
# Debug
@ -83,10 +86,10 @@ in {
# Dependencies
self.overlays.hyprland-packages
(final: prev: {
aquamarine = prev.aquamarine.override {debug = true;};
hyprutils = prev.hyprutils.override {debug = true;};
hyprland-debug = prev.hyprland.override {debug = true;};
(_final: prev: {
aquamarine = prev.aquamarine.override { debug = true; };
hyprutils = prev.hyprutils.override { debug = true; };
hyprland-debug = prev.hyprland.override { debug = true; };
})
];
@ -100,21 +103,23 @@ in {
# this version is the one used in the git submodule, and allows us to
# fetch the source without '?submodules=1'
udis86 = final: prev: {
udis86-hyprland = prev.udis86.overrideAttrs (_self: _super: {
src = final.fetchFromGitHub {
owner = "canihavesomecoffee";
repo = "udis86";
rev = "5336633af70f3917760a6d441ff02d93477b0c86";
hash = "sha256-HifdUQPGsKQKQprByeIznvRLONdOXeolOsU5nkwIv3g=";
};
udis86-hyprland = prev.udis86.overrideAttrs (
_self: _super: {
src = final.fetchFromGitHub {
owner = "canihavesomecoffee";
repo = "udis86";
rev = "5336633af70f3917760a6d441ff02d93477b0c86";
hash = "sha256-HifdUQPGsKQKQprByeIznvRLONdOXeolOsU5nkwIv3g=";
};
patches = [];
});
patches = [ ];
}
);
};
# 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
glaze = final: prev: {
glaze = _final: prev: {
glaze-hyprland = prev.glaze.override {
enableSSL = false;
enableInterop = false;

View file

@ -1,71 +1,75 @@
inputs: pkgs: let
inputs: pkgs:
let
flake = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
hyprland = flake.hyprland-with-tests;
in {
in
{
tests = pkgs.testers.runNixOSTest {
name = "hyprland-tests";
nodes.machine = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
# Programs needed for tests
jq
kitty
wl-clipboard
xeyes
];
nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [
# Programs needed for tests
jq
kitty
wl-clipboard
xeyes
];
# Enabled by default for some reason
services.speechd.enable = false;
# Enabled by default for some reason
services.speechd.enable = false;
environment.variables = {
"AQ_TRACE" = "1";
"HYPRLAND_TRACE" = "1";
"XDG_RUNTIME_DIR" = "/tmp";
"XDG_CACHE_HOME" = "/tmp";
"KITTY_CONFIG_DIRECTORY" = "/etc/kitty";
};
environment.etc."kitty/kitty.conf".text = ''
confirm_os_window_close 0
remember_window_size no
initial_window_width 640
initial_window_height 400
'';
programs.hyprland = {
enable = true;
package = hyprland;
# We don't need portals in this test, so we don't set portalPackage
};
# Test configuration
environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf";
# Disable portals
xdg.portal.enable = pkgs.lib.mkForce false;
# Autologin root into tty
services.getty.autologinUser = "alice";
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
virtualisation = {
cores = 4;
# Might crash with less
memorySize = 8192;
resolution = {
x = 1920;
y = 1080;
environment.variables = {
"AQ_TRACE" = "1";
"HYPRLAND_TRACE" = "1";
"XDG_RUNTIME_DIR" = "/tmp";
"XDG_CACHE_HOME" = "/tmp";
"KITTY_CONFIG_DIRECTORY" = "/etc/kitty";
};
# Doesn't seem to do much, thought it would fix XWayland crashing
qemu.options = ["-vga none -device virtio-gpu-pci"];
environment.etc."kitty/kitty.conf".text = ''
confirm_os_window_close 0
remember_window_size no
initial_window_width 640
initial_window_height 400
'';
programs.hyprland = {
enable = true;
package = hyprland;
# We don't need portals in this test, so we don't set portalPackage
};
# Test configuration
environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf";
# Disable portals
xdg.portal.enable = pkgs.lib.mkForce false;
# Autologin root into tty
services.getty.autologinUser = "alice";
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
virtualisation = {
cores = 4;
# Might crash with less
memorySize = 8192;
resolution = {
x = 1920;
y = 1080;
};
# Doesn't seem to do much, thought it would fix XWayland crashing
qemu.options = [ "-vga none -device virtio-gpu-pci" ];
};
};
};
testScript = ''
# Wait for tty to be up

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
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 "#include \"./${filename}.inc\"" >> ./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/Subcompositor.hpp"
#include "desktop/view/LayerSurface.hpp"
#include "layout/space/Space.hpp"
#include "render/Renderer.hpp"
#include "xwayland/XWayland.hpp"
#include "helpers/ByteOperations.hpp"
@ -1404,6 +1405,35 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
PHLWINDOW leaderWindow = nullptr;
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) {
if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || w->isHidden() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible())
continue;
@ -1426,24 +1456,20 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
switch (dir) {
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));
}
break;
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));
}
break;
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));
}
break;
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));
}
break;
default: break;
}
@ -1799,6 +1825,9 @@ void CCompositor::swapActiveWorkspaces(PHLMONITOR pMonitorA, PHLMONITOR pMonitor
g_layoutManager->recalculateMonitor(pMonitorA);
g_layoutManager->recalculateMonitor(pMonitorB);
g_pHyprRenderer->damageMonitor(pMonitorB);
g_pHyprRenderer->damageMonitor(pMonitorA);
g_pDesktopAnimationManager->setFullscreenFadeAnimation(
PWORKSPACEB, PWORKSPACEB->m_hasFullscreenWindow ? CDesktopAnimationManager::ANIMATION_TYPE_IN : CDesktopAnimationManager::ANIMATION_TYPE_OUT);
g_pDesktopAnimationManager->setFullscreenFadeAnimation(
@ -1953,6 +1982,7 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
// move the workspace
pWorkspace->m_monitor = pMonitor;
pWorkspace->m_space->recheckWorkArea();
pWorkspace->m_events.monitorChanged.emit();
for (auto const& w : m_windows) {
@ -2008,6 +2038,7 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
pWorkspace->m_events.activeChanged.emit();
g_layoutManager->recalculateMonitor(pMonitor);
g_pHyprRenderer->damageMonitor(pMonitor);
g_pDesktopAnimationManager->startAnimation(pWorkspace, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
pWorkspace->m_visible = true;
@ -2747,6 +2778,7 @@ void CCompositor::arrangeMonitors() {
}
PROTO::xdgOutput->updateAllOutputs();
Event::bus()->m_events.monitor.layoutChanged.emit();
#ifndef NO_XWAYLAND
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() {
if (!m_aqBackend->hasSession())
return std::nullopt;

View file

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

View file

@ -1579,6 +1579,18 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_BOOL,
.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:
@ -2093,6 +2105,18 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_CHOICE,
.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

View file

@ -655,6 +655,8 @@ CConfigManager::CConfigManager() {
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: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:workspace_wraparound", Hyprlang::INT{0});
@ -797,6 +799,8 @@ CConfigManager::CConfigManager() {
registerConfigVar("render:non_shader_cm", Hyprlang::INT{3});
registerConfigVar("render:cm_sdr_eotf", {"default"});
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_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", "max_luminance", Hyprlang::INT{-1});
m_config->addSpecialConfigValue("monitorv2", "max_avg_luminance", Hyprlang::INT{-1});
m_config->addSpecialConfigValue("monitorv2", "icc", Hyprlang::STRING{""});
// windowrule v3
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)
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();
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;
}
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() {
m_rule.disabled = true;
}
@ -2369,6 +2387,9 @@ std::optional<std::string> CConfigManager::handleMonitor(const std::string& comm
} else if (ARGS[argno] == "vrr") {
parser.parseVRR(std::string(ARGS[argno + 1]));
argno++;
} else if (ARGS[argno] == "icc") {
parser.parseICC(std::string(ARGS[argno + 1]));
argno++;
} else if (ARGS[argno] == "workspace") {
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 parseSDRSaturation(const std::string& value);
bool parseVRR(const std::string& value);
bool parseICC(const std::string& value);
void setDisabled();
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) {
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";
else
return format == FORMAT_JSON ? "{\"ok\": false}" : "error";
@ -2076,8 +2081,8 @@ CHyprCtl::CHyprCtl() {
registerCommand(SHyprCtlCommand{"locked", true, getIsLocked});
registerCommand(SHyprCtlCommand{"descriptions", true, getDescriptions});
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{"reload", false, reloadRequest});
registerCommand(SHyprCtlCommand{"plugin", false, dispatchPlugin});
@ -2197,6 +2202,15 @@ std::string CHyprCtl::getReply(std::string request) {
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) {
g_pHyprRenderer->damageMonitor(m);
}
@ -2210,16 +2224,38 @@ std::string CHyprCtl::makeDynamicCall(const std::string& input) {
}
static bool successWrite(int fd, const std::string& data, bool needLog = true) {
if (write(fd, data.c_str(), data.length()) > 0)
return true;
size_t totalWritten = 0;
size_t remaining = data.length();
size_t waitsDone = 0;
constexpr const size_t MAX_WAITS = 20; // 2000µs = 2ms
if (errno == EAGAIN)
return true;
while (totalWritten < data.length()) {
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) {

View file

@ -8,7 +8,7 @@
#include "../desktop/state/FocusState.hpp"
CHyprDebugOverlay::CHyprDebugOverlay() {
m_texture = makeShared<CTexture>();
m_texture = g_pHyprRenderer->createTexture();
}
void CHyprMonitorDebugOverlay::renderData(PHLMONITOR pMonitor, float durationUs) {
@ -259,15 +259,7 @@ void CHyprDebugOverlay::draw() {
cairo_surface_flush(m_cairoSurface);
// copy the data to an OpenGL texture we have
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, PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
m_texture = g_pHyprRenderer->createTexture(m_cairoSurface);
CTexPassElement::SRenderData data;
data.tex = m_texture;

View file

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

View file

@ -28,8 +28,6 @@ CHyprNotificationOverlay::CHyprNotificationOverlay() {
g_pHyprRenderer->damageBox(m_lastDamage);
});
m_texture = makeShared<CTexture>();
}
CHyprNotificationOverlay::~CHyprNotificationOverlay() {
@ -232,16 +230,7 @@ void CHyprNotificationOverlay::draw(PHLMONITOR pMonitor) {
m_lastDamage = damage;
// copy the data to an OpenGL texture we have
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);
m_texture = g_pHyprRenderer->createTexture(m_cairoSurface);
CTexPassElement::SRenderData data;
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 = {
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>{"", "", "", "", "", ""}};
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{179 / 255.0, 255 / 255.0, 204 / 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;
Vector2D m_lastSize = Vector2D(-1, -1);
SP<CTexture> m_texture;
SP<ITexture> m_texture;
};
inline UP<CHyprNotificationOverlay> g_pHyprNotificationOverlay;

View file

@ -4,6 +4,7 @@
#include "../../view/LayerSurface.hpp"
#include "../../types/OverridableVar.hpp"
#include "../../../helpers/MiscFunctions.hpp"
#include "../../../event/EventBus.hpp"
using namespace Desktop;
using namespace Desktop::Rule;
@ -32,11 +33,38 @@ void CLayerRuleApplicator::resetProps(std::underlying_type_t<eRuleProperty> prop
UNSET(aboveLock)
UNSET(ignoreAlpha)
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) {
for (const auto& [key, effect] : rule->effects()) {
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: {
Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: BUG THIS: LAYER_RULE_EFFECT_NONE??");
break;
@ -125,4 +153,7 @@ void CLayerRuleApplicator::propertiesChanged(std::underlying_type_t<eRulePropert
applyDynamicRule(wr);
}
// for plugins
Event::bus()->m_events.layer.updateRules.emit(m_ls.lock());
}

View file

@ -1,5 +1,6 @@
#pragma once
#include "LayerRuleEffectContainer.hpp"
#include "../../DesktopTypes.hpp"
#include "../Rule.hpp"
#include "../../types/OverridableVar.hpp"
@ -21,6 +22,17 @@ namespace Desktop::Rule {
void propertiesChanged(std::underlying_type_t<eRuleProperty> props);
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 DEFINE_PROP(type, name, def) \
private: \

View file

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

View file

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

View file

@ -120,7 +120,7 @@ void CGroup::add(PHLWINDOW w) {
m_target->recalc();
}
void CGroup::remove(PHLWINDOW w) {
void CGroup::remove(PHLWINDOW w, Math::eDirection dir) {
std::optional<size_t> idx;
for (size_t i = 0; i < m_windows.size(); ++i) {
if (m_windows.at(i) == w) {
@ -156,8 +156,20 @@ void CGroup::remove(PHLWINDOW w) {
updateWindowVisibility();
// do this here: otherwise the new current is hidden and workspace rules get wrong data
if (!REMOVING_GROUP)
w->m_target->assignToSpace(m_target->space());
if (!REMOVING_GROUP) {
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) {
@ -317,6 +329,7 @@ void CGroup::swapWithNext() {
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);
m_current = idx;
updateWindowVisibility();
@ -329,6 +342,7 @@ void CGroup::swapWithLast() {
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);
m_current = idx;
updateWindowVisibility();

View file

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

View file

@ -277,7 +277,7 @@ CBox CWindow::getWindowIdealBoundingBoxIgnoreReserved() {
// fucker fucking fuck
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)) {
POS.x -= RESERVED.left();
@ -510,12 +510,6 @@ void CWindow::moveToWorkspace(PHLWORKSPACE pWorkspace) {
setAnimationsToMove();
OLDWORKSPACE->updateWindows();
OLDWORKSPACE->updateWindowData();
pWorkspace->updateWindows();
pWorkspace->updateWindowData();
g_pCompositor->updateAllWindowsAnimatedDecorationValues();
if (valid(pWorkspace)) {
@ -807,9 +801,13 @@ void CWindow::updateWindowData() {
}
void CWindow::updateWindowData(const SWorkspaceRule& workspaceRule) {
m_ruleApplicator->borderSize().matchOptional(workspaceRule.borderSize, Desktop::Types::PRIORITY_WORKSPACE_RULE);
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);
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->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->noShadow().matchOptional(workspaceRule.noShadow, Desktop::Types::PRIORITY_WORKSPACE_RULE);
}
@ -1873,11 +1871,12 @@ void CWindow::mapWindow() {
if (WORKSPACEARGS.contains("silent"))
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;
requestedWorkspaceName = PWORKSPACE->m_name;
} else {
auto result = getWorkspaceIDNameFromString(WORKSPACEARGS.join(" ", 0, workspaceSilent ? WORKSPACEARGS.size() - 1 : 0));
auto result = getWorkspaceIDNameFromString(joined);
requestedWorkspaceID = result.id;
requestedWorkspaceName = result.name;
}
@ -1949,11 +1948,13 @@ void CWindow::mapWindow() {
g_pEventManager->postEvent(SHyprIPCEvent{"openwindow", std::format("{:x},{},{},{}", m_self.lock(), PWORKSPACE->m_name, m_class, m_title)});
Event::bus()->m_events.window.openEarly.emit(m_self.lock());
if (*PAUTOGROUP // auto_group enabled
&& Desktop::focusState()->window() // focused window exists
&& 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
&& !isModal() && !(parent() && m_isFloating) && !isX11OverrideRedirect() // not a modal, floating child or X11 OR
if (*PAUTOGROUP // auto_group enabled
&& Desktop::focusState()->window() // focused window exists
&& 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
&& !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
Desktop::focusState()->window()->m_group->add(m_self.lock());

View file

@ -41,6 +41,7 @@ namespace Event {
Event<PHLWINDOW> openEarly;
Event<PHLWINDOW> destroy;
Event<PHLWINDOW> close;
Event<PHLWINDOW> kill;
Event<PHLWINDOW, Desktop::eFocusReason> active;
Event<PHLWINDOW> urgent;
Event<PHLWINDOW> title;
@ -54,6 +55,7 @@ namespace Event {
struct {
Event<PHLLS> opened;
Event<PHLLS> closed;
Event<PHLLS> updateRules;
} layer;
struct {
@ -139,4 +141,4 @@ namespace Event {
};
UP<CEventBus>& bus();
};
};

View file

@ -297,31 +297,23 @@ int NFormatUtils::minStride(const SPixelFormat* const fmt, int32_t width) {
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) {
auto n = drmGetFormatName(drm);
auto n = drmGetFormatName(drm);
if (!n)
return "unknown";
std::string name = n;
free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors)
return name;
}
std::string NFormatUtils::drmModifierName(uint64_t mod) {
auto n = drmGetFormatModifierName(mod);
auto n = drmGetFormatModifierName(mod);
if (!n)
return "unknown";
std::string name = n;
free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors)
return name;

View file

@ -53,8 +53,6 @@ namespace NFormatUtils {
bool isFormatOpaque(DRMFormat drm);
int pixelsPerBlock(const SPixelFormat* const fmt);
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 drmModifierName(uint64_t mod);
DRMFormat alphaFormat(DRMFormat prevFormat);

View file

@ -30,7 +30,7 @@
#include "../hyprerror/HyprError.hpp"
#include "../layout/LayoutManager.hpp"
#include "../i18n/Engine.hpp"
#include "../protocols/types/ColorManagement.hpp"
#include "../helpers/cm/ColorManagement.hpp"
#include "sync/SyncTimeline.hpp"
#include "time/Time.hpp"
#include "../desktop/view/LayerSurface.hpp"
@ -82,7 +82,10 @@ void CMonitor::onConnect(bool noRule) {
m_zoomAnimProgress->setValueAndWarp(0.F);
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] {
if (m_frameScheduler)
@ -290,10 +293,16 @@ void CMonitor::onConnect(bool noRule) {
if (!valid(ws))
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_pDesktopAnimationManager->startAnimation(ws, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
ws->m_lastMonitor = "";
if (RETURNING)
ws->m_lastMonitor = "";
}
}
@ -429,19 +438,24 @@ void CMonitor::onDisconnect(bool destroy) {
m_enabled = false;
m_renderingInitPassed = false;
std::vector<PHLWORKSPACE> wspToMove;
for (auto const& w : g_pCompositor->getWorkspaces()) {
if (w->m_monitor == m_self || !w->m_monitor)
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) {
if (w && w->m_lastMonitor.empty())
w->m_lastMonitor = m_name;
}
if (BACKUPMON) {
// snap cursor
g_pCompositor->warpCursorTo(BACKUPMON->m_position + BACKUPMON->m_transformedSize / 2.F, true);
// move workspaces
std::vector<PHLWORKSPACE> wspToMove;
for (auto const& w : g_pCompositor->getWorkspaces()) {
if (w->m_monitor == m_self || !w->m_monitor)
wspToMove.emplace_back(w.lock());
}
for (auto const& w : wspToMove) {
w->m_lastMonitor = m_name;
g_pCompositor->moveWorkspaceToMonitor(w, BACKUPMON);
g_pDesktopAnimationManager->startAnimation(w, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
}
@ -919,29 +933,46 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
m_supportsWideColor = RULE->supportsHDR;
m_supportsHDR = RULE->supportsHDR;
m_cmType = RULE->cmType;
switch (m_cmType) {
case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break;
case NCMType::CM_EDID: m_cmType = m_output->parsedEDID.chromaticityCoords.has_value() ? NCMType::CM_EDID : NCMType::CM_SRGB; break;
case NCMType::CM_HDR:
case NCMType::CM_HDR_EDID: m_cmType = supportsHDR() ? m_cmType : NCMType::CM_SRGB; break;
default: break;
if (RULE->iccFile.empty()) {
// only apply explicit cm settings if we have no icc file
m_cmType = RULE->cmType;
switch (m_cmType) {
case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break;
case NCMType::CM_EDID: m_cmType = m_output->parsedEDID.chromaticityCoords.has_value() ? NCMType::CM_EDID : NCMType::CM_SRGB; break;
case NCMType::CM_HDR:
case NCMType::CM_HDR_EDID: m_cmType = supportsHDR() ? m_cmType : NCMType::CM_SRGB; break;
default: break;
}
m_sdrEotf = RULE->sdrEotf;
m_sdrMinLuminance = RULE->sdrMinLuminance;
m_sdrMaxLuminance = RULE->sdrMaxLuminance;
m_minLuminance = RULE->minLuminance;
m_maxLuminance = RULE->maxLuminance;
m_maxAvgLuminance = RULE->maxAvgLuminance;
applyCMType(m_cmType, m_sdrEotf);
m_sdrSaturation = RULE->sdrSaturation;
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{});
}
}
}
m_sdrEotf = RULE->sdrEotf;
m_sdrMinLuminance = RULE->sdrMinLuminance;
m_sdrMaxLuminance = RULE->sdrMaxLuminance;
m_minLuminance = RULE->minLuminance;
m_maxLuminance = RULE->maxLuminance;
m_maxAvgLuminance = RULE->maxAvgLuminance;
applyCMType(m_cmType, m_sdrEotf);
m_sdrSaturation = RULE->sdrSaturation;
m_sdrBrightness = RULE->sdrBrightness;
Vector2D logicalSize = m_pixelSize / m_scale;
if (!*PDISABLESCALECHECKS && (logicalSize.x != std::round(logicalSize.x) || logicalSize.y != std::round(logicalSize.y))) {
// invalid scale, will produce fractional pixels.
@ -1023,6 +1054,8 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
m_damage.setSize(m_transformedSize);
updateVCGTRamps();
// Set scale for all surfaces on this monitor, needed for some clients
// but not on unsafe state to avoid crashes
if (!g_pCompositor->m_unsafeState) {
@ -1329,7 +1362,7 @@ void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bo
// move pinned windows
for (auto const& w : g_pCompositor->m_windows) {
if (w->m_workspace == POLDWORKSPACE && w->m_pinned)
w->moveToWorkspace(pWorkspace);
w->layoutTarget()->assignToSpace(pWorkspace->m_space);
}
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) {
PMWSOWNER->m_activeSpecialWorkspace.reset();
g_layoutManager->recalculateMonitor(PMWSOWNER);
g_pHyprRenderer->damageMonitor(PMWSOWNER);
g_pEventManager->postEvent(SHyprIPCEvent{"activespecial", "," + 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;
}
const Mat3x3& CMonitor::getTransformMatrix() {
return m_projMatrix;
}
const Mat3x3& CMonitor::getScaleMatrix() {
return m_projOutputMatrix;
}
void CMonitor::updateMatrix() {
m_projMatrix = Mat3x3::identity();
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_projOutputMatrix = Mat3x3::outputProjection(m_pixelSize, HYPRUTILS_TRANSFORM_NORMAL);
}
WORKSPACEID CMonitor::activeWorkspaceID() {
@ -1835,7 +1879,7 @@ uint16_t CMonitor::isDSBlocked(bool full) {
// we can't scanout shm buffers.
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;
if (!full)
return reasons;
@ -2172,8 +2216,8 @@ bool CMonitor::canNoShaderCM() {
const auto SRC_DESC_VALUE = SRC_DESC.value()->value();
if (SRC_DESC_VALUE.icc.fd >= 0 || m_imageDescription->value().icc.fd >= 0)
return false; // no ICC support
if (m_imageDescription->value().icc.present)
return false;
const auto sdrEOTF = NTransferFunction::fromConfig();
// only primaries differ
@ -2192,6 +2236,71 @@ bool CMonitor::doesNoShaderCM() {
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) {
;
}

View file

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

View file

@ -26,7 +26,10 @@ std::string NTransferFunction::toString(eTF tf) {
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 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);
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 <hyprgraphics/color/Color.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_MAX_LUMINANCE 80.0
@ -12,6 +17,8 @@
#define HDR_REF_LUMINANCE 203.0
#define HLG_MAX_LUMINANCE 1000.0
class ITexture;
namespace NColorManagement {
enum eNoShader : uint8_t {
CM_NS_DISABLE = 0,
@ -67,7 +74,6 @@ namespace NColorManagement {
using SPCPRimaries = Hyprgraphics::SPCPRimaries;
namespace NColorPrimaries {
static const auto DEFAULT_PRIMARIES = SPCPRimaries{};
static const auto BT709 = SPCPRimaries{
.red = {.x = 0.64, .y = 0.33},
@ -76,6 +82,8 @@ namespace NColorManagement {
.white = {.x = 0.3127, .y = 0.3290},
};
static const auto DEFAULT_PRIMARIES = BT709;
static const auto PAL_M = SPCPRimaries{
.red = {.x = 0.67, .y = 0.33},
.green = {.x = 0.21, .y = 0.71},
@ -140,7 +148,16 @@ namespace NColorManagement {
};
}
const SPCPRimaries& getPrimaries(ePrimaries name);
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);
std::optional<Mat3x3> rgbToXYZFromPrimaries(SPCPRimaries pr);
Mat3x3 adaptBradford(Hyprgraphics::CColor::xy srcW, Hyprgraphics::CColor::xy dstW);
class CPrimaries {
public:
@ -163,22 +180,17 @@ namespace NColorManagement {
};
struct SImageDescription {
struct SIccFile {
int fd = -1;
uint32_t length = 0;
uint32_t offset = 0;
bool operator==(const SIccFile& i2) const {
return fd == i2.fd;
}
} icc;
static std::expected<SImageDescription, std::string> fromICC(const std::filesystem::path& file);
bool windowsScRGB = false;
//
std::vector<uint8_t> rawICC;
eTransferFunction transferFunction = CM_TRANSFER_FUNCTION_GAMMA22;
float transferFunctionPower = 1.0f;
eTransferFunction transferFunction = CM_TRANSFER_FUNCTION_GAMMA22;
float transferFunctionPower = 1.0f;
bool windowsScRGB = false;
bool primariesNameSet = false;
ePrimaries primariesNamed = CM_PRIMARIES_SRGB;
bool primariesNameSet = false;
ePrimaries primariesNamed = CM_PRIMARIES_SRGB;
// primaries are stored as FP values with the same scale as standard defines (0.0 - 1.0)
// wayland protocol expects int32_t values multiplied by 1000000
// drm expects uint16_t values multiplied by 50000
@ -202,11 +214,23 @@ namespace NColorManagement {
}
} 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 maxFALL = 0;
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)) &&
masteringPrimaries == d2.masteringPrimaries && luminances == d2.luminances && masteringLuminances == d2.masteringLuminances && maxCLL == d2.maxCLL &&
maxFALL == d2.maxFALL;
@ -280,44 +304,48 @@ namespace NColorManagement {
class CImageDescription {
public:
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;
const SImageDescription& value() const;
uint id() const;
uint32_t id() const;
WP<const CPrimaries> getPrimaries() const;
private:
CImageDescription(const SImageDescription& imageDescription, const uint imageDescriptionId);
uint m_id;
uint m_primariesId;
uint32_t m_id = 0;
uint32_t m_primariesId = 0;
SImageDescription m_imageDescription;
};
using PImageDescription = WP<const CImageDescription>;
static const auto DEFAULT_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_SRGB,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_SRGB),
.luminances = {.min = SDR_MIN_LUMINANCE, .max = 80, .reference = 80}});
static const auto DEFAULT_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{
.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_SRGB,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_SRGB),
.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,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.luminances = {.min = HDR_MIN_LUMINANCE, .max = 10000, .reference = 203},
});
static const auto DEFAULT_HDR_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.luminances = {.min = HDR_MIN_LUMINANCE, .max = 10000, .reference = 203}});
;
static const auto SCRGB_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{
.windowsScRGB = true,
.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_EXT_LINEAR,
.windowsScRGB = true,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_SRGB,
.primaries = NColorPrimaries::BT709,
.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)
g_pHyprRenderer->damageBox(m_damageBox);
});
m_texture = makeShared<CTexture>();
}
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() {
if (m_isCreated)
m_texture->destroyTexture();
if (m_isCreated && m_texture)
m_texture.reset();
m_fadeOpacity->setConfig(g_pConfigManager->getAnimationPropertyConfig("fadeIn"));
@ -145,12 +143,13 @@ void CHyprError::createQueued() {
// copy the data to an OpenGL texture we have
const auto DATA = cairo_image_surface_get_data(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);
auto tex = texture();
tex->allocate(PMONITOR->m_pixelSize);
tex->bind();
tex->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
tex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
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);
@ -187,7 +186,8 @@ void CHyprError::draw() {
if (!m_fadeOpacity->isBeingAnimated()) {
if (m_fadeOpacity->value() == 0.f) {
m_queuedDestroy = false;
m_texture->destroyTexture();
if (m_texture)
m_texture.reset();
m_isCreated = false;
m_queued = "";
@ -218,7 +218,7 @@ void CHyprError::draw() {
m_monitorChanged = false;
CTexPassElement::SRenderData data;
data.tex = m_texture;
data.tex = texture();
data.box = monbox;
data.a = m_fadeOpacity->value();
@ -239,3 +239,9 @@ bool CHyprError::active() {
float CHyprError::height() {
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();
float height(); // logical
//
SP<ITexture> texture();
private:
void createQueued();
std::string m_queued = "";
CHyprColor m_queuedColor;
bool m_queuedDestroy = false;
bool m_isCreated = false;
SP<CTexture> m_texture;
SP<ITexture> m_texture;
PHLANIMVAR<float> m_fadeOpacity;
CBox m_damageBox = {0, 0, 0, 0};
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_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_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)");

View file

@ -11,13 +11,7 @@
using namespace Layout;
CLayoutManager::CLayoutManager() {
static auto P = Event::bus()->m_events.monitor.layoutChanged.listen([] {
for (const auto& ws : g_pCompositor->getWorkspaces()) {
ws->m_space->recheckWorkArea();
}
});
}
CLayoutManager::CLayoutManager() = default;
void CLayoutManager::newTarget(SP<ITarget> target, SP<CSpace> space) {
// 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);
}
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) {
const auto MONITOR = Desktop::focusState()->monitor();

View file

@ -53,6 +53,7 @@ namespace Layout {
void moveMouse(const Vector2D& mousePos);
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
void moveTarget(const Vector2D& Δ, SP<ITarget> target);
void setTargetGeom(const CBox& box, SP<ITarget> target); // floats only
void endDragTarget();
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);
if (IS_FLOATING) {
m_floating->removeTarget(target);
std::erase(m_floatingTargets, target);
m_floating->removeTarget(target);
return;
}
const bool IS_TILED = std::ranges::contains(m_tiledTargets, target);
if (IS_TILED) {
m_tiled->removeTarget(target);
std::erase(m_tiledTargets, target);
m_tiled->removeTarget(target);
return;
}
@ -262,3 +262,10 @@ SP<ITarget> CAlgorithm::getNextCandidate(SP<ITarget> old) {
// god damn it, maybe empty?
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 moveTarget(const Vector2D& Δ, SP<ITarget> target);
void setTargetGeom(const CBox& box, SP<ITarget> target); // only for float
void updateFloatingAlgo(UP<IFloatingAlgorithm>&& algo);
void updateTiledAlgo(UP<ITiledAlgorithm>&& algo);

View file

@ -17,6 +17,9 @@ namespace Layout {
// a target is being moved by a delta
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 recalculate();

View file

@ -1,5 +1,10 @@
#include "ModeAlgorithm.hpp"
#include "../space/Space.hpp"
#include "Algorithm.hpp"
#include "../../helpers/Monitor.hpp"
#include "../../desktop/view/Window.hpp"
using namespace Layout;
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() {
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
virtual std::optional<Vector2D> predictSizeForNewTarget();
// Impl'd here: focal point for dir
virtual std::optional<Vector2D> focalPointForDir(SP<ITarget> t, Math::eDirection dir);
protected:
IModeAlgorithm() = default;

View file

@ -116,6 +116,8 @@ void CDefaultFloatingAlgorithm::newTarget(SP<ITarget> target) {
PWINDOW->m_reportedSize = PWINDOW->m_pendingReportedSize;
}
}
updateTarget(target);
}
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
target->setPositionGlobal(fitBoxInWorkArea(CBox{NEW_POS, LAST_SIZE}, target));
}
updateTarget(target);
}
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) {
target->rememberFloatingSize(target->position().size());
m_datas.erase(target);
}
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)
target->warpPositionSize();
updateTarget(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)
target->warpPositionSize();
updateTarget(target);
}
void CDefaultFloatingAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
auto posABackup = a->position();
a->setPositionGlobal(b->position());
b->setPositionGlobal(posABackup);
updateTarget(a);
updateTarget(b);
}
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);
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 <map>
namespace Layout {
class CAlgorithm;
}
@ -17,10 +19,22 @@ namespace Layout::Floating {
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
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 moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
virtual void recenter(SP<ITarget> t);
private:
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,11 +99,13 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, ACTIVE_MON))
OPENINGON = getClosestNode(MOUSECOORDS);
} else if (*PUSEACTIVE) {
} else if (*PUSEACTIVE || m_overrideFocalPoint) {
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 &&
ACTIVE_WINDOW->m_isMapped)
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)
OPENINGON = getNodeFromWindow(ACTIVE_WINDOW);
if (!OPENINGON)
@ -182,7 +184,7 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
// whether or not the override persists after opening one window
if (*PERMANENTDIRECTIONOVERRIDE == 0)
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_PROPORTIONS = NEWPARENT->box.h / NEWPARENT->box.w;
const auto DELTA = MOUSECOORDS - PARENT_CENTER;
@ -214,10 +216,7 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
}
}
} else if (*PFORCESPLIT == 0 || !newTarget) {
if ((SIDEBYSIDE &&
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))) {
if ((SIDEBYSIDE && MOUSECOORDS.x < NEWPARENT->box.x + (NEWPARENT->box.w / 2.F)) || (!SIDEBYSIDE && MOUSECOORDS.y < NEWPARENT->box.y + (NEWPARENT->box.h / 2.F))) {
// we are hovering over the first node, make PNODE first.
NEWPARENT->children[1] = OPENINGON;
NEWPARENT->children[0] = PNODE;
@ -242,11 +241,10 @@ void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
// and update the previous parent if it exists
if (OPENINGON->pParent) {
if (OPENINGON->pParent->children[0] == OPENINGON) {
if (OPENINGON->pParent->children[0] == OPENINGON)
OPENINGON->pParent->children[0] = NEWPARENT;
} else {
else
OPENINGON->pParent->children[1] = NEWPARENT;
}
}
// Update the children
@ -551,41 +549,35 @@ std::optional<Vector2D> CDwindleAlgorithm::predictSizeForNewTarget() {
}
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 Vector2D originalPos = t->position().middle();
if (!PNODE || !t->window())
return;
Vector2D focalPoint;
const auto FOCAL_POINT = focalPointForDir(t, dir);
const auto WINDOWIDEALBB =
t->fullscreenMode() != FSMODE_NONE ? m_parent->space()->workspace()->m_monitor->logicalBox() : t->window()->getWindowIdealBoundingBoxIgnoreReserved();
const auto PMONITORFOCAL = g_pCompositor->getMonitorFromVector(FOCAL_POINT.value_or(t->position().middle()));
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;
}
if (PMONITORFOCAL != m_parent->space()->workspace()->m_monitor && !*PMONITORFALLBACK)
return; // noop
t->window()->setAnimationsToMove();
removeTarget(t);
const auto PMONITORFOCAL = g_pCompositor->getMonitorFromVector(focalPoint);
if (PMONITORFOCAL != m_parent->space()->workspace()->m_monitor) {
// move with a focal point
if (PMONITORFOCAL->m_activeWorkspace)
t->assignToSpace(PMONITORFOCAL->m_activeWorkspace->m_space);
t->assignToSpace(PMONITORFOCAL->m_activeWorkspace->m_space, FOCAL_POINT);
return;
}
movedTarget(t, focalPoint);
movedTarget(t, FOCAL_POINT);
// restore focus to the previous position
if (silent) {
@ -665,11 +657,28 @@ std::expected<void, std::string> CDwindleAlgorithm::layoutMsg(const std::string_
const auto CURRENT_NODE = getNodeFromWindow(Desktop::focusState()->window());
if (ARGS[0] == "togglesplit") {
if (CURRENT_NODE)
toggleSplit(CURRENT_NODE);
if (CURRENT_NODE) {
if (!toggleSplit(CURRENT_NODE))
return std::unexpected("can't togglesplit in the current workspace");
}
} else if (ARGS[0] == "swapsplit") {
if (CURRENT_NODE)
swapSplit(CURRENT_NODE);
if (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") {
auto node = CURRENT_NODE;
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";
moveToRoot(node, STABLE);
if (!moveToRoot(node, STABLE))
return std::unexpected("can't movetoroot in the current workspace");
} else if (ARGS[0] == "preselect") {
auto direction = ARGS[1];
@ -714,42 +724,102 @@ std::expected<void, std::string> CDwindleAlgorithm::layoutMsg(const std::string_
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 {};
}
void CDwindleAlgorithm::toggleSplit(SP<SDwindleNodeData> x) {
bool CDwindleAlgorithm::toggleSplit(SP<SDwindleNodeData> x) {
if (!x || !x->pParent)
return;
return false;
if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return;
return false;
x->pParent->splitTop = !x->pParent->splitTop;
x->pParent->recalcSizePosRecursive();
return true;
}
void CDwindleAlgorithm::swapSplit(SP<SDwindleNodeData> x) {
if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return;
bool CDwindleAlgorithm::swapSplit(SP<SDwindleNodeData> x) {
if (x->pTarget->fullscreenMode() != FSMODE_NONE || !x->pParent)
return false;
std::swap(x->pParent->children[0], x->pParent->children[1]);
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)
return;
if (x->pTarget->fullscreenMode() != FSMODE_NONE)
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
if (!x->pParent->pParent)
return;
return false;
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]);
pRoot->recalcSizePosRecursive();
return true;
}

View file

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

View file

@ -403,7 +403,9 @@ void CMasterAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
}
void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
const auto PWINDOW2 = g_pCompositor->getWindowInDirection(t->window(), dir);
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
const auto PWINDOW2 = g_pCompositor->getWindowInDirection(t->window(), dir);
if (!t->window())
return;
@ -424,7 +426,10 @@ void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir
t->window()->setAnimationsToMove();
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) {
// if same monitor, switch windows
g_layoutManager->switchTargets(t, PWINDOW2->layoutTarget());
@ -720,8 +725,8 @@ std::expected<void, std::string> CMasterAlgorithm::layoutMsg(const std::string_v
for (auto& nd : m_masterNodesData) {
if (!nd->isMaster) {
const auto newMaster = nd;
newMaster->isMaster = true;
const auto& newMaster = nd;
newMaster->isMaster = true;
auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster);
@ -756,8 +761,8 @@ std::expected<void, std::string> CMasterAlgorithm::layoutMsg(const std::string_v
for (auto& nd : m_masterNodesData | std::views::reverse) {
if (!nd->isMaster) {
const auto newMaster = nd;
newMaster->isMaster = true;
const auto& newMaster = nd;
newMaster->isMaster = true;
auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster);
@ -956,7 +961,9 @@ void CMasterAlgorithm::calculateWorkspace() {
const auto STACKWINDOWS = WINDOWS - MASTERS;
const auto WORKAREA = m_parent->space()->workArea();
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 (STACKWINDOWS >= *SLAVECOUNTFORCENTER)
@ -1074,7 +1081,7 @@ void CMasterAlgorithm::calculateWorkspace() {
}
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});
mastersLeft--;
@ -1192,7 +1199,7 @@ void CMasterAlgorithm::calculateWorkspace() {
continue;
if (onRight) {
nextX = WIDTH + PMASTERNODE->size.x - (*PIGNORERESERVED ? PMONITOR->m_reservedArea.left() : 0);
nextX = WIDTH + PMASTERNODE->size.x - (*PIGNORERESERVED ? reservedLeft : 0);
nextY = nextYR;
heightLeft = heightLeftR;
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->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) {
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
if (!t || !t->space() || !t->space()->workspace())
return;
@ -215,7 +220,7 @@ void CMonocleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection di
if (t->window())
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;
}
void CScrollTapeController::insertStrip(size_t afterIndex, float size) {
if (afterIndex >= m_strips.size()) {
void CScrollTapeController::insertStrip(ssize_t afterIndex, float size) {
if (afterIndex >= sc<ssize_t>(m_strips.size())) {
addStrip(size);
return;
}
afterIndex = std::clamp(afterIndex, sc<ssize_t>(-1L), sc<ssize_t>(INT32_MAX));
SStripData newStrip;
newStrip.size = size;
m_strips.insert(m_strips.begin() + afterIndex + 1, newStrip);

View file

@ -40,7 +40,7 @@ namespace Layout::Tiled {
bool isReversed() const;
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);
size_t stripCount() const;
SStripData& getStrip(size_t index);

View file

@ -190,10 +190,12 @@ size_t SColumnData::idx(SP<ITarget> t) {
}
size_t SColumnData::idxForHeight(float y) {
if (targetDatas.empty())
return 0;
for (size_t i = 0; i < targetDatas.size(); ++i) {
if (targetDatas[i]->target->position().y < y)
continue;
return i - 1;
return i == 0 ? 0 : i - 1;
}
return targetDatas.size() - 1;
}
@ -245,24 +247,28 @@ void SColumnData::remove(SP<ITarget> t) {
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) {
if (targetDatas[i] != w)
continue;
std::swap(targetDatas[i], targetDatas[i - 1]);
break;
return true;
}
return false;
}
void SColumnData::down(SP<SScrollingTargetData> w) {
bool SColumnData::down(SP<SScrollingTargetData> w) {
for (size_t i = 0; i < targetDatas.size() - 1; ++i) {
if (targetDatas[i] != w)
continue;
std::swap(targetDatas[i], targetDatas[i + 1]);
break;
return true;
}
return false;
}
SP<SScrollingTargetData> SColumnData::next(SP<SScrollingTargetData> w) {
@ -296,23 +302,21 @@ SScrollingData::SScrollingData(CScrollingAlgorithm* algo) : algorithm(algo) {
}
SP<SColumnData> SScrollingData::add() {
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
auto col = columns.emplace_back(makeShared<SColumnData>(self.lock()));
col->self = col;
auto col = columns.emplace_back(makeShared<SColumnData>(self.lock()));
col->self = col;
size_t stripIdx = controller->addStrip(*PCOLWIDTH);
size_t stripIdx = controller->addStrip(algorithm->defaultColumnWidth());
controller->getStrip(stripIdx).userData = col;
return col;
}
SP<SColumnData> SScrollingData::add(int after) {
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
auto col = makeShared<SColumnData>(self.lock());
col->self = col;
auto col = makeShared<SColumnData>(self.lock());
col->self = col;
columns.insert(columns.begin() + after + 1, col);
controller->insertStrip(after, *PCOLWIDTH);
controller->insertStrip(after, algorithm->defaultColumnWidth());
controller->getStrip(after + 1).userData = col;
return col;
@ -466,6 +470,21 @@ CScrollingAlgorithm::CScrollingAlgorithm() {
m_scrollingData = makeShared<SScrollingData>(this);
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
auto parseDirection = [](const std::string& dir) -> eScrollDirection {
if (dir == "left")
@ -478,19 +497,11 @@ CScrollingAlgorithm::CScrollingAlgorithm() {
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");
m_config.configuredWidths.clear();
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};
m_config.configuredWidths = parseColumnWidths(*PCONFWIDTHS);
// Update scroll direction
m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION));
@ -523,7 +534,7 @@ CScrollingAlgorithm::CScrollingAlgorithm() {
});
// 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));
}
@ -616,16 +627,20 @@ void CScrollingAlgorithm::removeTarget(SP<ITarget> target) {
if (!m_scrollingData->next(DATA->column.lock()) && DATA->column->targetDatas.size() <= 1) {
// move the view if this is the last column
const auto USABLE = usableArea();
m_scrollingData->controller->adjustOffset(-(USABLE.w * DATA->column->getColumnWidth()));
const auto USABLE = usableArea();
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);
if (!DATA->column) {
// column got removed, let's ensure we don't leave any cringe extra space
const auto USABLE = usableArea();
double newOffset = std::clamp(m_scrollingData->controller->getOffset(), 0.0, std::max(m_scrollingData->maxWidth() - USABLE.w, 1.0));
const auto USABLE = usableArea();
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);
}
@ -833,66 +848,129 @@ void CScrollingAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection
}
void CScrollingAlgorithm::moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool silent) {
const auto DATA = dataFor(t);
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
const auto DATA = dataFor(t);
if (!DATA)
return;
const auto TAPE_DIR = getDynamicDirection();
const auto CURRENT_COL = DATA->column.lock();
const auto current_idx = m_scrollingData->idx(CURRENT_COL);
if (dir == Math::DIRECTION_LEFT) {
const auto COL = m_scrollingData->prev(DATA->column.lock());
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;
}
// ignore moves to the "origin" when on first column and moving opposite to tape direction
if (!COL && current_idx == 0 && (TAPE_DIR == SCROLL_DIR_RIGHT || TAPE_DIR == SCROLL_DIR_DOWN))
return;
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;
}
DATA->column->remove(t);
if (!COL) {
const auto NEWCOL = m_scrollingData->add(-1);
NEWCOL->add(DATA);
m_scrollingData->centerOrFitCol(NEWCOL);
} else {
if (COL->targetDatas.size() > 0)
COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
else
COL->add(DATA);
m_scrollingData->centerOrFitCol(COL);
}
} else if (dir == Math::DIRECTION_RIGHT) {
const auto COL = m_scrollingData->next(DATA->column.lock());
// ignore moves to the "origin" when on last column and moving opposite to tape direction
if (!COL && current_idx == (int64_t)m_scrollingData->columns.size() - 1 && (TAPE_DIR == SCROLL_DIR_LEFT || TAPE_DIR == SCROLL_DIR_UP))
return;
DATA->column->remove(t);
if (!COL) {
// make a new one
const auto NEWCOL = m_scrollingData->add();
NEWCOL->add(DATA);
m_scrollingData->centerOrFitCol(NEWCOL);
} else {
if (COL->targetDatas.size() > 0)
COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
else
COL->add(DATA);
m_scrollingData->centerOrFitCol(COL);
return dir;
}
default: break;
}
} else if (dir == Math::DIRECTION_UP)
DATA->column->up(DATA);
else if (dir == Math::DIRECTION_DOWN)
DATA->column->down(DATA);
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());
// ignore moves to the origin if we are alone
if (!COL && current_idx == 0 && DATA->column->targetDatas.size() == 1)
return false;
DATA->column->remove(t);
if (!COL) {
const auto NEWCOL = m_scrollingData->add(-1);
NEWCOL->add(DATA);
m_scrollingData->centerOrFitCol(NEWCOL);
} else {
if (COL->targetDatas.size() > 0)
COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
else
COL->add(DATA);
m_scrollingData->centerOrFitCol(COL);
}
return true;
} else if (ROTATED_DIR == Math::DIRECTION_RIGHT) {
const auto COL = m_scrollingData->next(DATA->column.lock());
// 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 && DATA->column->targetDatas.size() == 1)
return false;
DATA->column->remove(t);
if (!COL) {
// make a new one
const auto NEWCOL = m_scrollingData->add();
NEWCOL->add(DATA);
m_scrollingData->centerOrFitCol(NEWCOL);
} else {
if (COL->targetDatas.size() > 0)
COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
else
COL->add(DATA);
m_scrollingData->centerOrFitCol(COL);
}
return true;
} else if (ROTATED_DIR == Math::DIRECTION_UP)
return DATA->column->up(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();
focusTargetUpdate(t);
if (t->window())
g_pCompositor->warpCursorTo(t->window()->middle());
}
std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::string_view& sv) {
@ -1177,8 +1255,9 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
m_scrollingData->recalculate();
}
} else if (ARGS[0] == "focus") {
const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr);
static const auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback");
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 PCONFWRAPFOCUS = CConfigValue<Hyprlang::INT>("scrolling:wrap_focus");
if (!TDATA || ARGS[1].empty())
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());
return {};
} else
PREV = m_scrollingData->columns.back();
PREV = (*PCONFWRAPFOCUS == 1) ? m_scrollingData->columns.back() : m_scrollingData->columns.front();
}
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());
return {};
} else
NEXT = m_scrollingData->columns.front();
NEXT = (*PCONFWRAPFOCUS == 1) ? m_scrollingData->columns.front() : m_scrollingData->columns.back();
}
auto pTargetData = findBestNeighbor(TDATA, NEXT);
@ -1283,6 +1362,8 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
m_scrollingData->recalculate();
} else if (ARGS[0] == "swapcol") {
static const auto PCONFWRAPSWAPCOL = CConfigValue<Hyprlang::INT>("scrolling:wrap_swapcol");
if (ARGS.size() < 2)
return std::unexpected("not enough args");
@ -1308,9 +1389,15 @@ std::expected<void, std::string> CScrollingAlgorithm::layoutMsg(const std::strin
// wrap around swaps
if (direction == "l")
targetIdx = (currentIdx == 0) ? (colCount - 1) : (currentIdx - 1);
if (*PCONFWRAPSWAPCOL == 1)
targetIdx = (currentIdx == 0) ? (colCount - 1) : (currentIdx - 1);
else
targetIdx = (currentIdx == 0) ? 0 : (currentIdx - 1);
else if (direction == "r")
targetIdx = (currentIdx == (int64_t)colCount - 1) ? 0 : (currentIdx + 1);
if (*PCONFWRAPSWAPCOL == 1)
targetIdx = (currentIdx == (int64_t)colCount - 1) ? 0 : (currentIdx + 1);
else
targetIdx = (currentIdx == (int64_t)colCount - 1) ? (colCount - 1) : (currentIdx + 1);
else
return std::unexpected("no target (invalid direction?)");
;
@ -1418,9 +1505,14 @@ CBox CScrollingAlgorithm::usableArea() {
CBox box = m_parent->space()->workArea();
// 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;
box.translate(-m_parent->space()->workspace()->m_monitor->m_position);
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.
size_t idxForHeight(float y);
void up(SP<SScrollingTargetData> w);
void down(SP<SScrollingTargetData> w);
bool up(SP<SScrollingTargetData> w);
bool down(SP<SScrollingTargetData> w);
SP<SScrollingTargetData> next(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 focusOnInput(SP<ITarget> target, eInputMode input);
float defaultColumnWidth();
friend struct SScrollingData;
};
};

View file

@ -6,6 +6,7 @@
#include "../../debug/log/Logger.hpp"
#include "../../desktop/Workspace.hpp"
#include "../../config/ConfigManager.hpp"
#include "../../event/EventBus.hpp"
using namespace Layout;
@ -17,6 +18,12 @@ SP<CSpace> CSpace::create(PHLWORKSPACE w) {
CSpace::CSpace(PHLWORKSPACE parent) : m_parent(parent) {
recheckWorkArea();
// NOLINTNEXTLINE
m_geomUpdateCallback = Event::bus()->m_events.monitor.layoutChanged.listen([this] {
recheckWorkArea();
m_algorithm->recalculate();
});
}
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);
}
void CSpace::setTargetGeom(const CBox& box, SP<ITarget> target) {
if (m_algorithm)
m_algorithm->setTargetGeom(box, target);
}
SP<ITarget> CSpace::getNextCandidate(SP<ITarget> 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 moveTarget(const Vector2D& Δ, SP<ITarget> target);
void setTargetGeom(const CBox& box, SP<ITarget> target); // only for float
SP<CAlgorithm> algorithm() const;
@ -63,5 +64,8 @@ namespace Layout {
// work area is in global coords
CBox m_workArea, m_floatingWorkArea;
// for recalc
CHyprSignalListener m_geomUpdateCallback;
};
};

View file

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

View file

@ -10,6 +10,9 @@
#include "../../Compositor.hpp"
#include "../../render/Renderer.hpp"
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::Utils;
using namespace Layout;
SP<ITarget> CWindowTarget::create(PHLWINDOW w) {
@ -34,6 +37,9 @@ void CWindowTarget::setPositionGlobal(const CBox& box) {
void CWindowTarget::updatePos() {
g_pHyprRenderer->damageWindow(m_window.lock());
CScopeGuard x([this] { g_pHyprRenderer->damageWindow(m_window.lock()); });
if (!m_space)
return;
@ -55,6 +61,11 @@ void CWindowTarget::updatePos() {
// 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 PWORKSPACE = m_space->workspace();
@ -104,7 +115,7 @@ void CWindowTarget::updatePos() {
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 double requestedRatio = (*REQUESTEDRATIO).x / (*REQUESTEDRATIO).y;
@ -131,7 +142,7 @@ void CWindowTarget::updatePos() {
calcPos = calcPos + GAPOFFSETTOPLEFT + ratioPadding / 2;
calcSize = calcSize - GAPOFFSETTOPLEFT - GAPOFFSETBOTTOMRIGHT - ratioPadding;
if (isPseudo()) {
if (isPseudo() && fullscreenMode() == FSMODE_NONE) {
// Calculate pseudo
float scale = 1;
@ -162,18 +173,14 @@ void CWindowTarget::updatePos() {
static auto PCLAMP_TILED = CConfigValue<Hyprlang::INT>("misc:size_limits_tiled");
if (*PCLAMP_TILED) {
const auto borderSize = m_window->getRealBorderSize();
Vector2D monitorAvailable = MONITOR_WORKAREA.size() - Vector2D{2.0 * borderSize, 2.0 * borderSize};
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);
Vector2D minSize = m_window->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE});
Vector2D maxSize = m_window->isFullscreen() ? Vector2D{INFINITY, INFINITY} : m_window->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY});
calcSize = calcSize.clamp(minSize, maxSize);
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.y = std::clamp(calcPos.y, MONITOR_WORKAREA.y + borderSize, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h - calcSize.y - 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, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h - calcSize.y);
}
if (m_window->onSpecialWorkspace() && !m_window->isFullscreen()) {
@ -370,5 +377,6 @@ void CWindowTarget::onUpdateSpace() {
m_window->m_monitor = space()->workspace()->m_monitor;
m_window->moveToWorkspace(space()->workspace());
m_window->updateToplevel();
m_window->updateWindowData();
m_window->updateWindowDecos();
}

View file

@ -109,9 +109,6 @@ CKeybindManager::CKeybindManager() {
m_dispatchers["togglegroup"] = toggleGroup;
m_dispatchers["changegroupactive"] = changeGroupActive;
m_dispatchers["movegroupwindow"] = moveGroupWindow;
m_dispatchers["togglesplit"] = toggleSplit;
m_dispatchers["swapsplit"] = swapSplit;
m_dispatchers["splitratio"] = alterSplitRatio;
m_dispatchers["focusmonitor"] = focusMonitor;
m_dispatchers["movecursortocorner"] = moveCursorToCorner;
m_dispatchers["movecursor"] = moveCursor;
@ -1468,9 +1465,10 @@ SDispatchResult CKeybindManager::moveActiveToWorkspaceSilent(std::string args) {
}
SDispatchResult CKeybindManager::moveFocusTo(std::string args) {
static auto PFULLCYCLE = CConfigValue<Hyprlang::INT>("binds:movefocus_cycles_fullscreen");
static auto PGROUPCYCLE = CConfigValue<Hyprlang::INT>("binds:movefocus_cycles_groupfirst");
Math::eDirection dir = Math::fromChar(args[0]);
static auto PFULLCYCLE = CConfigValue<Hyprlang::INT>("binds:movefocus_cycles_fullscreen");
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]);
if (dir == Math::DIRECTION_DEFAULT) {
Log::logger->log(Log::ERR, "Cannot move focus in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0]);
@ -1479,7 +1477,8 @@ SDispatchResult CKeybindManager::moveFocusTo(std::string args) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW || !PLASTWINDOW->aliveAndVisible()) {
tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir));
if (*PMONITORFALLBACK)
tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir));
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));
if (tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir)))
if (*PMONITORFALLBACK && tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir)))
return {};
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
try {
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"}; }
return {};
@ -1704,18 +1706,6 @@ SDispatchResult CKeybindManager::changeGroupActive(std::string args) {
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) {
const auto PMONITOR = g_pCompositor->getMonitorFromString(arg);
tryMoveFocusToMonitor(PMONITOR);
@ -2747,7 +2737,9 @@ void CKeybindManager::moveWindowOutOfGroup(PHLWINDOW pWindow, const std::string&
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) {
Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND);

View file

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

View file

@ -8,6 +8,7 @@
#include "../protocols/IdleNotify.hpp"
#include "../protocols/core/Compositor.hpp"
#include "../protocols/core/Seat.hpp"
#include "debug/log/Logger.hpp"
#include "eventLoop/EventLoopManager.hpp"
#include "../render/pass/TexPassElement.hpp"
#include "../managers/input/InputManager.hpp"
@ -18,9 +19,12 @@
#include "../helpers/time/Time.hpp"
#include "../helpers/Drm.hpp"
#include "../event/EventBus.hpp"
#include <climits>
#include <cstring>
#include <gbm.h>
#include <cairo/cairo.h>
#include <hyprutils/math/Region.hpp>
#include <hyprutils/math/Vector2D.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::Utils;
@ -406,7 +410,7 @@ bool CPointerManager::setHWCursorBuffer(SP<SMonitorPointerState> state, SP<Aquam
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 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)
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_matrix_rotate(&matrixPre, M_PI_2 * sc<double>(TR));
// FIXME: this is wrong, and doesn't work for 5, 6 and 7. (flipped + rot)
// cba to do it rn, does anyone fucking use that??
if (TR >= WL_OUTPUT_TRANSFORM_FLIPPED) {
cairo_matrix_scale(&matrixPre, -1, 1);
cairo_matrix_translate(&matrixPre, -DMABUF.size.x, 0);
}
if (TR == 3 || TR == 7)
cairo_matrix_translate(&matrixPre, -DMABUF.size.x, 0);
else if (TR == 2 || TR == 6)
cairo_matrix_translate(&matrixPre, -DMABUF.size.x, -DMABUF.size.y);
else if (TR == 1 || TR == 5)
cairo_matrix_translate(&matrixPre, 0, -DMABUF.size.y);
// Cairo pattern matrix maps destination coords to source coords (inverse of visual transform).
// x_src = xx * x_dst + xy * y_dst + x0
// y_src = yx * x_dst + yy * y_dst + y0
// cairo_matrix_init(&m, xx, yx, xy, yy, x0, y0)
switch (TR) {
case WL_OUTPUT_TRANSFORM_NORMAL:
default: cairo_matrix_init(&matrixPre, SX, 0, 0, SY, 0, 0); break;
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;
case WL_OUTPUT_TRANSFORM_FLIPPED: cairo_matrix_init(&matrixPre, -SX, 0, 0, SY, SX * BW, 0); break;
case WL_OUTPUT_TRANSFORM_FLIPPED_90: cairo_matrix_init(&matrixPre, 0, SY, SX, 0, 0, 0); break;
case WL_OUTPUT_TRANSFORM_FLIPPED_180: cairo_matrix_init(&matrixPre, SX, 0, 0, -SY, 0, SY * BH); break;
case WL_OUTPUT_TRANSFORM_FLIPPED_270: cairo_matrix_init(&matrixPre, 0, -SY, -SX, 0, SX * BW, SY * BH); break;
}
cairo_pattern_set_matrix(PATTERNPRE, &matrixPre);
@ -589,15 +592,14 @@ SP<Aquamarine::IBuffer> CPointerManager::renderHWCursorBuffer(SP<CPointerManager
RBO->bind();
const auto& damageSize = state->monitor->m_output->cursorPlaneSize();
g_pHyprOpenGL->beginSimple(state->monitor.lock(), {0, 0, damageSize.x, damageSize.y}, RBO);
g_pHyprOpenGL->beginSimple(state->monitor.lock(), {0, 0, INT_MAX, INT_MAX}, RBO);
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()};
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());
g_pHyprOpenGL->renderTexture(texture, xbox, {});
g_pHyprOpenGL->renderTexture(texture, xbox, {.noCM = true});
g_pHyprOpenGL->end();
g_pHyprOpenGL->m_renderData.pMonitor.reset();
@ -901,13 +903,13 @@ const CPointerManager::SCursorImage& CPointerManager::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))
return nullptr;
if (m_currentCursorImage.pBuffer) {
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;
}

View file

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

View file

@ -847,6 +847,9 @@ void CInputManager::processMouseDownKill(const IPointer::SButtonEvent& e) {
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(PWINDOW->getPID(), SIGKILL);
break;

View file

@ -169,10 +169,10 @@ bool CCursorshareSession::copy() {
return false;
}
CFramebuffer outFB;
outFB.alloc(m_bufferSize.x, m_bufferSize.y, m_format);
auto outFB = g_pHyprRenderer->createFB();
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");
return false;
}
@ -182,8 +182,8 @@ bool CCursorshareSession::copy() {
g_pHyprRenderer->endRender();
g_pHyprOpenGL->m_renderData.pMonitor = m_pendingFrame.monitor;
outFB.bind();
glBindFramebuffer(GL_READ_FRAMEBUFFER, outFB.getFBID());
outFB->bind();
glBindFramebuffer(GL_READ_FRAMEBUFFER, GLFB(outFB)->getFBID());
glPixelStorei(GL_PACK_ALIGNMENT, 1);
@ -212,7 +212,7 @@ bool CCursorshareSession::copy() {
g_pHyprOpenGL->m_renderData.pMonitor.reset();
m_pendingFrame.buffer->endDataPtr();
outFB.unbind();
GLFB(outFB)->unbind();
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);

View file

@ -10,6 +10,7 @@
#include "../../helpers/Monitor.hpp"
#include "../../desktop/view/Window.hpp"
#include "../../desktop/state/FocusState.hpp"
#include <hyprutils/math/Region.hpp>
using namespace Screenshare;
@ -133,7 +134,7 @@ void CScreenshareFrame::copy() {
// 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);
if (PERM == PERMISSION_RULE_ALLOW_MODE_PENDING) {
if (!m_session->m_tempFB.isAllocated())
if (!m_session->m_tempFB || !m_session->m_tempFB->isAllocated())
storeTempFB();
// 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();
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);
g_pHyprOpenGL->m_renderData.transformDamage = false;
@ -295,8 +296,11 @@ void CScreenshareFrame::renderWindow() {
return;
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;
if (Desktop::focusState()->window() != m_session->m_window)
@ -322,10 +326,10 @@ void CScreenshareFrame::render() {
return;
}
if (m_session->m_tempFB.isAllocated()) {
if (m_session->m_tempFB && m_session->m_tempFB->isAllocated()) {
CBox texbox = {{}, m_bufferSize};
g_pHyprOpenGL->renderTexture(m_session->m_tempFB.getTexture(), texbox, {});
m_session->m_tempFB.release();
g_pHyprOpenGL->renderTexture(m_session->m_tempFB->getTexture(), texbox, {});
m_session->m_tempFB->release();
return;
}
@ -378,12 +382,12 @@ bool CScreenshareFrame::copyShm() {
return false;
}
const auto PMONITOR = m_session->monitor();
const auto PMONITOR = m_session->monitor();
CFramebuffer outFB;
outFB.alloc(m_bufferSize.x, m_bufferSize.y, shm.format);
auto outFB = g_pHyprRenderer->createFB();
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");
return false;
}
@ -395,8 +399,8 @@ bool CScreenshareFrame::copyShm() {
g_pHyprRenderer->endRender();
g_pHyprOpenGL->m_renderData.pMonitor = PMONITOR;
outFB.bind();
glBindFramebuffer(GL_READ_FRAMEBUFFER, outFB.getFBID());
outFB->bind();
glBindFramebuffer(GL_READ_FRAMEBUFFER, GLFB(outFB)->getFBID());
glPixelStorei(GL_PACK_ALIGNMENT, 1);
@ -438,7 +442,7 @@ bool CScreenshareFrame::copyShm() {
});
}
outFB.unbind();
GLFB(outFB)->unbind();
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
@ -453,13 +457,13 @@ bool CScreenshareFrame::copyShm() {
}
void CScreenshareFrame::storeTempFB() {
g_pHyprRenderer->makeEGLCurrent();
m_session->m_tempFB.alloc(m_bufferSize.x, m_bufferSize.y);
if (!m_session->m_tempFB)
m_session->m_tempFB = g_pHyprRenderer->createFB();
m_session->m_tempFB->alloc(m_bufferSize.x, m_bufferSize.y);
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");
return;
}

View file

@ -145,15 +145,19 @@ WP<CScreenshareSession> CScreenshareManager::getManagedSession(eScreenshareType
auto& session = *it;
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;
}
void CScreenshareManager::destroyClientSessions(wl_client* client) {
LOGM(Log::TRACE, "Destroy client sessions for {:x}", (uintptr_t)client);
std::erase_if(m_managedSessions, [&](const auto& session) { return !session || session->m_session->m_client == client; });
bool CScreenshareManager::isOutputBeingSSd(PHLMONITOR monitor) {
return std::ranges::any_of(m_sessions, [monitor](const auto& s) {
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)) {

View file

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

View file

@ -39,7 +39,8 @@ CScreenshareSession::CScreenshareSession(PHLMONITOR monitor, CBox captureRegion,
CScreenshareSession::~CScreenshareSession() {
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() {
@ -52,6 +53,9 @@ void CScreenshareSession::stop() {
}
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>(
std::chrono::milliseconds(500),
[this](SP<CEventLoopTimer> self, void* data) {
@ -99,7 +103,7 @@ void CScreenshareSession::calculateConstraints() {
m_name = PMONITOR->m_name;
break;
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;
break;
case SHARE_REGION:
@ -121,7 +125,7 @@ void CScreenshareSession::screenshareEvents(bool startSharing) {
m_sharing = true;
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)});
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);
} else if (!startSharing && m_sharing) {

View file

@ -3,7 +3,7 @@
#include "color-management-v1.hpp"
#include "../helpers/Monitor.hpp"
#include "core/Output.hpp"
#include "types/ColorManagement.hpp"
#include "../helpers/cm/ColorManagement.hpp"
#include <cstdint>
using namespace NColorManagement;
@ -388,12 +388,6 @@ CColorManagementFeedbackSurface::CColorManagementFeedbackSurface(SP<CWpColorMana
RESOURCE->m_settings = m_surface->getPreferredImageDescription();
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);
});
@ -429,7 +423,7 @@ CColorManagementIccCreator::CColorManagementIccCreator(SP<CWpImageDescriptionCre
LOGM(Log::TRACE, "Create image description from icc for id {}", id);
// 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");
return;
}
@ -443,10 +437,10 @@ CColorManagementIccCreator::CColorManagementIccCreator(SP<CWpImageDescriptionCre
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
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");
return;
}
@ -459,9 +453,9 @@ CColorManagementIccCreator::CColorManagementIccCreator(SP<CWpImageDescriptionCre
});
m_resource->setSetIccFile([this](CWpImageDescriptionCreatorIccV1* r, int fd, uint32_t offset, uint32_t length) {
m_settings.icc.fd = fd;
m_settings.icc.offset = offset;
m_settings.icc.length = length;
m_icc.fd = fd;
m_icc.offset = offset;
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)); };
if (m_settings.icc.fd >= 0)
m_resource->sendIccFile(m_settings.icc.fd, m_settings.icc.length);
// FIXME:
// if (m_icc.fd >= 0)
// m_resource->sendIccFile(m_icc.fd, m_icc.length);
// 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),

View file

@ -7,7 +7,7 @@
#include "../helpers/Monitor.hpp"
#include "core/Compositor.hpp"
#include "color-management-v1.hpp"
#include "types/ColorManagement.hpp"
#include "../helpers/cm/ColorManagement.hpp"
class CColorManager;
class CColorManagementOutput;
@ -109,6 +109,14 @@ class CColorManagementIccCreator {
WP<CColorManagementIccCreator> m_self;
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:
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);
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
int fdFlags = fcntl(gammaFd.get(), F_GETFL, 0);
if UNLIKELY (fdFlags < 0) {

View file

@ -13,10 +13,7 @@ CScreencopyClient::CScreencopyClient(SP<CZwlrScreencopyManagerV1> resource_) : m
return;
m_resource->setDestroy([this](CZwlrScreencopyManagerV1* pMgr) { PROTO::screencopy->destroyResource(this); });
m_resource->setOnDestroy([this](CZwlrScreencopyManagerV1* pMgr) {
Screenshare::mgr()->destroyClientSessions(m_savedClient);
PROTO::screencopy->destroyResource(this);
});
m_resource->setOnDestroy([this](CZwlrScreencopyManagerV1* pMgr) { PROTO::screencopy->destroyResource(this); });
m_resource->setCaptureOutput(
[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,
@ -25,10 +22,6 @@ CScreencopyClient::CScreencopyClient(SP<CZwlrScreencopyManagerV1> resource_) : m
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) {
const auto PMONITORRES = CWLOutputResource::fromResource(output);
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->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);
auto formats = m_session->allowedFormats();
@ -87,7 +75,14 @@ CScreencopyFrame::CScreencopyFrame(SP<CZwlrScreencopyFrameV1> resource_, WP<CScr
auto bufSize = m_frame->bufferSize();
const auto PSHMINFO = NFormatUtils::getPixelFormatFromDRM(format);
const auto stride = NFormatUtils::minStride(PSHMINFO, bufSize.x);
if (!PSHMINFO) {
LOGM(Log::ERR, "No pixel format for drm format");
m_resource->sendFailed();
return;
}
const auto stride = NFormatUtils::minStride(PSHMINFO, bufSize.x);
m_resource->sendBuffer(NFormatUtils::drmToShm(format), bufSize.x, bufSize.y, stride);
if (m_resource->version() >= 3) {
@ -104,6 +99,12 @@ void CScreencopyFrame::shareFrame(CZwlrScreencopyFrameV1* pFrame, wl_resource* b
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) {
LOGM(Log::ERR, "Buffer used in {:x}", (uintptr_t)this);
m_resource->error(ZWLR_SCREENCOPY_FRAME_V1_ERROR_ALREADY_USED, "frame already used");

View file

@ -19,7 +19,6 @@ namespace Screenshare {
class CScreencopyClient {
public:
CScreencopyClient(SP<CZwlrScreencopyManagerV1> resource_);
~CScreencopyClient();
bool good();
@ -52,10 +51,7 @@ class CScreencopyFrame {
Time::steady_tp m_timestamp;
bool m_overlayCursor = true;
struct {
CHyprSignalListener stopped;
} m_listeners;
//
void shareFrame(CZwlrScreencopyFrameV1* pFrame, wl_resource* buffer, bool withDamage);
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_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_success = m_texture->m_texID;
m_success = m_texture->ok();
size = {1, 1};

View file

@ -13,10 +13,7 @@ CToplevelExportClient::CToplevelExportClient(SP<CHyprlandToplevelExportManagerV1
if UNLIKELY (!good())
return;
m_resource->setOnDestroy([this](CHyprlandToplevelExportManagerV1* pMgr) {
Screenshare::mgr()->destroyClientSessions(m_savedClient);
PROTO::toplevelExport->destroyResource(this);
});
m_resource->setOnDestroy([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) {
captureToplevel(frame, overlayCursor, g_pCompositor->getWindowFromHandle(handle));
@ -28,10 +25,6 @@ CToplevelExportClient::CToplevelExportClient(SP<CHyprlandToplevelExportManagerV1
m_savedClient = m_resource->client();
}
CToplevelExportClient::~CToplevelExportClient() {
Screenshare::mgr()->destroyClientSessions(m_savedClient);
}
void CToplevelExportClient::captureToplevel(uint32_t frame, int32_t overlayCursor_, PHLWINDOW 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->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);
auto formats = m_session->allowedFormats();
@ -100,6 +88,12 @@ void CToplevelExportFrame::shareFrame(wl_resource* buffer, bool ignoreDamage) {
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) {
LOGM(Log::ERR, "Buffer used in {:x}", (uintptr_t)this);
m_resource->error(HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_ALREADY_USED, "frame already used");

View file

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

View file

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

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