jay, many thing
This commit is contained in:
parent
d4d04d7d6d
commit
6e62eccfba
32 changed files with 5580 additions and 333 deletions
|
|
@ -11,6 +11,7 @@ scope "nix.settings" {
|
|||
"https://cache.flox.dev"
|
||||
"https://cache.amaanq.com"
|
||||
"https://cache.nixos-cuda.org"
|
||||
"https://niri.cachix.org"
|
||||
];
|
||||
trusted-public-keys = [
|
||||
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
|
||||
|
|
@ -21,5 +22,6 @@ scope "nix.settings" {
|
|||
"flox-cache-public-1:7F4OyH7ZCnFhcze3fJdfyXYLQw/aV7GEed86nQ7IsOs="
|
||||
"cache.amaanq.com:H0iXsEEFsvUNtWb5v9V8Kss+L4F/tnXwDHXcY+xbmKk="
|
||||
"cache.nixos-cuda.org:74DUi4Ye579gUqzH4ziL9IyiJBlDpMRn9MBN8oNan9M="
|
||||
"niri.cachix.org-1:Wv0OmO7PsuocRKzfDoJ3mulSl7Z6oezYhGhR+3W2964="
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,4 @@
|
|||
StartLimitBurst = 5;
|
||||
};
|
||||
};
|
||||
# niri-flake is death
|
||||
systemd.user.services.niri-flake-polkit = lib.mkForce { };
|
||||
services.gnome.gnome-keyring.enable = lib.mkForce false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,23 +8,6 @@
|
|||
}:
|
||||
let
|
||||
inherit (pkgs) nushell;
|
||||
# init =
|
||||
# let
|
||||
# comma = lib.getExe' (getFlakePkg' inputs.nix-index-database "comma-with-db") "comma";
|
||||
# in
|
||||
# ''
|
||||
# function fish_greeting
|
||||
# ${./rice/header.sh}
|
||||
# echo ""
|
||||
# end
|
||||
# function fish_title
|
||||
# set -q argv[1]; or set argv fish
|
||||
# echo (fish_prompt_pwd_dir_length=100 prompt_pwd): $argv;
|
||||
# end
|
||||
# function fish_command_not_found
|
||||
# ${comma} $argv
|
||||
# end
|
||||
# '';
|
||||
prompt = ''
|
||||
do --env {
|
||||
def prompt-header [
|
||||
|
|
|
|||
154
flake.lock
generated
154
flake.lock
generated
|
|
@ -25,11 +25,11 @@
|
|||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771605131,
|
||||
"narHash": "sha256-9K3F2PSorw7cvqotXRLzz9wE29XfioMCtliFclkL/hM=",
|
||||
"lastModified": 1772210137,
|
||||
"narHash": "sha256-w5w8OAexQ8Ba62Yd/GG9xjvEp2U6G6ua3+ZCvSGcO7g=",
|
||||
"owner": "linyinfeng",
|
||||
"repo": "angrr",
|
||||
"rev": "11fee1e3089bdbc0fb144366bc62ee8a95d4628f",
|
||||
"rev": "3ab5cecfb8a478e3012606396ab36e1b793609f6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -55,11 +55,11 @@
|
|||
},
|
||||
"bunker": {
|
||||
"locked": {
|
||||
"lastModified": 1772004723,
|
||||
"narHash": "sha256-sTAQT6QejSY5PSJuoCRtPBAGo/wgWzglgaFqtHvv9KQ=",
|
||||
"lastModified": 1772253494,
|
||||
"narHash": "sha256-qIVFT2H6YQbnFur0/aKkb2tbX6bppugdYVYZ3eMwzso=",
|
||||
"owner": "amaanq",
|
||||
"repo": "bunker-patches",
|
||||
"rev": "4f77ba4e6b2579290514dcdcbbb5577b64b027cb",
|
||||
"rev": "b5e79f6a3c6ccbbbe4065503b16e91a3c876ddef",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -239,11 +239,11 @@
|
|||
"nixpkgs-lib": "nixpkgs-lib_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769996383,
|
||||
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
||||
"lastModified": 1772408722,
|
||||
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
||||
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -320,11 +320,11 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771320802,
|
||||
"narHash": "sha256-fVwjjcxivStYGSLOYJxtEISKXv/8/TxZI4EB+wqPBpc=",
|
||||
"lastModified": 1772020767,
|
||||
"narHash": "sha256-OxeMEMxRJ6dF3UGXVJoNRwxU/F1nOVbdcyX9n8S3Mxk=",
|
||||
"owner": "amaanq",
|
||||
"repo": "helium-flake",
|
||||
"rev": "e8c651bc6b16925b1f5ede3ca2e206f22d0eb96c",
|
||||
"rev": "9d3ef138f70b3540397320d25ead6aa96101371d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -419,6 +419,22 @@
|
|||
"url": "https://git.lobotomise.me/atagen/hudcore-plymouth"
|
||||
}
|
||||
},
|
||||
"jay-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1772808967,
|
||||
"narHash": "sha256-F4ZYChLfWymZhdUZIRuz1HNt2htm3vawAh9BY3yuWBU=",
|
||||
"owner": "mahkoh",
|
||||
"repo": "jay",
|
||||
"rev": "893be823b6e215eea14bfc11f3025aa6dd1d6c70",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mahkoh",
|
||||
"repo": "jay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"kitty-themes-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
|
@ -545,11 +561,11 @@
|
|||
"xwayland-satellite-unstable": "xwayland-satellite-unstable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771940378,
|
||||
"narHash": "sha256-qe5t8E8uK5eSgPTxtfcde3VO8fnIr/Tu+hn72FDry/E=",
|
||||
"lastModified": 1772631301,
|
||||
"narHash": "sha256-wAHeBX+aSiA+cAlWtwe5NisjMfrJq6WxrtT+CE7/tFM=",
|
||||
"owner": "sodiboo",
|
||||
"repo": "niri-flake",
|
||||
"rev": "f8899e60a1425d21a03a05ac2c069a85398039b5",
|
||||
"rev": "110e61e49828860499ead8bc539470f1261896ae",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -616,11 +632,11 @@
|
|||
"niri-unstable": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1771849386,
|
||||
"narHash": "sha256-CFvjBjS2LxbBMR3Lu6wZhME6ck3CXyKUufRoJA5tlmw=",
|
||||
"lastModified": 1772207631,
|
||||
"narHash": "sha256-Jkkg+KqshFO3CbTszVVpkKN2AOObYz+wMsM3ONo1z5g=",
|
||||
"owner": "YaLTeR",
|
||||
"repo": "niri",
|
||||
"rev": "2dc6f4482c4eeed75ea8b133d89cad8658d38429",
|
||||
"rev": "e708f546153f74acf33eb183b3b2992587a701e5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -653,11 +669,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771520882,
|
||||
"narHash": "sha256-9SeTZ4Pwr730YfT7V8Azb8GFbwk1ZwiQDAwft3qAD+o=",
|
||||
"lastModified": 1771992996,
|
||||
"narHash": "sha256-Y/ijH/unOPxzUicbla6yT/14RJgubUWnY2I2A6Ast2Q=",
|
||||
"owner": "nix-darwin",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "6a7fdcd5839ec8b135821179eea3b58092171bcf",
|
||||
"rev": "3bfa436c1975674ca465ce34586467be301ff509",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -713,11 +729,11 @@
|
|||
"nixpkgs": "nixpkgs_9"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771734689,
|
||||
"narHash": "sha256-/phvMgr1yutyAMjKnZlxkVplzxHiz60i4rc+gKzpwhg=",
|
||||
"lastModified": 1772341813,
|
||||
"narHash": "sha256-/PQ0ubBCMj/MVCWEI/XMStn55a8dIKsvztj4ZVLvUrQ=",
|
||||
"owner": "Mic92",
|
||||
"repo": "nix-index-database",
|
||||
"rev": "8f590b832326ab9699444f3a48240595954a4b10",
|
||||
"rev": "a2051ff239ce2e8a0148fa7a152903d9a78e854f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -821,11 +837,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771369470,
|
||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||
"lastModified": 1771848320,
|
||||
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -867,11 +883,11 @@
|
|||
},
|
||||
"nixpkgs-lib_3": {
|
||||
"locked": {
|
||||
"lastModified": 1769909678,
|
||||
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
|
||||
"lastModified": 1772328832,
|
||||
"narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "72716169fe93074c333e8d0173151350670b824c",
|
||||
"rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -882,11 +898,11 @@
|
|||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1771903837,
|
||||
"narHash": "sha256-sdaqdnsQCv3iifzxwB22tUwN/fSHoN7j2myFW5EIkGk=",
|
||||
"lastModified": 1772598333,
|
||||
"narHash": "sha256-YaHht/C35INEX3DeJQNWjNaTcPjYmBwwjFJ2jdtr+5U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e764fc9a405871f1f6ca3d1394fb422e0a0c3951",
|
||||
"rev": "fabb8c9deee281e50b1065002c9828f2cf7b2239",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -898,11 +914,11 @@
|
|||
},
|
||||
"nixpkgs-stable_2": {
|
||||
"locked": {
|
||||
"lastModified": 1771903837,
|
||||
"narHash": "sha256-sdaqdnsQCv3iifzxwB22tUwN/fSHoN7j2myFW5EIkGk=",
|
||||
"lastModified": 1772598333,
|
||||
"narHash": "sha256-YaHht/C35INEX3DeJQNWjNaTcPjYmBwwjFJ2jdtr+5U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e764fc9a405871f1f6ca3d1394fb422e0a0c3951",
|
||||
"rev": "fabb8c9deee281e50b1065002c9828f2cf7b2239",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -929,11 +945,11 @@
|
|||
},
|
||||
"nixpkgs_11": {
|
||||
"locked": {
|
||||
"lastModified": 1772016756,
|
||||
"narHash": "sha256-noRPhcPF6zI2Wc3khn2Uo01AMmLO7CLFRcgSN1CQXSg=",
|
||||
"lastModified": 1772671111,
|
||||
"narHash": "sha256-ftGqh2Ps1bjPUxGPoCOua3nKHY96IyFPvPjWthafre0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "154d55d78649878684bc797cec119e66cceed8b5",
|
||||
"rev": "9f8880d06df2c55d66df71495620ae1a51b68097",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -974,6 +990,22 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_14": {
|
||||
"locked": {
|
||||
"lastModified": 1766651565,
|
||||
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1770562336,
|
||||
|
|
@ -1056,11 +1088,11 @@
|
|||
},
|
||||
"nixpkgs_7": {
|
||||
"locked": {
|
||||
"lastModified": 1771848320,
|
||||
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
|
||||
"lastModified": 1772542754,
|
||||
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
|
||||
"rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -1088,11 +1120,11 @@
|
|||
},
|
||||
"nixpkgs_9": {
|
||||
"locked": {
|
||||
"lastModified": 1771369470,
|
||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||
"lastModified": 1772198003,
|
||||
"narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||
"rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -1133,6 +1165,7 @@
|
|||
"hjem": "hjem",
|
||||
"hjem-rum": "hjem-rum",
|
||||
"hudcore": "hudcore",
|
||||
"jay-src": "jay-src",
|
||||
"meat": "meat",
|
||||
"nil": "nil",
|
||||
"niri": "niri",
|
||||
|
|
@ -1148,6 +1181,7 @@
|
|||
"run0-shim": "run0-shim",
|
||||
"stash": "stash",
|
||||
"stasis": "stasis",
|
||||
"tuigreet": "tuigreet",
|
||||
"yoke": "yoke"
|
||||
}
|
||||
},
|
||||
|
|
@ -1330,11 +1364,11 @@
|
|||
"nixpkgs": "nixpkgs_13"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772174212,
|
||||
"narHash": "sha256-Rq3JnZAYzysIPdcVVM/ctKBARPGxKzzae2owVwqNPt8=",
|
||||
"lastModified": 1772661948,
|
||||
"narHash": "sha256-YNwi8tvrLkfmPd+4Sofk00Ev44qJReDTLV+VHpODy0U=",
|
||||
"owner": "saltnpepper97",
|
||||
"repo": "stasis",
|
||||
"rev": "6ce1a1391e1157457a588701b8ca21e3d72fd7f1",
|
||||
"rev": "043880f89ec3f7def5c92d7a6f2e32967dd7a9f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -1511,6 +1545,24 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"tuigreet": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_14"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771455425,
|
||||
"narHash": "sha256-vaiq9NaaXM4oNIXxcff3EXp3T2Mu3OLxFyFK+el4BZs=",
|
||||
"owner": "notashelf",
|
||||
"repo": "tuigreet",
|
||||
"rev": "b05e1c335cc79881b3c4822f8c8192c38efb2d80",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "notashelf",
|
||||
"repo": "tuigreet",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"unf": {
|
||||
"inputs": {
|
||||
"ndg": "ndg_2",
|
||||
|
|
@ -1550,11 +1602,11 @@
|
|||
"xwayland-satellite-unstable": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1771787042,
|
||||
"narHash": "sha256-7bM6Y4KldhKnfopSALF8XALxcX7ehkomXH9sPl4MXp0=",
|
||||
"lastModified": 1772429643,
|
||||
"narHash": "sha256-M+bAeCCcjBnVk6w/4dIVvXvpJwOKnXjwi/lDbaN6Yws=",
|
||||
"owner": "Supreeeme",
|
||||
"repo": "xwayland-satellite",
|
||||
"rev": "33c344fee50504089a447a8fef5878cf4f6215fc",
|
||||
"rev": "10f985b84cdbcc3bbf35b3e7e43d1b2a84fa9ce2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
11
flake.nix
11
flake.nix
|
|
@ -21,8 +21,6 @@
|
|||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
niri.url = "github:sodiboo/niri-flake";
|
||||
|
||||
hjem = {
|
||||
url = "github:feel-co/hjem";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
|
@ -34,6 +32,8 @@
|
|||
};
|
||||
|
||||
hudcore.url = "atagen:hudcore-plymouth";
|
||||
niri.url = "github:sodiboo/niri-flake";
|
||||
|
||||
niri-tag = {
|
||||
url = "atagen:niri-tag";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
|
@ -88,6 +88,13 @@
|
|||
|
||||
stash.url = "github:notashelf/stash";
|
||||
|
||||
jay-src = {
|
||||
url = "github:mahkoh/jay";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
tuigreet.url = "github:notashelf/tuigreet";
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
volumeOSD = pkgs.avizo;
|
||||
inherit (pkgs) playerctl;
|
||||
};
|
||||
quick.services."avizo-service" = "${lib.getExe' pkgs.avizo "avizo-service"}";
|
||||
quick.services."avizo-service".cmd = "${lib.getExe' pkgs.avizo "avizo-service"}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ let
|
|||
sponsorblock.id = "mnjggcdmjocbbbhaepdhchncahnbgone";
|
||||
vimium-c.id = "hfjbmagddngcpeloejdejnfgbamkjaeg";
|
||||
web-archives.id = "hkligngkgcpcolhcnkgccglchdafcnao";
|
||||
url-rewriter.id = "khncccgpokiedblbaahpfchghohlahje";
|
||||
# webrtc-leak-shield.id = "bppamachkoflopbagkdoflbgfjflfnfl";
|
||||
};
|
||||
extensionStrings = map (
|
||||
|
|
|
|||
74
graphical/desktop/jay.nix
Normal file
74
graphical/desktop/jay.nix
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
lib,
|
||||
getFlakePkg,
|
||||
...
|
||||
}:
|
||||
let
|
||||
jay = pkgs.jay.overrideAttrs (prev: {
|
||||
version = "unstable-slay-${toString inputs.jay-src.lastModified}";
|
||||
src = inputs.jay-src;
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock {
|
||||
lockFile = "${inputs.jay-src}/Cargo.lock";
|
||||
};
|
||||
patches = [
|
||||
./patches/0001-add-configurable-gap-between-tiled-windows.patch
|
||||
./patches/0002-add-position-and-size-animations-for-tiled-windows.patch
|
||||
./patches/0003-add-window-border-frames-when-gaps-are-enabled.patch
|
||||
./patches/0004-add-sequential-animation-mode-for-tiled-windows.patch
|
||||
./patches/0005-add-cursor-follows-focus-setting.patch
|
||||
./patches/0006-add-toggle-focus-between-floating-and-tiled-layers.patch
|
||||
./patches/0007-add-open-close-animations-for-tiled-windows.patch
|
||||
./patches/0008-add-directional-focus-navigation-for-floating-window.patch
|
||||
];
|
||||
});
|
||||
|
||||
jay-session = pkgs.writeShellScript "jay-session" ''
|
||||
systemctl --user import-environment
|
||||
dbus-update-activation-environment --all
|
||||
systemctl --user start jay-session-bridge.service &
|
||||
exec ${lib.getExe jay} run
|
||||
'';
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [ jay ];
|
||||
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
settings.default_session.command = "${lib.getExe (getFlakePkg inputs.tuigreet)} --sessions /etc/greetd/wayland-sessions --remember-session";
|
||||
};
|
||||
|
||||
environment.etc."greetd/wayland-sessions/jay.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Name=Jay
|
||||
Comment=A Wayland compositor written in Rust
|
||||
Exec=${jay-session}
|
||||
Type=Application
|
||||
DesktopNames=jay
|
||||
'';
|
||||
|
||||
# bridge service to activate graphical-session.target for direct-launched jay.
|
||||
# waits for jay IPC readiness before pulling in the target.
|
||||
systemd.user.services.jay-session-bridge = {
|
||||
unitConfig = {
|
||||
Description = "Activate graphical-session.target for direct-launched jay";
|
||||
BindsTo = [ "graphical-session.target" ];
|
||||
Before = [ "graphical-session.target" ];
|
||||
Wants = [ "graphical-session-pre.target" ];
|
||||
After = [ "graphical-session-pre.target" ];
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
# Jay's IPC goes through the Wayland socket (unlike niri's separate IPC socket),
|
||||
# so we can't use a jay subcommand without WAYLAND_DISPLAY already set.
|
||||
# Wait for Jay to create its wayland socket in XDG_RUNTIME_DIR instead.
|
||||
ExecStart = pkgs.writeShellScript "jay-ready" ''
|
||||
until find "$XDG_RUNTIME_DIR" -maxdepth 1 -name "wayland-*" ! -name "*.lock" -type s | grep -q .; do
|
||||
sleep 0.1
|
||||
done
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -57,10 +57,10 @@ spawn-at-startup "systemctl" "--user" "start" "startup-sound.service"
|
|||
window-rule {
|
||||
match app-id="Bitwarden"
|
||||
open-floating true
|
||||
default-column-width { proportion 0.2; }
|
||||
default-column-width { proportion 0.333333; }
|
||||
}
|
||||
window-rule {
|
||||
match app-id="chrome-listen.lobotomise.me__-Default"
|
||||
open-floating true
|
||||
default-column-width { proportion 0.2; }
|
||||
default-column-width { proportion 0.333333; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
From c46dbd6689b1a69e23068334e784b53d1bccebb0 Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 13:23:01 +1100
|
||||
Subject: [PATCH 01/10] add configurable gap between tiled windows
|
||||
|
||||
Adds a `gap` theme size that controls spacing between tiled windows.
|
||||
When gap > 0, windows use the gap as inter-window spacing instead of
|
||||
border_width. The workspace rect is also inset by the gap amount to
|
||||
provide outer padding around all windows.
|
||||
---
|
||||
jay-config/src/theme.rs | 7 +++++++
|
||||
src/config/handler.rs | 1 +
|
||||
src/theme.rs | 1 +
|
||||
src/tree/container.rs | 10 ++++++----
|
||||
src/tree/output.rs | 15 +++++++++++++++
|
||||
toml-config/src/config.rs | 1 +
|
||||
toml-config/src/config/parsers/theme.rs | 4 +++-
|
||||
toml-config/src/lib.rs | 1 +
|
||||
8 files changed, 35 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs
|
||||
index 64883b09..134498d2 100644
|
||||
--- a/jay-config/src/theme.rs
|
||||
+++ b/jay-config/src/theme.rs
|
||||
@@ -363,5 +363,12 @@ pub mod sized {
|
||||
///
|
||||
/// Default: 1
|
||||
const 04 => BAR_SEPARATOR_WIDTH,
|
||||
+ /// The gap between tiled windows in pixels.
|
||||
+ ///
|
||||
+ /// When set to a value greater than 0, windows are separated by this
|
||||
+ /// gap rather than by the border width.
|
||||
+ ///
|
||||
+ /// Default: 0
|
||||
+ const 05 => GAP,
|
||||
}
|
||||
}
|
||||
diff --git a/src/config/handler.rs b/src/config/handler.rs
|
||||
index 4ff91fe0..75f21dc5 100644
|
||||
--- a/src/config/handler.rs
|
||||
+++ b/src/config/handler.rs
|
||||
@@ -2466,6 +2466,7 @@ impl ConfigProxyHandler {
|
||||
BORDER_WIDTH => ThemeSized::border_width,
|
||||
BAR_HEIGHT => ThemeSized::bar_height,
|
||||
BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width,
|
||||
+ GAP => ThemeSized::gap,
|
||||
_ => return Err(CphError::UnknownSized(sized.0)),
|
||||
};
|
||||
Ok(sized)
|
||||
diff --git a/src/theme.rs b/src/theme.rs
|
||||
index f470993e..5a895623 100644
|
||||
--- a/src/theme.rs
|
||||
+++ b/src/theme.rs
|
||||
@@ -512,6 +512,7 @@ sizes! {
|
||||
bar_height = (0, 1000, 17),
|
||||
border_width = (0, 1000, 4),
|
||||
bar_separator_width = (0, 1000, 1),
|
||||
+ gap = (0, 1000, 0),
|
||||
}
|
||||
|
||||
pub const DEFAULT_FONT: &str = "monospace 8";
|
||||
diff --git a/src/tree/container.rs b/src/tree/container.rs
|
||||
index 7f48f2b1..587721ce 100644
|
||||
--- a/src/tree/container.rs
|
||||
+++ b/src/tree/container.rs
|
||||
@@ -447,6 +447,7 @@ impl ContainerNode {
|
||||
fn perform_split_layout(self: &Rc<Self>) {
|
||||
let sum_factors = self.sum_factors.get();
|
||||
let border_width = self.state.theme.sizes.border_width.get();
|
||||
+ let spacing = self.state.theme.sizes.gap.get().max(border_width);
|
||||
let title_height_tmp = self.state.theme.title_height();
|
||||
let title_plus_underline_height = self.state.theme.title_plus_underline_height();
|
||||
let split = self.split.get();
|
||||
@@ -482,7 +483,7 @@ impl ContainerNode {
|
||||
};
|
||||
let body = Rect::new_sized_saturating(x1, y1, width, height);
|
||||
child.body.set(body);
|
||||
- pos += body_size + border_width;
|
||||
+ pos += body_size + spacing;
|
||||
if split == ContainerSplit::Vertical {
|
||||
pos += title_plus_underline_height;
|
||||
}
|
||||
@@ -522,7 +523,7 @@ impl ContainerNode {
|
||||
};
|
||||
body = Rect::new_sized_saturating(x1, y1, width, height);
|
||||
child.body.set(body);
|
||||
- pos += size + border_width;
|
||||
+ pos += size + spacing;
|
||||
if split == ContainerSplit::Vertical {
|
||||
pos += title_plus_underline_height;
|
||||
}
|
||||
@@ -545,11 +546,12 @@ impl ContainerNode {
|
||||
|
||||
fn update_content_size(&self) {
|
||||
let border_width = self.state.theme.sizes.border_width.get();
|
||||
+ let spacing = self.state.theme.sizes.gap.get().max(border_width);
|
||||
let title_plus_underline_height = self.state.theme.title_plus_underline_height();
|
||||
let nc = self.num_children.get();
|
||||
match self.split.get() {
|
||||
ContainerSplit::Horizontal => {
|
||||
- let new_content_size = self.width.get().sub((nc - 1) as i32 * border_width).max(0);
|
||||
+ let new_content_size = self.width.get().sub((nc - 1) as i32 * spacing).max(0);
|
||||
self.content_width.set(new_content_size);
|
||||
self.content_height
|
||||
.set(self.height.get().sub(title_plus_underline_height).max(0));
|
||||
@@ -560,7 +562,7 @@ impl ContainerNode {
|
||||
.get()
|
||||
.sub(
|
||||
title_plus_underline_height
|
||||
- + (nc - 1) as i32 * (border_width + title_plus_underline_height),
|
||||
+ + (nc - 1) as i32 * (spacing + title_plus_underline_height),
|
||||
)
|
||||
.max(0);
|
||||
self.content_height.set(new_content_size);
|
||||
diff --git a/src/tree/output.rs b/src/tree/output.rs
|
||||
index fd232355..c3e2ae3a 100644
|
||||
--- a/src/tree/output.rs
|
||||
+++ b/src/tree/output.rs
|
||||
@@ -814,6 +814,21 @@ impl OutputNode {
|
||||
.set(bar_rect_with_separator_rel);
|
||||
self.bar_separator_rect.set(bar_separator_rect);
|
||||
self.bar_separator_rect_rel.set(bar_separator_rect_rel);
|
||||
+ let gap = self.state.theme.sizes.gap.get();
|
||||
+ if gap > 0 {
|
||||
+ workspace_rect = Rect::new_sized_saturating(
|
||||
+ workspace_rect.x1() + gap,
|
||||
+ workspace_rect.y1() + gap,
|
||||
+ (workspace_rect.width() - 2 * gap).max(0),
|
||||
+ (workspace_rect.height() - 2 * gap).max(0),
|
||||
+ );
|
||||
+ workspace_rect_rel = Rect::new_sized_saturating(
|
||||
+ workspace_rect_rel.x1() + gap,
|
||||
+ workspace_rect_rel.y1() + gap,
|
||||
+ (workspace_rect_rel.width() - 2 * gap).max(0),
|
||||
+ (workspace_rect_rel.height() - 2 * gap).max(0),
|
||||
+ );
|
||||
+ }
|
||||
self.workspace_rect.set(workspace_rect);
|
||||
self.workspace_rect_rel.set(workspace_rect_rel);
|
||||
self.update_tray_positions();
|
||||
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
|
||||
index 45a62ab6..6f8d1277 100644
|
||||
--- a/toml-config/src/config.rs
|
||||
+++ b/toml-config/src/config.rs
|
||||
@@ -209,6 +209,7 @@ pub struct Theme {
|
||||
pub bar_font: Option<String>,
|
||||
pub bar_position: Option<BarPosition>,
|
||||
pub bar_separator_width: Option<i32>,
|
||||
+ pub gap: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs
|
||||
index efcf6110..dae2674d 100644
|
||||
--- a/toml-config/src/config/parsers/theme.rs
|
||||
+++ b/toml-config/src/config/parsers/theme.rs
|
||||
@@ -63,7 +63,7 @@ impl Parser for ThemeParser<'_> {
|
||||
font,
|
||||
title_font,
|
||||
),
|
||||
- (bar_font, bar_position_val, bar_separator_width),
|
||||
+ (bar_font, bar_position_val, bar_separator_width, gap),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("attention-requested-bg-color")),
|
||||
@@ -93,6 +93,7 @@ impl Parser for ThemeParser<'_> {
|
||||
recover(opt(str("bar-font"))),
|
||||
recover(opt(str("bar-position"))),
|
||||
recover(opt(s32("bar-separator-width"))),
|
||||
+ recover(opt(s32("gap"))),
|
||||
),
|
||||
))?;
|
||||
macro_rules! color {
|
||||
@@ -146,6 +147,7 @@ impl Parser for ThemeParser<'_> {
|
||||
bar_font: bar_font.map(|f| f.value.to_string()),
|
||||
bar_position,
|
||||
bar_separator_width: bar_separator_width.despan(),
|
||||
+ gap: gap.despan(),
|
||||
})
|
||||
}
|
||||
}
|
||||
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
|
||||
index db9058e6..72c29b30 100644
|
||||
--- a/toml-config/src/lib.rs
|
||||
+++ b/toml-config/src/lib.rs
|
||||
@@ -980,6 +980,7 @@ impl State {
|
||||
size!(TITLE_HEIGHT, title_height);
|
||||
size!(BAR_HEIGHT, bar_height);
|
||||
size!(BAR_SEPARATOR_WIDTH, bar_separator_width);
|
||||
+ size!(GAP, gap);
|
||||
macro_rules! font {
|
||||
($fun:ident, $field:ident) => {
|
||||
if let Some(font) = &theme.$field {
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,576 @@
|
|||
From bb20a9359b5f520aa3d52803ce4e7fc565c74a93 Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 13:32:29 +1100
|
||||
Subject: [PATCH 02/10] add position and size animations for tiled windows
|
||||
|
||||
Animate window position and size changes when layout shifts (e.g.
|
||||
closing
|
||||
a window causes neighbors to slide and resize). Uses ease-out-cubic
|
||||
easing
|
||||
with configurable duration via theme.animation-duration. Animation
|
||||
deltas
|
||||
are matched by node ID to handle insertions/removals correctly. Includes
|
||||
seed_child_body() for cross-container moves.
|
||||
---
|
||||
jay-config/src/theme.rs | 6 +
|
||||
src/animation.rs | 37 ++++++
|
||||
src/config/handler.rs | 1 +
|
||||
src/main.rs | 1 +
|
||||
src/renderer.rs | 154 +++++++++++++++++++++++-
|
||||
src/theme.rs | 1 +
|
||||
src/tree/container.rs | 105 ++++++++++++++--
|
||||
toml-config/src/config.rs | 1 +
|
||||
toml-config/src/config/parsers/theme.rs | 4 +-
|
||||
toml-config/src/lib.rs | 1 +
|
||||
10 files changed, 298 insertions(+), 13 deletions(-)
|
||||
create mode 100644 src/animation.rs
|
||||
|
||||
diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs
|
||||
index 134498d2..53c53f8f 100644
|
||||
--- a/jay-config/src/theme.rs
|
||||
+++ b/jay-config/src/theme.rs
|
||||
@@ -370,5 +370,11 @@ pub mod sized {
|
||||
///
|
||||
/// Default: 0
|
||||
const 05 => GAP,
|
||||
+ /// The duration of window position/size animations in milliseconds.
|
||||
+ ///
|
||||
+ /// Set to 0 to disable animations.
|
||||
+ ///
|
||||
+ /// Default: 0
|
||||
+ const 06 => ANIMATION_DURATION,
|
||||
}
|
||||
}
|
||||
diff --git a/src/animation.rs b/src/animation.rs
|
||||
new file mode 100644
|
||||
index 00000000..328540b6
|
||||
--- /dev/null
|
||||
+++ b/src/animation.rs
|
||||
@@ -0,0 +1,37 @@
|
||||
+#[derive(Copy, Clone, Debug)]
|
||||
+pub struct PositionAnimation {
|
||||
+ pub offset_x: f64,
|
||||
+ pub offset_y: f64,
|
||||
+ pub offset_w: f64,
|
||||
+ pub offset_h: f64,
|
||||
+ pub start_usec: u64,
|
||||
+ pub duration_usec: u64,
|
||||
+}
|
||||
+
|
||||
+fn ease_out_cubic(t: f64) -> f64 {
|
||||
+ let t = 1.0 - t;
|
||||
+ 1.0 - t * t * t
|
||||
+}
|
||||
+
|
||||
+impl PositionAnimation {
|
||||
+ /// Returns the current animation offset, or None if the animation is complete.
|
||||
+ pub fn current_offset(&self, now_usec: u64) -> Option<(i32, i32, i32, i32)> {
|
||||
+ if self.duration_usec == 0 {
|
||||
+ return None;
|
||||
+ }
|
||||
+ let elapsed = now_usec.saturating_sub(self.start_usec);
|
||||
+ if elapsed >= self.duration_usec {
|
||||
+ return None;
|
||||
+ }
|
||||
+ let t = elapsed as f64 / self.duration_usec as f64;
|
||||
+ let remaining = 1.0 - ease_out_cubic(t);
|
||||
+ let dx = (self.offset_x * remaining).round() as i32;
|
||||
+ let dy = (self.offset_y * remaining).round() as i32;
|
||||
+ let dw = (self.offset_w * remaining).round() as i32;
|
||||
+ let dh = (self.offset_h * remaining).round() as i32;
|
||||
+ if dx == 0 && dy == 0 && dw == 0 && dh == 0 {
|
||||
+ return None;
|
||||
+ }
|
||||
+ Some((dx, dy, dw, dh))
|
||||
+ }
|
||||
+}
|
||||
diff --git a/src/config/handler.rs b/src/config/handler.rs
|
||||
index 75f21dc5..56135590 100644
|
||||
--- a/src/config/handler.rs
|
||||
+++ b/src/config/handler.rs
|
||||
@@ -2467,6 +2467,7 @@ impl ConfigProxyHandler {
|
||||
BAR_HEIGHT => ThemeSized::bar_height,
|
||||
BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width,
|
||||
GAP => ThemeSized::gap,
|
||||
+ ANIMATION_DURATION => ThemeSized::animation_duration,
|
||||
_ => return Err(CphError::UnknownSized(sized.0)),
|
||||
};
|
||||
Ok(sized)
|
||||
diff --git a/src/main.rs b/src/main.rs
|
||||
index 129978a5..b892bc44 100644
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -48,6 +48,7 @@ mod leaks;
|
||||
mod tracy;
|
||||
mod acceptor;
|
||||
mod allocator;
|
||||
+mod animation;
|
||||
mod async_engine;
|
||||
mod backend;
|
||||
mod backends;
|
||||
diff --git a/src/renderer.rs b/src/renderer.rs
|
||||
index 6acd59e5..a0e81fca 100644
|
||||
--- a/src/renderer.rs
|
||||
+++ b/src/renderer.rs
|
||||
@@ -13,6 +13,7 @@ use {
|
||||
scale::Scale,
|
||||
state::State,
|
||||
theme::Color,
|
||||
+ time::Time,
|
||||
tree::{
|
||||
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
|
||||
ToplevelNodeBase, WorkspaceNode,
|
||||
@@ -262,7 +263,7 @@ impl Renderer<'_> {
|
||||
}
|
||||
|
||||
pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) {
|
||||
- {
|
||||
+ if container.mono_child.is_some() {
|
||||
let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
let srgb = &srgb_srgb.linear;
|
||||
let rd = container.render_data.borrow_mut();
|
||||
@@ -320,17 +321,162 @@ impl Renderer<'_> {
|
||||
.node
|
||||
.node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
||||
} else {
|
||||
+ let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
+ let srgb = &srgb_srgb.linear;
|
||||
+ let theme = &self.state.theme;
|
||||
+ let colors = &theme.colors;
|
||||
+ let border_color = colors.border.get();
|
||||
+ let unfocused_title_bg = colors.unfocused_title_background.get();
|
||||
+ let focused_title_bg = colors.focused_title_background.get();
|
||||
+ let focused_inactive_title_bg = colors.focused_inactive_title_background.get();
|
||||
+ let attention_title_bg = colors.attention_requested_background.get();
|
||||
+ let separator_color = colors.separator.get();
|
||||
+ let tpuh = theme.title_plus_underline_height();
|
||||
+ let tuh = theme.title_underline_height();
|
||||
+ let have_active = container.children.iter().any(|c| c.active.get());
|
||||
+ let gap = theme.sizes.gap.get();
|
||||
+ let bw = theme.sizes.border_width.get();
|
||||
+ let rd = container.render_data.borrow();
|
||||
+ self.base
|
||||
+ .fill_boxes2(&rd.border_rects, &border_color, srgb, x, y);
|
||||
+ let last_active_rect = rd.last_active_rect;
|
||||
+ drop(rd);
|
||||
+ let now_usec = Time::now_unchecked().usec();
|
||||
+ let mut has_active_animation = false;
|
||||
for child in container.children.iter() {
|
||||
let body = child.body.get();
|
||||
if body.x1() >= container.width.get() || body.y1() >= container.height.get() {
|
||||
break;
|
||||
}
|
||||
+ let (anim_dx, anim_dy, anim_dw, anim_dh) =
|
||||
+ if let Some(anim) = child.body_animation.get() {
|
||||
+ match anim.current_offset(now_usec) {
|
||||
+ Some(offsets) => {
|
||||
+ has_active_animation = true;
|
||||
+ offsets
|
||||
+ }
|
||||
+ None => {
|
||||
+ child.body_animation.set(None);
|
||||
+ has_active_animation = true;
|
||||
+ (0, 0, 0, 0)
|
||||
+ }
|
||||
+ }
|
||||
+ } else {
|
||||
+ (0, 0, 0, 0)
|
||||
+ };
|
||||
+ // Render title background and underline with animation offset.
|
||||
+ let title_rect = child.title_rect.get();
|
||||
+ if tpuh > 0 {
|
||||
+ let anim_title = title_rect.move_(anim_dx, anim_dy);
|
||||
+ let title_bg = if child.active.get() {
|
||||
+ &focused_title_bg
|
||||
+ } else if child.attention_requested.get() {
|
||||
+ &attention_title_bg
|
||||
+ } else if !have_active
|
||||
+ && last_active_rect == Some(title_rect)
|
||||
+ {
|
||||
+ &focused_inactive_title_bg
|
||||
+ } else {
|
||||
+ &unfocused_title_bg
|
||||
+ };
|
||||
+ self.base.fill_boxes2(
|
||||
+ std::slice::from_ref(&anim_title),
|
||||
+ title_bg,
|
||||
+ srgb,
|
||||
+ x,
|
||||
+ y,
|
||||
+ );
|
||||
+ if tuh > 0 {
|
||||
+ let underline = Rect::new_sized_saturating(
|
||||
+ anim_title.x1(),
|
||||
+ anim_title.y2(),
|
||||
+ anim_title.width(),
|
||||
+ tuh,
|
||||
+ );
|
||||
+ self.base.fill_boxes2(
|
||||
+ std::slice::from_ref(&underline),
|
||||
+ &separator_color,
|
||||
+ srgb,
|
||||
+ x,
|
||||
+ y,
|
||||
+ );
|
||||
+ }
|
||||
+ let tt = &*child.title_tex.borrow();
|
||||
+ for (scale, tex) in tt {
|
||||
+ if *scale != self.base.scale {
|
||||
+ continue;
|
||||
+ }
|
||||
+ if let Some(tex) = tex.texture() {
|
||||
+ let rect = anim_title.move_(x, y);
|
||||
+ let bounds = self.base.scale_rect(rect);
|
||||
+ let (tx, ty) = self.base.scale_point(rect.x1(), rect.y1());
|
||||
+ self.base.render_texture(
|
||||
+ &tex,
|
||||
+ None,
|
||||
+ tx,
|
||||
+ ty,
|
||||
+ None,
|
||||
+ None,
|
||||
+ self.base.scale,
|
||||
+ Some(&bounds),
|
||||
+ None,
|
||||
+ AcquireSync::None,
|
||||
+ ReleaseSync::None,
|
||||
+ false,
|
||||
+ srgb_srgb,
|
||||
+ AlphaMode::PremultipliedElectrical,
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ let anim_w = (body.width() + anim_dw).max(1);
|
||||
+ let anim_h = (body.height() + anim_dh).max(1);
|
||||
+ let anim_body = Rect::new_sized_saturating(
|
||||
+ body.x1() + anim_dx,
|
||||
+ body.y1() + anim_dy,
|
||||
+ anim_w,
|
||||
+ anim_h,
|
||||
+ );
|
||||
+ // Fill the animated body so that shrinking windows show a
|
||||
+ // solid background instead of the underlying content in the
|
||||
+ // gap between the committed surface and the animated border.
|
||||
+ if anim_w > body.width() || anim_h > body.height() {
|
||||
+ self.base.fill_boxes2(
|
||||
+ std::slice::from_ref(&anim_body),
|
||||
+ &border_color,
|
||||
+ srgb,
|
||||
+ x,
|
||||
+ y,
|
||||
+ );
|
||||
+ }
|
||||
+ let clip_w = anim_w.min(body.width());
|
||||
+ let clip_h = anim_h.min(body.height());
|
||||
+ let body = Rect::new_sized_saturating(
|
||||
+ anim_body.x1(),
|
||||
+ anim_body.y1(),
|
||||
+ clip_w,
|
||||
+ clip_h,
|
||||
+ );
|
||||
let body = body.move_(x, y);
|
||||
let body = self.base.scale_rect(body);
|
||||
let content = child.content.get();
|
||||
- child
|
||||
- .node
|
||||
- .node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
||||
+ child.node.node_render(
|
||||
+ self,
|
||||
+ x + content.x1() + anim_dx,
|
||||
+ y + content.y1() + anim_dy,
|
||||
+ Some(&body),
|
||||
+ );
|
||||
+ }
|
||||
+ if has_active_animation {
|
||||
+ let abs_x = container.abs_x1.get();
|
||||
+ let abs_y = container.abs_y1.get();
|
||||
+ let bw = if gap > 0 { bw } else { 0 };
|
||||
+ self.state.damage(Rect::new_sized_saturating(
|
||||
+ abs_x - bw,
|
||||
+ abs_y - bw,
|
||||
+ container.width.get() + 2 * bw,
|
||||
+ container.height.get() + 2 * bw,
|
||||
+ ));
|
||||
}
|
||||
}
|
||||
self.render_tl_aux(container.tl_data(), None, false);
|
||||
diff --git a/src/theme.rs b/src/theme.rs
|
||||
index 5a895623..52c6392d 100644
|
||||
--- a/src/theme.rs
|
||||
+++ b/src/theme.rs
|
||||
@@ -513,6 +513,7 @@ sizes! {
|
||||
border_width = (0, 1000, 4),
|
||||
bar_separator_width = (0, 1000, 1),
|
||||
gap = (0, 1000, 0),
|
||||
+ animation_duration = (0, 5000, 0),
|
||||
}
|
||||
|
||||
pub const DEFAULT_FONT: &str = "monospace 8";
|
||||
diff --git a/src/tree/container.rs b/src/tree/container.rs
|
||||
index 587721ce..ea9fb62b 100644
|
||||
--- a/src/tree/container.rs
|
||||
+++ b/src/tree/container.rs
|
||||
@@ -1,5 +1,6 @@
|
||||
use {
|
||||
crate::{
|
||||
+ animation::PositionAnimation,
|
||||
backend::ButtonState,
|
||||
cursor::KnownCursor,
|
||||
cursor_user::CursorUser,
|
||||
@@ -16,6 +17,7 @@ use {
|
||||
scale::Scale,
|
||||
state::State,
|
||||
text::TextTexture,
|
||||
+ time::Time,
|
||||
tree::{
|
||||
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node,
|
||||
NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination,
|
||||
@@ -125,7 +127,7 @@ pub struct ContainerNode {
|
||||
layout_scheduled: Cell<bool>,
|
||||
compute_render_positions_scheduled: Cell<bool>,
|
||||
render_titles_scheduled: Cell<bool>,
|
||||
- num_children: NumCell<usize>,
|
||||
+ pub num_children: NumCell<usize>,
|
||||
pub children: LinkedList<ContainerChild>,
|
||||
focus_history: LinkedList<NodeRef<ContainerChild>>,
|
||||
child_nodes: RefCell<AHashMap<NodeId, LinkedNode<ContainerChild>>>,
|
||||
@@ -158,6 +160,7 @@ pub struct ContainerChild {
|
||||
pub body: Cell<Rect>,
|
||||
pub content: Cell<Rect>,
|
||||
factor: Cell<f64>,
|
||||
+ pub body_animation: Cell<Option<PositionAnimation>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -211,6 +214,7 @@ impl ContainerNode {
|
||||
title_rect: Default::default(),
|
||||
focus_history: Default::default(),
|
||||
attention_requested: Cell::new(false),
|
||||
+ body_animation: Default::default(),
|
||||
});
|
||||
let child_node_ref = child_node.clone();
|
||||
let mut child_nodes = AHashMap::new();
|
||||
@@ -332,6 +336,7 @@ impl ContainerNode {
|
||||
title_rect: Default::default(),
|
||||
focus_history: Default::default(),
|
||||
attention_requested: Default::default(),
|
||||
+ body_animation: Default::default(),
|
||||
});
|
||||
let r = link.to_ref();
|
||||
links.insert(new.node_id(), link);
|
||||
@@ -362,6 +367,27 @@ impl ContainerNode {
|
||||
self.cancel_seat_ops();
|
||||
}
|
||||
|
||||
+ /// Seed a child's body position from an absolute rect, converting to
|
||||
+ /// container-local coordinates. This allows `perform_layout` to compute
|
||||
+ /// a meaningful animation delta for children that just moved between
|
||||
+ /// containers.
|
||||
+ fn seed_child_body(&self, child_id: NodeId, abs_body: Rect) {
|
||||
+ if let Some(link) = self.child_nodes.borrow().get(&child_id) {
|
||||
+ let local = abs_body.move_(-self.abs_x1.get(), -self.abs_y1.get());
|
||||
+ link.to_ref().body.set(local);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// Get a child's absolute body rect.
|
||||
+ fn child_abs_body(&self, child_id: NodeId) -> Option<Rect> {
|
||||
+ self.child_nodes.borrow().get(&child_id).map(|link| {
|
||||
+ link.to_ref()
|
||||
+ .body
|
||||
+ .get()
|
||||
+ .move_(self.abs_x1.get(), self.abs_y1.get())
|
||||
+ })
|
||||
+ }
|
||||
+
|
||||
fn cancel_seat_ops(&self) {
|
||||
let mut seats = self.cursors.borrow_mut();
|
||||
for seat in seats.values_mut() {
|
||||
@@ -382,11 +408,16 @@ impl ContainerNode {
|
||||
}
|
||||
|
||||
fn damage(&self) {
|
||||
+ let bw = if self.state.theme.sizes.gap.get() > 0 {
|
||||
+ self.state.theme.sizes.border_width.get()
|
||||
+ } else {
|
||||
+ 0
|
||||
+ };
|
||||
self.state.damage(Rect::new_sized_saturating(
|
||||
- self.abs_x1.get(),
|
||||
- self.abs_y1.get(),
|
||||
- self.width.get(),
|
||||
- self.height.get(),
|
||||
+ self.abs_x1.get() - bw,
|
||||
+ self.abs_y1.get() - bw,
|
||||
+ self.width.get() + 2 * bw,
|
||||
+ self.height.get() + 2 * bw,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -404,11 +435,60 @@ impl ContainerNode {
|
||||
return;
|
||||
}
|
||||
self.layout_scheduled.set(false);
|
||||
+ let animation_duration = self.state.theme.sizes.animation_duration.get();
|
||||
+ let is_split = self.mono_child.is_none();
|
||||
+ let old_bodies: SmallVec<[(NodeId, Rect); 8]> = if animation_duration > 0 && is_split {
|
||||
+ self.children
|
||||
+ .iter()
|
||||
+ .map(|c| (c.node.node_id(), c.body.get()))
|
||||
+ .collect()
|
||||
+ } else {
|
||||
+ SmallVec::new()
|
||||
+ };
|
||||
if let Some(child) = self.mono_child.get() {
|
||||
self.perform_mono_layout(&child);
|
||||
} else {
|
||||
self.perform_split_layout();
|
||||
}
|
||||
+ if animation_duration > 0 && is_split && !old_bodies.is_empty() {
|
||||
+ let now_usec = Time::now_unchecked().usec();
|
||||
+ for child in self.children.iter() {
|
||||
+ let child_id = child.node.node_id();
|
||||
+ let old = match old_bodies.iter().find(|(id, _)| *id == child_id) {
|
||||
+ Some((_, rect)) if !rect.is_empty() => *rect,
|
||||
+ _ => continue,
|
||||
+ };
|
||||
+ let new_body = child.body.get();
|
||||
+ let dx = old.x1() - new_body.x1();
|
||||
+ let dy = old.y1() - new_body.y1();
|
||||
+ let dw = old.width() - new_body.width();
|
||||
+ let dh = old.height() - new_body.height();
|
||||
+ if dx == 0 && dy == 0 && dw == 0 && dh == 0 {
|
||||
+ continue;
|
||||
+ }
|
||||
+ let (start_dx, start_dy, start_dw, start_dh) =
|
||||
+ if let Some(prev) = child.body_animation.get() {
|
||||
+ let (cur_dx, cur_dy, cur_dw, cur_dh) =
|
||||
+ prev.current_offset(now_usec).unwrap_or((0, 0, 0, 0));
|
||||
+ (
|
||||
+ dx as f64 + cur_dx as f64,
|
||||
+ dy as f64 + cur_dy as f64,
|
||||
+ dw as f64 + cur_dw as f64,
|
||||
+ dh as f64 + cur_dh as f64,
|
||||
+ )
|
||||
+ } else {
|
||||
+ (dx as f64, dy as f64, dw as f64, dh as f64)
|
||||
+ };
|
||||
+ child.body_animation.set(Some(PositionAnimation {
|
||||
+ offset_x: start_dx,
|
||||
+ offset_y: start_dy,
|
||||
+ offset_w: start_dw,
|
||||
+ offset_h: start_dh,
|
||||
+ start_usec: now_usec,
|
||||
+ duration_usec: (animation_duration as u64) * 1000,
|
||||
+ }));
|
||||
+ }
|
||||
+ }
|
||||
self.state.tree_changed();
|
||||
// log::info!("perform_layout");
|
||||
self.schedule_render_titles();
|
||||
@@ -856,8 +936,8 @@ impl ContainerNode {
|
||||
} else {
|
||||
rd.title_rects.push(rect);
|
||||
}
|
||||
- if !mono {
|
||||
- let rect = Rect::new_sized_saturating(rect.x1(), rect.y2(), rect.width(), 1);
|
||||
+ if !mono && tuh > 0 {
|
||||
+ let rect = Rect::new_sized_saturating(rect.x1(), rect.y2(), rect.width(), tuh);
|
||||
rd.underline_rects.push(rect);
|
||||
}
|
||||
let tt = &*child.title_tex.borrow();
|
||||
@@ -1103,8 +1183,12 @@ impl ContainerNode {
|
||||
{
|
||||
self.activate_child2(&neighbor, true);
|
||||
}
|
||||
+ let abs_body = self.child_abs_body(child.node_id());
|
||||
self.cnode_remove_child2(&*child, true);
|
||||
- cn.insert_child(child, direction);
|
||||
+ cn.insert_child(child.clone(), direction);
|
||||
+ if let Some(ab) = abs_body {
|
||||
+ cn.seed_child_body(child.node_id(), ab);
|
||||
+ }
|
||||
return;
|
||||
}
|
||||
match prev {
|
||||
@@ -1133,11 +1217,15 @@ impl ContainerNode {
|
||||
return;
|
||||
}
|
||||
};
|
||||
+ let abs_body = self.child_abs_body(child.node_id());
|
||||
self.cnode_remove_child2(&*child, true);
|
||||
match prev {
|
||||
true => parent.add_child_before(&*neighbor, child.clone()),
|
||||
false => parent.add_child_after(&*neighbor, child.clone()),
|
||||
}
|
||||
+ if let Some(ab) = abs_body {
|
||||
+ parent.seed_child_body(child.node_id(), ab);
|
||||
+ }
|
||||
}
|
||||
|
||||
pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) {
|
||||
@@ -1915,6 +2003,7 @@ impl ContainingNode for ContainerNode {
|
||||
title_rect: Cell::new(node.title_rect.get()),
|
||||
focus_history: Cell::new(None),
|
||||
attention_requested: Cell::new(false),
|
||||
+ body_animation: Default::default(),
|
||||
});
|
||||
if let Some(fh) = node.focus_history.take() {
|
||||
link.focus_history.set(Some(fh.append(link.to_ref())));
|
||||
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
|
||||
index 6f8d1277..03f3a0ca 100644
|
||||
--- a/toml-config/src/config.rs
|
||||
+++ b/toml-config/src/config.rs
|
||||
@@ -210,6 +210,7 @@ pub struct Theme {
|
||||
pub bar_position: Option<BarPosition>,
|
||||
pub bar_separator_width: Option<i32>,
|
||||
pub gap: Option<i32>,
|
||||
+ pub animation_duration: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs
|
||||
index dae2674d..977dbc14 100644
|
||||
--- a/toml-config/src/config/parsers/theme.rs
|
||||
+++ b/toml-config/src/config/parsers/theme.rs
|
||||
@@ -63,7 +63,7 @@ impl Parser for ThemeParser<'_> {
|
||||
font,
|
||||
title_font,
|
||||
),
|
||||
- (bar_font, bar_position_val, bar_separator_width, gap),
|
||||
+ (bar_font, bar_position_val, bar_separator_width, gap, animation_duration),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("attention-requested-bg-color")),
|
||||
@@ -94,6 +94,7 @@ impl Parser for ThemeParser<'_> {
|
||||
recover(opt(str("bar-position"))),
|
||||
recover(opt(s32("bar-separator-width"))),
|
||||
recover(opt(s32("gap"))),
|
||||
+ recover(opt(s32("animation-duration"))),
|
||||
),
|
||||
))?;
|
||||
macro_rules! color {
|
||||
@@ -148,6 +149,7 @@ impl Parser for ThemeParser<'_> {
|
||||
bar_position,
|
||||
bar_separator_width: bar_separator_width.despan(),
|
||||
gap: gap.despan(),
|
||||
+ animation_duration: animation_duration.despan(),
|
||||
})
|
||||
}
|
||||
}
|
||||
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
|
||||
index 72c29b30..e60d6837 100644
|
||||
--- a/toml-config/src/lib.rs
|
||||
+++ b/toml-config/src/lib.rs
|
||||
@@ -981,6 +981,7 @@ impl State {
|
||||
size!(BAR_HEIGHT, bar_height);
|
||||
size!(BAR_SEPARATOR_WIDTH, bar_separator_width);
|
||||
size!(GAP, gap);
|
||||
+ size!(ANIMATION_DURATION, animation_duration);
|
||||
macro_rules! font {
|
||||
($fun:ident, $field:ident) => {
|
||||
if let Some(font) = &theme.$field {
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
From 595b88b6a1d321086f33e213f86e80055e26bbe9 Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 13:35:58 +1100
|
||||
Subject: [PATCH 03/10] add window border frames when gaps are enabled
|
||||
|
||||
When gap > 0, draw a full border frame (left/right/top/bottom) around
|
||||
each non-container child window instead of only separator borders
|
||||
between
|
||||
windows. Border rects are computed per-child in compute_render_positions
|
||||
and stored on ContainerChild, then rendered with animation offsets in
|
||||
the
|
||||
renderer loop. Focus changes trigger damage when gaps are enabled so
|
||||
border color updates are visible.
|
||||
---
|
||||
src/renderer.rs | 40 ++++++++++++++++++++++++++++++++++++
|
||||
src/tree/container.rs | 48 ++++++++++++++++++++++++++++++++++++++++---
|
||||
2 files changed, 85 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/src/renderer.rs b/src/renderer.rs
|
||||
index a0e81fca..c2622fac 100644
|
||||
--- a/src/renderer.rs
|
||||
+++ b/src/renderer.rs
|
||||
@@ -326,6 +326,7 @@ impl Renderer<'_> {
|
||||
let theme = &self.state.theme;
|
||||
let colors = &theme.colors;
|
||||
let border_color = colors.border.get();
|
||||
+ let focused_border_color = colors.focused_title_background.get();
|
||||
let unfocused_title_bg = colors.unfocused_title_background.get();
|
||||
let focused_title_bg = colors.focused_title_background.get();
|
||||
let focused_inactive_title_bg = colors.focused_inactive_title_background.get();
|
||||
@@ -429,6 +430,7 @@ impl Renderer<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
+ // Compute the animated body rect.
|
||||
let anim_w = (body.width() + anim_dw).max(1);
|
||||
let anim_h = (body.height() + anim_dh).max(1);
|
||||
let anim_body = Rect::new_sized_saturating(
|
||||
@@ -437,6 +439,44 @@ impl Renderer<'_> {
|
||||
anim_w,
|
||||
anim_h,
|
||||
);
|
||||
+ // Draw per-child border frames at the animated position/size.
|
||||
+ if gap > 0 && !child.node.node_is_container() {
|
||||
+ let c = if child.border_color_is_focused.get() {
|
||||
+ &focused_border_color
|
||||
+ } else {
|
||||
+ &border_color
|
||||
+ };
|
||||
+ let top_y = if tpuh > 0 {
|
||||
+ title_rect.y1() + anim_dy
|
||||
+ } else {
|
||||
+ anim_body.y1()
|
||||
+ };
|
||||
+ let full_h = anim_body.y2() - top_y;
|
||||
+ let full_w = anim_body.width();
|
||||
+ let frame_rects = [
|
||||
+ Rect::new_sized_saturating(anim_body.x1() - bw, top_y, bw, full_h),
|
||||
+ Rect::new_sized_saturating(anim_body.x2(), top_y, bw, full_h),
|
||||
+ Rect::new_sized_saturating(anim_body.x1() - bw, top_y - bw, full_w + 2 * bw, bw),
|
||||
+ Rect::new_sized_saturating(anim_body.x1() - bw, anim_body.y2(), full_w + 2 * bw, bw),
|
||||
+ ];
|
||||
+ self.base.fill_boxes2(&frame_rects, c, srgb, x, y);
|
||||
+ } else if gap == 0 {
|
||||
+ let borders = child.border_rects.borrow();
|
||||
+ if !borders.is_empty() {
|
||||
+ let c = if child.border_color_is_focused.get() {
|
||||
+ &focused_border_color
|
||||
+ } else {
|
||||
+ &border_color
|
||||
+ };
|
||||
+ self.base.fill_boxes2(
|
||||
+ &borders,
|
||||
+ c,
|
||||
+ srgb,
|
||||
+ x + anim_dx,
|
||||
+ y + anim_dy,
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
// Fill the animated body so that shrinking windows show a
|
||||
// solid background instead of the underlying content in the
|
||||
// gap between the committed surface and the animated border.
|
||||
diff --git a/src/tree/container.rs b/src/tree/container.rs
|
||||
index ea9fb62b..25f95415 100644
|
||||
--- a/src/tree/container.rs
|
||||
+++ b/src/tree/container.rs
|
||||
@@ -161,6 +161,8 @@ pub struct ContainerChild {
|
||||
pub content: Cell<Rect>,
|
||||
factor: Cell<f64>,
|
||||
pub body_animation: Cell<Option<PositionAnimation>>,
|
||||
+ pub border_rects: RefCell<SmallVec<[Rect; 4]>>,
|
||||
+ pub border_color_is_focused: Cell<bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -215,6 +217,8 @@ impl ContainerNode {
|
||||
focus_history: Default::default(),
|
||||
attention_requested: Cell::new(false),
|
||||
body_animation: Default::default(),
|
||||
+ border_rects: Default::default(),
|
||||
+ border_color_is_focused: Default::default(),
|
||||
});
|
||||
let child_node_ref = child_node.clone();
|
||||
let mut child_nodes = AHashMap::new();
|
||||
@@ -337,6 +341,8 @@ impl ContainerNode {
|
||||
focus_history: Default::default(),
|
||||
attention_requested: Default::default(),
|
||||
body_animation: Default::default(),
|
||||
+ border_rects: Default::default(),
|
||||
+ border_color_is_focused: Default::default(),
|
||||
});
|
||||
let r = link.to_ref();
|
||||
links.insert(new.node_id(), link);
|
||||
@@ -905,6 +911,7 @@ impl ContainerNode {
|
||||
let mono = self.mono_child.is_some();
|
||||
let split = self.split.get();
|
||||
let have_active = self.children.iter().any(|c| c.active.get());
|
||||
+ let gap = theme.sizes.gap.get();
|
||||
let abs_x = self.abs_x1.get();
|
||||
let abs_y = self.abs_y1.get();
|
||||
for (i, child) in self.children.iter().enumerate() {
|
||||
@@ -917,16 +924,36 @@ impl ContainerNode {
|
||||
rect.height() + tuh,
|
||||
));
|
||||
}
|
||||
- if i > 0 {
|
||||
- let rect = if mono {
|
||||
+ // Per-child border rects
|
||||
+ let mut child_borders = child.border_rects.borrow_mut();
|
||||
+ child_borders.clear();
|
||||
+ if gap > 0 && !mono && !child.node.node_is_container() {
|
||||
+ // Frame around each window (title + body)
|
||||
+ let body = child.body.get();
|
||||
+ let top_y = if tpuh > 0 { rect.y1() } else { body.y1() };
|
||||
+ let full_h = body.y2() - top_y;
|
||||
+ let full_w = body.width();
|
||||
+ // Left
|
||||
+ child_borders.push(Rect::new_sized_saturating(body.x1() - bw, top_y, bw, full_h));
|
||||
+ // Right
|
||||
+ child_borders.push(Rect::new_sized_saturating(body.x2(), top_y, bw, full_h));
|
||||
+ // Top
|
||||
+ child_borders.push(Rect::new_sized_saturating(body.x1() - bw, top_y - bw, full_w + 2 * bw, bw));
|
||||
+ // Bottom
|
||||
+ child_borders.push(Rect::new_sized_saturating(body.x1() - bw, body.y2(), full_w + 2 * bw, bw));
|
||||
+ } else if gap == 0 && i > 0 {
|
||||
+ // Separator borders when gap == 0
|
||||
+ let sep = if mono {
|
||||
Rect::new_sized_saturating(rect.x1() - bw, 0, bw, th)
|
||||
} else if split == ContainerSplit::Horizontal {
|
||||
Rect::new_sized_saturating(rect.x1() - bw, 0, bw, cheight)
|
||||
} else {
|
||||
Rect::new_sized_saturating(0, rect.y1() - bw, cwidth, bw)
|
||||
};
|
||||
- rd.border_rects.push(rect);
|
||||
+ rd.border_rects.push(sep);
|
||||
}
|
||||
+ drop(child_borders);
|
||||
+ child.border_color_is_focused.set(child.active.get());
|
||||
if child.active.get() {
|
||||
rd.active_title_rects.push(rect);
|
||||
} else if child.attention_requested.get() {
|
||||
@@ -1267,6 +1294,9 @@ impl ContainerNode {
|
||||
// log::info!("node_child_active_changed");
|
||||
self.schedule_render_titles();
|
||||
self.schedule_compute_render_positions();
|
||||
+ if self.state.theme.sizes.gap.get() > 0 && self.toplevel_data.visible.get() {
|
||||
+ self.damage();
|
||||
+ }
|
||||
if let Some(parent) = self.toplevel_data.parent.get() {
|
||||
parent.node_child_active_changed(self.deref(), active, depth + 1);
|
||||
}
|
||||
@@ -2004,6 +2034,8 @@ impl ContainingNode for ContainerNode {
|
||||
focus_history: Cell::new(None),
|
||||
attention_requested: Cell::new(false),
|
||||
body_animation: Default::default(),
|
||||
+ border_rects: Default::default(),
|
||||
+ border_color_is_focused: Default::default(),
|
||||
});
|
||||
if let Some(fh) = node.focus_history.take() {
|
||||
link.focus_history.set(Some(fh.append(link.to_ref())));
|
||||
@@ -2058,6 +2090,16 @@ impl ContainingNode for ContainerNode {
|
||||
};
|
||||
let num_children = self.num_children.fetch_sub(1) - 1;
|
||||
if num_children == 0 {
|
||||
+ let gap = self.state.theme.sizes.gap.get();
|
||||
+ if gap > 0 {
|
||||
+ let bw = self.state.theme.sizes.border_width.get();
|
||||
+ self.state.damage(Rect::new_sized_saturating(
|
||||
+ self.abs_x1.get() - bw,
|
||||
+ self.abs_y1.get() - bw,
|
||||
+ self.width.get() + 2 * bw,
|
||||
+ self.height.get() + 2 * bw,
|
||||
+ ));
|
||||
+ }
|
||||
self.tl_destroy();
|
||||
return;
|
||||
}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,545 @@
|
|||
From 4555defec92f59b019f116b3fae0d914f171eea2 Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 13:40:54 +1100
|
||||
Subject: [PATCH 04/10] add sequential animation mode for tiled windows
|
||||
|
||||
Introduces AnimationMode enum with Concurrent (default) and Sequential
|
||||
variants. Sequential mode automatically picks the phase order per-window
|
||||
based on whether it is growing or shrinking: growing windows move first
|
||||
then resize, shrinking windows resize first then move.
|
||||
---
|
||||
jay-config/src/_private/client.rs | 12 +++-
|
||||
jay-config/src/_private/ipc.rs | 9 ++-
|
||||
jay-config/src/theme.rs | 24 ++++++++
|
||||
src/animation.rs | 48 +++++++++++++--
|
||||
src/config/handler.rs | 22 ++++++-
|
||||
src/theme.rs | 35 ++++++++++-
|
||||
src/tree/container.rs | 82 ++++++++++++++++++++++++-
|
||||
toml-config/src/config.rs | 3 +-
|
||||
toml-config/src/config/parsers/theme.rs | 19 +++++-
|
||||
toml-config/src/lib.rs | 7 ++-
|
||||
10 files changed, 244 insertions(+), 17 deletions(-)
|
||||
|
||||
diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs
|
||||
index 7c71461e..c72c17f1 100644
|
||||
--- a/jay-config/src/_private/client.rs
|
||||
+++ b/jay-config/src/_private/client.rs
|
||||
@@ -26,7 +26,7 @@ use {
|
||||
},
|
||||
logging::LogLevel,
|
||||
tasks::{JoinHandle, JoinSlot},
|
||||
- theme::{BarPosition, Color, colors::Colorable, sized::Resizable},
|
||||
+ theme::{AnimationMode, BarPosition, Color, colors::Colorable, sized::Resizable},
|
||||
timer::Timer,
|
||||
video::{
|
||||
BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, Mode, TearingMode,
|
||||
@@ -1035,6 +1035,16 @@ impl ConfigClient {
|
||||
position
|
||||
}
|
||||
|
||||
+ pub fn set_animation_mode(&self, mode: AnimationMode) {
|
||||
+ self.send(&ClientMessage::SetAnimationMode { mode });
|
||||
+ }
|
||||
+
|
||||
+ pub fn get_animation_mode(&self) -> AnimationMode {
|
||||
+ let res = self.send_with_response(&ClientMessage::GetAnimationMode);
|
||||
+ get_response!(res, AnimationMode::Concurrent, GetAnimationMode { mode });
|
||||
+ mode
|
||||
+ }
|
||||
+
|
||||
pub fn set_middle_click_paste_enabled(&self, enabled: bool) {
|
||||
self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled });
|
||||
}
|
||||
diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs
|
||||
index ab1de845..b0273f91 100644
|
||||
--- a/jay-config/src/_private/ipc.rs
|
||||
+++ b/jay-config/src/_private/ipc.rs
|
||||
@@ -10,7 +10,7 @@ use {
|
||||
},
|
||||
keyboard::{Group, Keymap, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
- theme::{BarPosition, Color, colors::Colorable, sized::Resizable},
|
||||
+ theme::{AnimationMode, BarPosition, Color, colors::Colorable, sized::Resizable},
|
||||
timer::Timer,
|
||||
video::{
|
||||
BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, TearingMode,
|
||||
@@ -817,6 +817,10 @@ pub enum ClientMessage<'a> {
|
||||
position: BarPosition,
|
||||
},
|
||||
GetBarPosition,
|
||||
+ SetAnimationMode {
|
||||
+ mode: AnimationMode,
|
||||
+ },
|
||||
+ GetAnimationMode,
|
||||
ConnectorSetUseNativeGamut {
|
||||
connector: Connector,
|
||||
use_native_gamut: bool,
|
||||
@@ -1088,6 +1092,9 @@ pub enum Response {
|
||||
GetBarPosition {
|
||||
position: BarPosition,
|
||||
},
|
||||
+ GetAnimationMode {
|
||||
+ mode: AnimationMode,
|
||||
+ },
|
||||
KeymapFromNames {
|
||||
keymap: Keymap,
|
||||
},
|
||||
diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs
|
||||
index 53c53f8f..90945b47 100644
|
||||
--- a/jay-config/src/theme.rs
|
||||
+++ b/jay-config/src/theme.rs
|
||||
@@ -197,6 +197,30 @@ pub fn get_bar_position() -> BarPosition {
|
||||
get!(BarPosition::Top).get_bar_position()
|
||||
}
|
||||
|
||||
+/// The animation mode for window position/size transitions.
|
||||
+#[non_exhaustive]
|
||||
+#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
+pub enum AnimationMode {
|
||||
+ /// Position and size animate together.
|
||||
+ #[default]
|
||||
+ Concurrent,
|
||||
+ /// Position and size animate sequentially, with the phase order chosen
|
||||
+ /// automatically per window based on whether it is growing or shrinking.
|
||||
+ Sequential,
|
||||
+}
|
||||
+
|
||||
+/// Sets the animation mode for window transitions.
|
||||
+///
|
||||
+/// Default: `Concurrent`.
|
||||
+pub fn set_animation_mode(mode: AnimationMode) {
|
||||
+ get!().set_animation_mode(mode);
|
||||
+}
|
||||
+
|
||||
+/// Gets the current animation mode.
|
||||
+pub fn get_animation_mode() -> AnimationMode {
|
||||
+ get!(AnimationMode::Concurrent).get_animation_mode()
|
||||
+}
|
||||
+
|
||||
/// Elements of the compositor whose color can be changed.
|
||||
pub mod colors {
|
||||
use {
|
||||
diff --git a/src/animation.rs b/src/animation.rs
|
||||
index 328540b6..02d034a7 100644
|
||||
--- a/src/animation.rs
|
||||
+++ b/src/animation.rs
|
||||
@@ -1,3 +1,11 @@
|
||||
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
+pub enum AnimationMode {
|
||||
+ #[default]
|
||||
+ Concurrent,
|
||||
+ MoveFirst,
|
||||
+ ResizeFirst,
|
||||
+}
|
||||
+
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct PositionAnimation {
|
||||
pub offset_x: f64,
|
||||
@@ -6,6 +14,8 @@ pub struct PositionAnimation {
|
||||
pub offset_h: f64,
|
||||
pub start_usec: u64,
|
||||
pub duration_usec: u64,
|
||||
+ pub mode_x: AnimationMode,
|
||||
+ pub mode_y: AnimationMode,
|
||||
}
|
||||
|
||||
fn ease_out_cubic(t: f64) -> f64 {
|
||||
@@ -13,6 +23,33 @@ fn ease_out_cubic(t: f64) -> f64 {
|
||||
1.0 - t * t * t
|
||||
}
|
||||
|
||||
+fn sequential_factors(t: f64, mode: AnimationMode) -> (f64, f64) {
|
||||
+ match mode {
|
||||
+ AnimationMode::Concurrent => {
|
||||
+ let r = 1.0 - ease_out_cubic(t);
|
||||
+ (r, r)
|
||||
+ }
|
||||
+ AnimationMode::MoveFirst => {
|
||||
+ if t < 0.5 {
|
||||
+ let r = 1.0 - ease_out_cubic(t * 2.0);
|
||||
+ (r, 1.0)
|
||||
+ } else {
|
||||
+ let r = 1.0 - ease_out_cubic((t - 0.5) * 2.0);
|
||||
+ (0.0, r)
|
||||
+ }
|
||||
+ }
|
||||
+ AnimationMode::ResizeFirst => {
|
||||
+ if t < 0.5 {
|
||||
+ let r = 1.0 - ease_out_cubic(t * 2.0);
|
||||
+ (1.0, r)
|
||||
+ } else {
|
||||
+ let r = 1.0 - ease_out_cubic((t - 0.5) * 2.0);
|
||||
+ (r, 0.0)
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
impl PositionAnimation {
|
||||
/// Returns the current animation offset, or None if the animation is complete.
|
||||
pub fn current_offset(&self, now_usec: u64) -> Option<(i32, i32, i32, i32)> {
|
||||
@@ -24,11 +61,12 @@ impl PositionAnimation {
|
||||
return None;
|
||||
}
|
||||
let t = elapsed as f64 / self.duration_usec as f64;
|
||||
- let remaining = 1.0 - ease_out_cubic(t);
|
||||
- let dx = (self.offset_x * remaining).round() as i32;
|
||||
- let dy = (self.offset_y * remaining).round() as i32;
|
||||
- let dw = (self.offset_w * remaining).round() as i32;
|
||||
- let dh = (self.offset_h * remaining).round() as i32;
|
||||
+ let (move_remaining_x, size_remaining_x) = sequential_factors(t, self.mode_x);
|
||||
+ let (move_remaining_y, size_remaining_y) = sequential_factors(t, self.mode_y);
|
||||
+ let dx = (self.offset_x * move_remaining_x).round() as i32;
|
||||
+ let dy = (self.offset_y * move_remaining_y).round() as i32;
|
||||
+ let dw = (self.offset_w * size_remaining_x).round() as i32;
|
||||
+ let dh = (self.offset_h * size_remaining_y).round() as i32;
|
||||
if dx == 0 && dy == 0 && dw == 0 && dh == 0 {
|
||||
return None;
|
||||
}
|
||||
diff --git a/src/config/handler.rs b/src/config/handler.rs
|
||||
index 56135590..78f6281c 100644
|
||||
--- a/src/config/handler.rs
|
||||
+++ b/src/config/handler.rs
|
||||
@@ -67,7 +67,7 @@ use {
|
||||
},
|
||||
keyboard::{Group, Keymap, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
- theme::{BarPosition, colors::Colorable, sized::Resizable},
|
||||
+ theme::{AnimationMode, BarPosition, colors::Colorable, sized::Resizable},
|
||||
timer::Timer as JayTimer,
|
||||
video::{
|
||||
BlendSpace as ConfigBlendSpace, ColorSpace, Connector, DrmDevice, Eotf as ConfigEotf,
|
||||
@@ -1456,6 +1456,20 @@ impl ConfigProxyHandler {
|
||||
});
|
||||
}
|
||||
|
||||
+ fn handle_set_animation_mode(&self, mode: AnimationMode) -> Result<(), CphError> {
|
||||
+ let Ok(mode) = mode.try_into() else {
|
||||
+ return Err(CphError::UnknownAnimationMode(mode));
|
||||
+ };
|
||||
+ self.state.theme.animation_mode.set(mode);
|
||||
+ Ok(())
|
||||
+ }
|
||||
+
|
||||
+ fn handle_get_animation_mode(&self) {
|
||||
+ self.respond(Response::GetAnimationMode {
|
||||
+ mode: self.state.theme.animation_mode.get().into(),
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
fn handle_set_show_float_pin_icon(&self, show: bool) {
|
||||
self.state.show_pin_icon.set(show);
|
||||
for stacked in self.state.root.stacked.iter() {
|
||||
@@ -3320,6 +3334,10 @@ impl ConfigProxyHandler {
|
||||
.handle_set_bar_position(position)
|
||||
.wrn("set_bar_position")?,
|
||||
ClientMessage::GetBarPosition => self.handle_get_bar_position(),
|
||||
+ ClientMessage::SetAnimationMode { mode } => self
|
||||
+ .handle_set_animation_mode(mode)
|
||||
+ .wrn("set_animation_mode")?,
|
||||
+ ClientMessage::GetAnimationMode => self.handle_get_animation_mode(),
|
||||
ClientMessage::SeatFocusHistory { seat, timeline } => self
|
||||
.handle_seat_focus_history(seat, timeline)
|
||||
.wrn("seat_focus_history")?,
|
||||
@@ -3562,6 +3580,8 @@ enum CphError {
|
||||
UnknownBlendSpace(ConfigBlendSpace),
|
||||
#[error("Unknown bar position {0:?}")]
|
||||
UnknownBarPosition(BarPosition),
|
||||
+ #[error("Unknown animation mode {0:?}")]
|
||||
+ UnknownAnimationMode(AnimationMode),
|
||||
#[error("Unknown gfx API {0:?}")]
|
||||
UnknownGfxApi(GfxApi),
|
||||
#[error("Unknown fallback output mode {0:?}")]
|
||||
diff --git a/src/theme.rs b/src/theme.rs
|
||||
index 52c6392d..0709f3d4 100644
|
||||
--- a/src/theme.rs
|
||||
+++ b/src/theme.rs
|
||||
@@ -6,7 +6,9 @@ use {
|
||||
gfx_api::AlphaMode,
|
||||
utils::clonecell::CloneCell,
|
||||
},
|
||||
- jay_config::theme::BarPosition as ConfigBarPosition,
|
||||
+ jay_config::theme::{
|
||||
+ AnimationMode as ConfigAnimationMode, BarPosition as ConfigBarPosition,
|
||||
+ },
|
||||
linearize::Linearize,
|
||||
num_traits::Float,
|
||||
std::{cell::Cell, cmp::Ordering, ops::Mul, sync::Arc},
|
||||
@@ -538,6 +540,28 @@ impl TryFrom<ConfigBarPosition> for BarPosition {
|
||||
}
|
||||
}
|
||||
|
||||
+impl TryFrom<ConfigAnimationMode> for AnimationMode {
|
||||
+ type Error = ();
|
||||
+
|
||||
+ fn try_from(value: ConfigAnimationMode) -> Result<Self, Self::Error> {
|
||||
+ let v = match value {
|
||||
+ ConfigAnimationMode::Concurrent => Self::Concurrent,
|
||||
+ ConfigAnimationMode::Sequential => Self::Sequential,
|
||||
+ _ => return Err(()),
|
||||
+ };
|
||||
+ Ok(v)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+impl Into<ConfigAnimationMode> for AnimationMode {
|
||||
+ fn into(self) -> ConfigAnimationMode {
|
||||
+ match self {
|
||||
+ AnimationMode::Concurrent => ConfigAnimationMode::Concurrent,
|
||||
+ AnimationMode::Sequential => ConfigAnimationMode::Sequential,
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
impl Into<ConfigBarPosition> for BarPosition {
|
||||
fn into(self) -> ConfigBarPosition {
|
||||
match self {
|
||||
@@ -547,6 +571,13 @@ impl Into<ConfigBarPosition> for BarPosition {
|
||||
}
|
||||
}
|
||||
|
||||
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
+pub enum AnimationMode {
|
||||
+ #[default]
|
||||
+ Concurrent,
|
||||
+ Sequential,
|
||||
+}
|
||||
+
|
||||
pub struct Theme {
|
||||
pub colors: ThemeColors,
|
||||
pub sizes: ThemeSizes,
|
||||
@@ -556,6 +587,7 @@ pub struct Theme {
|
||||
pub default_font: Arc<String>,
|
||||
pub show_titles: Cell<bool>,
|
||||
pub bar_position: Cell<BarPosition>,
|
||||
+ pub animation_mode: Cell<AnimationMode>,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
@@ -570,6 +602,7 @@ impl Default for Theme {
|
||||
default_font,
|
||||
show_titles: Cell::new(true),
|
||||
bar_position: Default::default(),
|
||||
+ animation_mode: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
diff --git a/src/tree/container.rs b/src/tree/container.rs
|
||||
index 25f95415..85a0b97d 100644
|
||||
--- a/src/tree/container.rs
|
||||
+++ b/src/tree/container.rs
|
||||
@@ -377,7 +377,7 @@ impl ContainerNode {
|
||||
/// container-local coordinates. This allows `perform_layout` to compute
|
||||
/// a meaningful animation delta for children that just moved between
|
||||
/// containers.
|
||||
- fn seed_child_body(&self, child_id: NodeId, abs_body: Rect) {
|
||||
+ pub(super) fn seed_child_body(&self, child_id: NodeId, abs_body: Rect) {
|
||||
if let Some(link) = self.child_nodes.borrow().get(&child_id) {
|
||||
let local = abs_body.move_(-self.abs_x1.get(), -self.abs_y1.get());
|
||||
link.to_ref().body.set(local);
|
||||
@@ -458,6 +458,42 @@ impl ContainerNode {
|
||||
}
|
||||
if animation_duration > 0 && is_split && !old_bodies.is_empty() {
|
||||
let now_usec = Time::now_unchecked().usec();
|
||||
+ let has_spawn = self.children.iter().any(|c| {
|
||||
+ !old_bodies
|
||||
+ .iter()
|
||||
+ .any(|(id, rect)| *id == c.node.node_id() && !rect.is_empty())
|
||||
+ });
|
||||
+ // Determine per-child animation modes based on split-axis analysis.
|
||||
+ let use_sequential = !has_spawn
|
||||
+ && matches!(
|
||||
+ self.state.theme.animation_mode.get(),
|
||||
+ crate::theme::AnimationMode::Sequential
|
||||
+ );
|
||||
+ // Check if any child has a two-phase animation (both position and
|
||||
+ // size change along the split axis). Single-phase children will
|
||||
+ // delay their start to synchronise with the second phase.
|
||||
+ let has_two_phase = use_sequential
|
||||
+ && self.children.iter().any(|c| {
|
||||
+ let child_id = c.node.node_id();
|
||||
+ if let Some((_, old)) = old_bodies.iter().find(|(id, _)| *id == child_id) {
|
||||
+ if old.is_empty() {
|
||||
+ return false;
|
||||
+ }
|
||||
+ let new_body = c.body.get();
|
||||
+ let split = self.split.get();
|
||||
+ let (dp, ds) = match split {
|
||||
+ ContainerSplit::Horizontal => {
|
||||
+ (old.x1() - new_body.x1(), old.width() - new_body.width())
|
||||
+ }
|
||||
+ ContainerSplit::Vertical => {
|
||||
+ (old.y1() - new_body.y1(), old.height() - new_body.height())
|
||||
+ }
|
||||
+ };
|
||||
+ dp != 0 && ds != 0
|
||||
+ } else {
|
||||
+ false
|
||||
+ }
|
||||
+ });
|
||||
for child in self.children.iter() {
|
||||
let child_id = child.node.node_id();
|
||||
let old = match old_bodies.iter().find(|(id, _)| *id == child_id) {
|
||||
@@ -485,13 +521,53 @@ impl ContainerNode {
|
||||
} else {
|
||||
(dx as f64, dy as f64, dw as f64, dh as f64)
|
||||
};
|
||||
+ // Determine per-child, per-axis animation mode.
|
||||
+ let (mode_x, mode_y) = if use_sequential {
|
||||
+ use crate::animation::AnimationMode::*;
|
||||
+ let split = self.split.get();
|
||||
+ let (delta_pos, delta_size) = match split {
|
||||
+ ContainerSplit::Horizontal => (dx, dw),
|
||||
+ ContainerSplit::Vertical => (dy, dh),
|
||||
+ };
|
||||
+ let child_mode = if delta_pos == 0 || delta_size == 0 {
|
||||
+ Concurrent
|
||||
+ } else if delta_size < 0 {
|
||||
+ // Shrinking: resize first to free space, then move.
|
||||
+ ResizeFirst
|
||||
+ } else {
|
||||
+ // Growing: move into freed space first, then grow.
|
||||
+ MoveFirst
|
||||
+ };
|
||||
+ match split {
|
||||
+ ContainerSplit::Horizontal => (child_mode, Concurrent),
|
||||
+ ContainerSplit::Vertical => (Concurrent, child_mode),
|
||||
+ }
|
||||
+ } else {
|
||||
+ use crate::animation::AnimationMode::Concurrent;
|
||||
+ (Concurrent, Concurrent)
|
||||
+ };
|
||||
+ // In sequential mode, single-phase (Concurrent) children wait
|
||||
+ // for double-phase children to complete their first phase.
|
||||
+ let duration_usec = (animation_duration as u64) * 1000;
|
||||
+ let (child_start, child_duration) = if use_sequential
|
||||
+ && has_two_phase
|
||||
+ && mode_x == crate::animation::AnimationMode::Concurrent
|
||||
+ && mode_y == crate::animation::AnimationMode::Concurrent
|
||||
+ {
|
||||
+ (now_usec + duration_usec / 2, duration_usec / 2)
|
||||
+ } else {
|
||||
+ (now_usec, duration_usec)
|
||||
+ };
|
||||
child.body_animation.set(Some(PositionAnimation {
|
||||
offset_x: start_dx,
|
||||
offset_y: start_dy,
|
||||
offset_w: start_dw,
|
||||
offset_h: start_dh,
|
||||
- start_usec: now_usec,
|
||||
- duration_usec: (animation_duration as u64) * 1000,
|
||||
+ start_usec: child_start,
|
||||
+ duration_usec: child_duration,
|
||||
+ mode_x,
|
||||
+ mode_y,
|
||||
+ reverse: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
|
||||
index 03f3a0ca..d21f7556 100644
|
||||
--- a/toml-config/src/config.rs
|
||||
+++ b/toml-config/src/config.rs
|
||||
@@ -32,7 +32,7 @@ use {
|
||||
keyboard::{Keymap, ModifiedKeySym, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
status::MessageFormat,
|
||||
- theme::{BarPosition, Color},
|
||||
+ theme::{AnimationMode, BarPosition, Color},
|
||||
video::{BlendSpace, ColorSpace, Eotf, Format, GfxApi, TearingMode, Transform, VrrMode},
|
||||
window::{ContentType, TileState, WindowType},
|
||||
workspace::WorkspaceDisplayOrder,
|
||||
@@ -211,6 +211,7 @@ pub struct Theme {
|
||||
pub bar_separator_width: Option<i32>,
|
||||
pub gap: Option<i32>,
|
||||
pub animation_duration: Option<i32>,
|
||||
+ pub animation_mode: Option<AnimationMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs
|
||||
index 977dbc14..ae9b7ccd 100644
|
||||
--- a/toml-config/src/config/parsers/theme.rs
|
||||
+++ b/toml-config/src/config/parsers/theme.rs
|
||||
@@ -13,7 +13,7 @@ use {
|
||||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
- jay_config::theme::BarPosition,
|
||||
+ jay_config::theme::{AnimationMode, BarPosition},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Parser for ThemeParser<'_> {
|
||||
font,
|
||||
title_font,
|
||||
),
|
||||
- (bar_font, bar_position_val, bar_separator_width, gap, animation_duration),
|
||||
+ (bar_font, bar_position_val, bar_separator_width, gap, animation_duration, animation_mode_val),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("attention-requested-bg-color")),
|
||||
@@ -95,6 +95,7 @@ impl Parser for ThemeParser<'_> {
|
||||
recover(opt(s32("bar-separator-width"))),
|
||||
recover(opt(s32("gap"))),
|
||||
recover(opt(s32("animation-duration"))),
|
||||
+ recover(opt(str("animation-mode"))),
|
||||
),
|
||||
))?;
|
||||
macro_rules! color {
|
||||
@@ -124,6 +125,19 @@ impl Parser for ThemeParser<'_> {
|
||||
None
|
||||
}
|
||||
});
|
||||
+ let animation_mode =
|
||||
+ animation_mode_val.and_then(|value| match value.value.to_lowercase().as_str() {
|
||||
+ "concurrent" => Some(AnimationMode::Concurrent),
|
||||
+ "sequential" => Some(AnimationMode::Sequential),
|
||||
+ _ => {
|
||||
+ log::warn!(
|
||||
+ "Unknown animation mode '{}': {}",
|
||||
+ value.value,
|
||||
+ self.0.error3(value.span)
|
||||
+ );
|
||||
+ None
|
||||
+ }
|
||||
+ });
|
||||
Ok(Theme {
|
||||
attention_requested_bg_color: color!(attention_requested_bg_color),
|
||||
bg_color: color!(bg_color),
|
||||
@@ -150,6 +164,7 @@ impl Parser for ThemeParser<'_> {
|
||||
bar_separator_width: bar_separator_width.despan(),
|
||||
gap: gap.despan(),
|
||||
animation_duration: animation_duration.despan(),
|
||||
+ animation_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
|
||||
index e60d6837..4f9c6a4d 100644
|
||||
--- a/toml-config/src/lib.rs
|
||||
+++ b/toml-config/src/lib.rs
|
||||
@@ -44,8 +44,8 @@ use {
|
||||
switch_to_vt,
|
||||
tasks::{self, JoinHandle},
|
||||
theme::{
|
||||
- reset_colors, reset_font, reset_sizes, set_bar_font, set_bar_position, set_font,
|
||||
- set_title_font,
|
||||
+ reset_colors, reset_font, reset_sizes, set_animation_mode, set_bar_font,
|
||||
+ set_bar_position, set_font, set_title_font,
|
||||
},
|
||||
toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles,
|
||||
video::{
|
||||
@@ -1613,6 +1613,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||
if let Some(v) = config.theme.bar_position {
|
||||
set_bar_position(v);
|
||||
}
|
||||
+ if let Some(v) = config.theme.animation_mode {
|
||||
+ set_animation_mode(v);
|
||||
+ }
|
||||
if let Some(v) = config.focus_history {
|
||||
if let Some(v) = v.only_visible {
|
||||
persistent.seat.focus_history_set_only_visible(v);
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
From 986b7638b94c45b5cd4526a4f2cc66f8091508af Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 13:47:02 +1100
|
||||
Subject: [PATCH 05/10] add cursor-follows-focus setting
|
||||
|
||||
When enabled, the cursor warps to the center of the focused window on
|
||||
keyboard-driven focus changes (move_focus). This is a per-seat boolean
|
||||
setting, configurable via the Seat API and cursor-follows-focus in TOML.
|
||||
Defaults to false.
|
||||
---
|
||||
jay-config/src/_private/client.rs | 10 +++++++++
|
||||
jay-config/src/_private/ipc.rs | 10 +++++++++
|
||||
jay-config/src/input.rs | 13 ++++++++++++
|
||||
src/config/handler.rs | 24 ++++++++++++++++++++++
|
||||
src/ifs/wl_seat.rs | 26 ++++++++++++++++++++++++
|
||||
src/tree/output.rs | 7 +++++--
|
||||
toml-config/src/config.rs | 1 +
|
||||
toml-config/src/config/parsers/config.rs | 3 +++
|
||||
toml-config/src/lib.rs | 3 +++
|
||||
9 files changed, 95 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs
|
||||
index c72c17f1..9a83a87e 100644
|
||||
--- a/jay-config/src/_private/client.rs
|
||||
+++ b/jay-config/src/_private/client.rs
|
||||
@@ -1387,6 +1387,16 @@ impl ConfigClient {
|
||||
self.send(&ClientMessage::SetFocusFollowsMouseMode { seat, mode })
|
||||
}
|
||||
|
||||
+ pub fn set_cursor_follows_focus(&self, seat: Seat, enabled: bool) {
|
||||
+ self.send(&ClientMessage::SetCursorFollowsFocus { seat, enabled });
|
||||
+ }
|
||||
+
|
||||
+ pub fn get_cursor_follows_focus(&self, seat: Seat) -> bool {
|
||||
+ let res = self.send_with_response(&ClientMessage::GetCursorFollowsFocus { seat });
|
||||
+ get_response!(res, false, GetCursorFollowsFocus { enabled });
|
||||
+ enabled
|
||||
+ }
|
||||
+
|
||||
pub fn set_fallback_output_mode(&self, seat: Seat, mode: FallbackOutputMode) {
|
||||
self.send(&ClientMessage::SetFallbackOutputMode { seat, mode })
|
||||
}
|
||||
diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs
|
||||
index b0273f91..74e27f76 100644
|
||||
--- a/jay-config/src/_private/ipc.rs
|
||||
+++ b/jay-config/src/_private/ipc.rs
|
||||
@@ -501,6 +501,13 @@ pub enum ClientMessage<'a> {
|
||||
seat: Seat,
|
||||
mode: FocusFollowsMouseMode,
|
||||
},
|
||||
+ SetCursorFollowsFocus {
|
||||
+ seat: Seat,
|
||||
+ enabled: bool,
|
||||
+ },
|
||||
+ GetCursorFollowsFocus {
|
||||
+ seat: Seat,
|
||||
+ },
|
||||
SetInputDeviceConnector {
|
||||
input_device: InputDevice,
|
||||
connector: Connector,
|
||||
@@ -1095,6 +1102,9 @@ pub enum Response {
|
||||
GetAnimationMode {
|
||||
mode: AnimationMode,
|
||||
},
|
||||
+ GetCursorFollowsFocus {
|
||||
+ enabled: bool,
|
||||
+ },
|
||||
KeymapFromNames {
|
||||
keymap: Keymap,
|
||||
},
|
||||
diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs
|
||||
index 2e985766..23162537 100644
|
||||
--- a/jay-config/src/input.rs
|
||||
+++ b/jay-config/src/input.rs
|
||||
@@ -503,6 +503,19 @@ impl Seat {
|
||||
get!().set_focus_follows_mouse_mode(self, mode);
|
||||
}
|
||||
|
||||
+ /// Sets whether the cursor warps to the center of a window when focus
|
||||
+ /// changes via keyboard.
|
||||
+ ///
|
||||
+ /// Default: `false`.
|
||||
+ pub fn set_cursor_follows_focus(self, enabled: bool) {
|
||||
+ get!().set_cursor_follows_focus(self, enabled);
|
||||
+ }
|
||||
+
|
||||
+ /// Gets the cursor-follows-focus setting.
|
||||
+ pub fn get_cursor_follows_focus(self) -> bool {
|
||||
+ get!(false).get_cursor_follows_focus(self)
|
||||
+ }
|
||||
+
|
||||
/// Sets the fallback output mode.
|
||||
///
|
||||
/// The default is `Cursor`.
|
||||
diff --git a/src/config/handler.rs b/src/config/handler.rs
|
||||
index 78f6281c..9ab0934a 100644
|
||||
--- a/src/config/handler.rs
|
||||
+++ b/src/config/handler.rs
|
||||
@@ -520,6 +520,24 @@ impl ConfigProxyHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+ fn handle_set_cursor_follows_focus(
|
||||
+ &self,
|
||||
+ seat: Seat,
|
||||
+ enabled: bool,
|
||||
+ ) -> Result<(), CphError> {
|
||||
+ let seat = self.get_seat(seat)?;
|
||||
+ seat.set_cursor_follows_focus(enabled);
|
||||
+ Ok(())
|
||||
+ }
|
||||
+
|
||||
+ fn handle_get_cursor_follows_focus(&self, seat: Seat) -> Result<(), CphError> {
|
||||
+ let seat = self.get_seat(seat)?;
|
||||
+ self.respond(Response::GetCursorFollowsFocus {
|
||||
+ enabled: seat.get_cursor_follows_focus(),
|
||||
+ });
|
||||
+ Ok(())
|
||||
+ }
|
||||
+
|
||||
fn handle_set_fallback_output_mode(
|
||||
&self,
|
||||
seat: Seat,
|
||||
@@ -3115,6 +3133,12 @@ impl ConfigProxyHandler {
|
||||
ClientMessage::SetFocusFollowsMouseMode { seat, mode } => self
|
||||
.handle_set_focus_follows_mouse_mode(seat, mode)
|
||||
.wrn("set_focus_follows_mouse_mode")?,
|
||||
+ ClientMessage::SetCursorFollowsFocus { seat, enabled } => self
|
||||
+ .handle_set_cursor_follows_focus(seat, enabled)
|
||||
+ .wrn("set_cursor_follows_focus")?,
|
||||
+ ClientMessage::GetCursorFollowsFocus { seat } => self
|
||||
+ .handle_get_cursor_follows_focus(seat)
|
||||
+ .wrn("get_cursor_follows_focus")?,
|
||||
ClientMessage::SetInputDeviceConnector {
|
||||
input_device,
|
||||
connector,
|
||||
diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs
|
||||
index e3f930e0..68fe5366 100644
|
||||
--- a/src/ifs/wl_seat.rs
|
||||
+++ b/src/ifs/wl_seat.rs
|
||||
@@ -230,6 +230,7 @@ pub struct WlSeatGlobal {
|
||||
input_method_grab: CloneCell<Option<Rc<dyn InputMethodKeyboardGrab>>>,
|
||||
forward: Cell<bool>,
|
||||
focus_follows_mouse: Cell<bool>,
|
||||
+ cursor_follows_focus: Cell<bool>,
|
||||
fallback_output_mode: Cell<FallbackOutputMode>,
|
||||
swipe_bindings: PerClientBindings<ZwpPointerGestureSwipeV1>,
|
||||
pinch_bindings: PerClientBindings<ZwpPointerGesturePinchV1>,
|
||||
@@ -358,6 +359,7 @@ impl WlSeatGlobal {
|
||||
input_method_grab: Default::default(),
|
||||
forward: Cell::new(false),
|
||||
focus_follows_mouse: Cell::new(true),
|
||||
+ cursor_follows_focus: Cell::new(false),
|
||||
fallback_output_mode: Cell::new(FallbackOutputMode::Cursor),
|
||||
swipe_bindings: Default::default(),
|
||||
pinch_bindings: Default::default(),
|
||||
@@ -776,6 +778,7 @@ impl WlSeatGlobal {
|
||||
.find_output_in_direction(&ws.output.get(), direction)
|
||||
{
|
||||
target.take_keyboard_navigation_focus(self, direction);
|
||||
+ self.warp_cursor_to_focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -795,6 +798,7 @@ impl WlSeatGlobal {
|
||||
c.move_focus_from_child(self, tl.deref(), direction);
|
||||
}
|
||||
}
|
||||
+ self.warp_cursor_to_focus();
|
||||
}
|
||||
|
||||
pub fn move_focused(self: &Rc<Self>, direction: Direction) {
|
||||
@@ -1460,6 +1464,28 @@ impl WlSeatGlobal {
|
||||
self.focus_follows_mouse.set(focus_follows_mouse);
|
||||
}
|
||||
|
||||
+ pub fn set_cursor_follows_focus(&self, enabled: bool) {
|
||||
+ self.cursor_follows_focus.set(enabled);
|
||||
+ }
|
||||
+
|
||||
+ pub fn get_cursor_follows_focus(&self) -> bool {
|
||||
+ self.cursor_follows_focus.get()
|
||||
+ }
|
||||
+
|
||||
+ pub fn warp_cursor_to_focus(&self) {
|
||||
+ if !self.cursor_follows_focus.get() {
|
||||
+ return;
|
||||
+ }
|
||||
+ if let Some(tl) = self.keyboard_node.get().node_toplevel() {
|
||||
+ let rect = tl.node_absolute_position();
|
||||
+ if !rect.is_empty() {
|
||||
+ let cx = Fixed::from_int(rect.x1() + rect.width() / 2);
|
||||
+ let cy = Fixed::from_int(rect.y1() + rect.height() / 2);
|
||||
+ self.pointer_cursor.set_position(cx, cy);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
pub fn set_fallback_output_mode(&self, fallback_output_mode: FallbackOutputMode) {
|
||||
self.fallback_output_mode.set(fallback_output_mode);
|
||||
}
|
||||
diff --git a/src/tree/output.rs b/src/tree/output.rs
|
||||
index c3e2ae3a..b43b387e 100644
|
||||
--- a/src/tree/output.rs
|
||||
+++ b/src/tree/output.rs
|
||||
@@ -711,8 +711,11 @@ impl OutputNode {
|
||||
fs.tl_change_extents(&self.global.pos.get());
|
||||
}
|
||||
ws.change_extents(&self.workspace_rect.get());
|
||||
- for seat in seats {
|
||||
- ws.clone().node_do_focus(&seat, Direction::Unspecified);
|
||||
+ for seat in &seats {
|
||||
+ ws.clone().node_do_focus(seat, Direction::Unspecified);
|
||||
+ }
|
||||
+ for seat in &seats {
|
||||
+ seat.warp_cursor_to_focus();
|
||||
}
|
||||
if self.node_visible() {
|
||||
self.state.damage(self.global.pos.get());
|
||||
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
|
||||
index d21f7556..86120f08 100644
|
||||
--- a/toml-config/src/config.rs
|
||||
+++ b/toml-config/src/config.rs
|
||||
@@ -522,6 +522,7 @@ pub struct Config {
|
||||
pub grace_period: Option<Duration>,
|
||||
pub explicit_sync_enabled: Option<bool>,
|
||||
pub focus_follows_mouse: bool,
|
||||
+ pub cursor_follows_focus: bool,
|
||||
pub window_management_key: Option<ModifiedKeySym>,
|
||||
pub vrr: Option<Vrr>,
|
||||
pub tearing: Option<Tearing>,
|
||||
diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs
|
||||
index 10e12fca..9c0e1140 100644
|
||||
--- a/toml-config/src/config/parsers/config.rs
|
||||
+++ b/toml-config/src/config/parsers/config.rs
|
||||
@@ -150,6 +150,7 @@ impl Parser for ConfigParser<'_> {
|
||||
simple_im_val,
|
||||
show_titles,
|
||||
fallback_output_mode_val,
|
||||
+ cursor_follows_focus,
|
||||
),
|
||||
) = ext.extract((
|
||||
(
|
||||
@@ -208,6 +209,7 @@ impl Parser for ConfigParser<'_> {
|
||||
opt(val("simple-im")),
|
||||
recover(opt(bol("show-titles"))),
|
||||
opt(val("fallback-output-mode")),
|
||||
+ recover(opt(bol("cursor-follows-focus"))),
|
||||
),
|
||||
))?;
|
||||
let mut keymap = None;
|
||||
@@ -565,6 +567,7 @@ impl Parser for ConfigParser<'_> {
|
||||
idle,
|
||||
grace_period,
|
||||
focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true),
|
||||
+ cursor_follows_focus: cursor_follows_focus.despan().unwrap_or(false),
|
||||
window_management_key,
|
||||
vrr,
|
||||
tearing,
|
||||
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
|
||||
index 4f9c6a4d..b2fd985f 100644
|
||||
--- a/toml-config/src/lib.rs
|
||||
+++ b/toml-config/src/lib.rs
|
||||
@@ -1555,6 +1555,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||
true => FocusFollowsMouseMode::True,
|
||||
false => FocusFollowsMouseMode::False,
|
||||
});
|
||||
+ persistent
|
||||
+ .seat
|
||||
+ .set_cursor_follows_focus(config.cursor_follows_focus);
|
||||
if let Some(window_management_key) = config.window_management_key {
|
||||
persistent
|
||||
.seat
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
From 05e9f39292784d9aa6295517c1aa3ca20fad0e46 Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 14:55:31 +1100
|
||||
Subject: [PATCH 06/10] add toggle focus between floating and tiled layers
|
||||
|
||||
Adds focus_floats(), toggle_focus_float_tiled(), and their IPC
|
||||
bindings so users can bind a key to swap focus between the floating
|
||||
and tiled layers.
|
||||
---
|
||||
jay-config/src/_private/client.rs | 8 +++++++
|
||||
jay-config/src/_private/ipc.rs | 6 +++++
|
||||
jay-config/src/input.rs | 13 ++++++++++
|
||||
src/config/handler.rs | 19 +++++++++++++++
|
||||
src/ifs/wl_seat.rs | 30 ++++++++++++++++++++++++
|
||||
toml-config/src/config.rs | 1 +
|
||||
toml-config/src/config/parsers/action.rs | 1 +
|
||||
toml-config/src/lib.rs | 4 ++++
|
||||
8 files changed, 82 insertions(+)
|
||||
|
||||
diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs
|
||||
index 9a83a87e..9581b771 100644
|
||||
--- a/jay-config/src/_private/client.rs
|
||||
+++ b/jay-config/src/_private/client.rs
|
||||
@@ -399,6 +399,14 @@ impl ConfigClient {
|
||||
self.send(&ClientMessage::SeatFocusTiles { seat });
|
||||
}
|
||||
|
||||
+ pub fn seat_focus_floats(&self, seat: Seat) {
|
||||
+ self.send(&ClientMessage::SeatFocusFloats { seat });
|
||||
+ }
|
||||
+
|
||||
+ pub fn seat_toggle_focus_float_tiled(&self, seat: Seat) {
|
||||
+ self.send(&ClientMessage::SeatToggleFocusFloatTiled { seat });
|
||||
+ }
|
||||
+
|
||||
pub fn seat_focus(&self, seat: Seat, direction: Direction) {
|
||||
self.send(&ClientMessage::SeatFocus { seat, direction });
|
||||
}
|
||||
diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs
|
||||
index 74e27f76..a4ebc27c 100644
|
||||
--- a/jay-config/src/_private/ipc.rs
|
||||
+++ b/jay-config/src/_private/ipc.rs
|
||||
@@ -754,6 +754,12 @@ pub enum ClientMessage<'a> {
|
||||
SeatFocusTiles {
|
||||
seat: Seat,
|
||||
},
|
||||
+ SeatFocusFloats {
|
||||
+ seat: Seat,
|
||||
+ },
|
||||
+ SeatToggleFocusFloatTiled {
|
||||
+ seat: Seat,
|
||||
+ },
|
||||
SetMiddleClickPasteEnabled {
|
||||
enabled: bool,
|
||||
},
|
||||
diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs
|
||||
index 23162537..8f3d4d27 100644
|
||||
--- a/jay-config/src/input.rs
|
||||
+++ b/jay-config/src/input.rs
|
||||
@@ -321,6 +321,19 @@ impl Seat {
|
||||
get!().seat_focus_tiles(self)
|
||||
}
|
||||
|
||||
+ /// Moves the keyboard focus to the topmost floating window.
|
||||
+ pub fn focus_floats(self) {
|
||||
+ get!().seat_focus_floats(self)
|
||||
+ }
|
||||
+
|
||||
+ /// Toggles keyboard focus between the floating and tiled layers.
|
||||
+ ///
|
||||
+ /// If focus is on the tiled or fullscreen layer, focus moves to the topmost float.
|
||||
+ /// If focus is on the floating layer, focus moves to the tiled layer.
|
||||
+ pub fn toggle_focus_float_tiled(self) {
|
||||
+ get!().seat_toggle_focus_float_tiled(self)
|
||||
+ }
|
||||
+
|
||||
/// Moves the keyboard focus of the seat in the specified direction.
|
||||
pub fn focus(self, direction: Direction) {
|
||||
get!().seat_focus(self, direction)
|
||||
diff --git a/src/config/handler.rs b/src/config/handler.rs
|
||||
index 9ab0934a..88d801b7 100644
|
||||
--- a/src/config/handler.rs
|
||||
+++ b/src/config/handler.rs
|
||||
@@ -2397,6 +2397,18 @@ impl ConfigProxyHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+ fn handle_seat_focus_floats(&self, seat: Seat) -> Result<(), CphError> {
|
||||
+ let seat = self.get_seat(seat)?;
|
||||
+ seat.focus_floats();
|
||||
+ Ok(())
|
||||
+ }
|
||||
+
|
||||
+ fn handle_seat_toggle_focus_float_tiled(&self, seat: Seat) -> Result<(), CphError> {
|
||||
+ let seat = self.get_seat(seat)?;
|
||||
+ seat.toggle_focus_float_tiled();
|
||||
+ Ok(())
|
||||
+ }
|
||||
+
|
||||
fn handle_set_middle_click_paste_enabled(&self, enabled: bool) {
|
||||
self.state.set_primary_selection_enabled(enabled);
|
||||
}
|
||||
@@ -3380,6 +3392,13 @@ impl ConfigProxyHandler {
|
||||
ClientMessage::SeatFocusTiles { seat } => {
|
||||
self.handle_seat_focus_tiles(seat).wrn("seat_focus_tiles")?
|
||||
}
|
||||
+ ClientMessage::SeatFocusFloats { seat } => {
|
||||
+ self.handle_seat_focus_floats(seat).wrn("seat_focus_floats")?
|
||||
+ }
|
||||
+ ClientMessage::SeatToggleFocusFloatTiled { seat } => {
|
||||
+ self.handle_seat_toggle_focus_float_tiled(seat)
|
||||
+ .wrn("seat_toggle_focus_float_tiled")?
|
||||
+ }
|
||||
ClientMessage::SetMiddleClickPasteEnabled { enabled } => {
|
||||
self.handle_set_middle_click_paste_enabled(enabled)
|
||||
}
|
||||
diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs
|
||||
index 68fe5366..31084948 100644
|
||||
--- a/src/ifs/wl_seat.rs
|
||||
+++ b/src/ifs/wl_seat.rs
|
||||
@@ -1103,6 +1103,36 @@ impl WlSeatGlobal {
|
||||
);
|
||||
}
|
||||
|
||||
+ pub fn toggle_focus_float_tiled(self: &Rc<Self>) {
|
||||
+ let current = self.keyboard_node.get();
|
||||
+ match current.node_layer().layer() {
|
||||
+ NodeLayer::Tiled | NodeLayer::Fullscreen => self.focus_floats(),
|
||||
+ _ => self.focus_tiles(),
|
||||
+ }
|
||||
+ self.warp_cursor_to_focus();
|
||||
+ }
|
||||
+
|
||||
+ pub fn focus_floats(self: &Rc<Self>) {
|
||||
+ let current = self.keyboard_node.get();
|
||||
+ if current.node_layer().layer() == NodeLayer::Stacked {
|
||||
+ return;
|
||||
+ }
|
||||
+ let Some(output) = current.node_output() else {
|
||||
+ return;
|
||||
+ };
|
||||
+ let Some(ws) = output.workspace.get() else {
|
||||
+ return;
|
||||
+ };
|
||||
+ if let Some(child) = ws
|
||||
+ .stacked
|
||||
+ .rev_iter()
|
||||
+ .filter_map(|node| (*node).clone().node_into_float())
|
||||
+ .find_map(|float| float.child.get())
|
||||
+ {
|
||||
+ child.node_do_focus(self, Direction::Unspecified);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
pub fn focus_tiles(self: &Rc<Self>) {
|
||||
let current = self.keyboard_node.get();
|
||||
if matches!(
|
||||
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
|
||||
index 86120f08..cedcf78f 100644
|
||||
--- a/toml-config/src/config.rs
|
||||
+++ b/toml-config/src/config.rs
|
||||
@@ -83,6 +83,7 @@ pub enum SimpleCommand {
|
||||
FocusHistory(Timeline),
|
||||
FocusLayerRel(LayerDirection),
|
||||
FocusTiles,
|
||||
+ ToggleFocusFloatTiled,
|
||||
CreateMark,
|
||||
JumpToMark,
|
||||
PopMode(bool),
|
||||
diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs
|
||||
index 7ea8fec4..43b53774 100644
|
||||
--- a/toml-config/src/config/parsers/action.rs
|
||||
+++ b/toml-config/src/config/parsers/action.rs
|
||||
@@ -158,6 +158,7 @@ impl ActionParser<'_> {
|
||||
"focus-below" => FocusLayerRel(LayerDirection::Below),
|
||||
"focus-above" => FocusLayerRel(LayerDirection::Above),
|
||||
"focus-tiles" => FocusTiles,
|
||||
+ "toggle-float-focus" => ToggleFocusFloatTiled,
|
||||
"create-mark" => CreateMark,
|
||||
"jump-to-mark" => JumpToMark,
|
||||
"clear-modes" => PopMode(false),
|
||||
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
|
||||
index b2fd985f..34c69903 100644
|
||||
--- a/toml-config/src/lib.rs
|
||||
+++ b/toml-config/src/lib.rs
|
||||
@@ -217,6 +217,10 @@ impl Action {
|
||||
let persistent = state.persistent.clone();
|
||||
b.new(move || persistent.seat.focus_tiles())
|
||||
}
|
||||
+ SimpleCommand::ToggleFocusFloatTiled => {
|
||||
+ let persistent = state.persistent.clone();
|
||||
+ b.new(move || persistent.seat.toggle_focus_float_tiled())
|
||||
+ }
|
||||
SimpleCommand::CreateMark => {
|
||||
let persistent = state.persistent.clone();
|
||||
b.new(move || persistent.seat.create_mark(None))
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,975 @@
|
|||
From 424a39fb0a9f3d84a483fc99f7edf274536627d8 Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 18:40:54 +1100
|
||||
Subject: [PATCH 07/10] add open/close animations for tiled windows
|
||||
|
||||
---
|
||||
jay-config/src/_private/client.rs | 14 +-
|
||||
jay-config/src/_private/ipc.rs | 11 +-
|
||||
jay-config/src/theme.rs | 31 +++++
|
||||
src/animation.rs | 21 ++-
|
||||
src/config/handler.rs | 24 +++-
|
||||
src/ifs/wl_seat/pointer_owner.rs | 3 +-
|
||||
src/renderer.rs | 130 +++++++++++++++++--
|
||||
src/state.rs | 2 +-
|
||||
src/theme.rs | 44 +++++++
|
||||
src/tree/container.rs | 163 ++++++++++++++++++++++--
|
||||
src/tree/float.rs | 3 +
|
||||
src/tree/toplevel.rs | 49 ++++++-
|
||||
src/tree/workspace.rs | 8 +-
|
||||
toml-config/src/config.rs | 3 +-
|
||||
toml-config/src/config/parsers/theme.rs | 23 +++-
|
||||
toml-config/src/lib.rs | 5 +-
|
||||
16 files changed, 494 insertions(+), 40 deletions(-)
|
||||
|
||||
diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs
|
||||
index 9581b771..2a1ee047 100644
|
||||
--- a/jay-config/src/_private/client.rs
|
||||
+++ b/jay-config/src/_private/client.rs
|
||||
@@ -26,7 +26,9 @@ use {
|
||||
},
|
||||
logging::LogLevel,
|
||||
tasks::{JoinHandle, JoinSlot},
|
||||
- theme::{AnimationMode, BarPosition, Color, colors::Colorable, sized::Resizable},
|
||||
+ theme::{
|
||||
+ AnimationMode, BarPosition, Color, SpawnAnimation, colors::Colorable, sized::Resizable,
|
||||
+ },
|
||||
timer::Timer,
|
||||
video::{
|
||||
BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, Mode, TearingMode,
|
||||
@@ -1053,6 +1055,16 @@ impl ConfigClient {
|
||||
mode
|
||||
}
|
||||
|
||||
+ pub fn set_spawn_animation(&self, animation: SpawnAnimation) {
|
||||
+ self.send(&ClientMessage::SetSpawnAnimation { animation });
|
||||
+ }
|
||||
+
|
||||
+ pub fn get_spawn_animation(&self) -> SpawnAnimation {
|
||||
+ let res = self.send_with_response(&ClientMessage::GetSpawnAnimation);
|
||||
+ get_response!(res, SpawnAnimation::None, GetSpawnAnimation { animation });
|
||||
+ animation
|
||||
+ }
|
||||
+
|
||||
pub fn set_middle_click_paste_enabled(&self, enabled: bool) {
|
||||
self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled });
|
||||
}
|
||||
diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs
|
||||
index a4ebc27c..9ed55666 100644
|
||||
--- a/jay-config/src/_private/ipc.rs
|
||||
+++ b/jay-config/src/_private/ipc.rs
|
||||
@@ -10,7 +10,9 @@ use {
|
||||
},
|
||||
keyboard::{Group, Keymap, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
- theme::{AnimationMode, BarPosition, Color, colors::Colorable, sized::Resizable},
|
||||
+ theme::{
|
||||
+ AnimationMode, BarPosition, Color, SpawnAnimation, colors::Colorable, sized::Resizable,
|
||||
+ },
|
||||
timer::Timer,
|
||||
video::{
|
||||
BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, TearingMode,
|
||||
@@ -834,6 +836,10 @@ pub enum ClientMessage<'a> {
|
||||
mode: AnimationMode,
|
||||
},
|
||||
GetAnimationMode,
|
||||
+ SetSpawnAnimation {
|
||||
+ animation: SpawnAnimation,
|
||||
+ },
|
||||
+ GetSpawnAnimation,
|
||||
ConnectorSetUseNativeGamut {
|
||||
connector: Connector,
|
||||
use_native_gamut: bool,
|
||||
@@ -1108,6 +1114,9 @@ pub enum Response {
|
||||
GetAnimationMode {
|
||||
mode: AnimationMode,
|
||||
},
|
||||
+ GetSpawnAnimation {
|
||||
+ animation: SpawnAnimation,
|
||||
+ },
|
||||
GetCursorFollowsFocus {
|
||||
enabled: bool,
|
||||
},
|
||||
diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs
|
||||
index 90945b47..18cb2ff9 100644
|
||||
--- a/jay-config/src/theme.rs
|
||||
+++ b/jay-config/src/theme.rs
|
||||
@@ -221,6 +221,37 @@ pub fn get_animation_mode() -> AnimationMode {
|
||||
get!(AnimationMode::Concurrent).get_animation_mode()
|
||||
}
|
||||
|
||||
+/// The animation used when tiled windows are opened or closed.
|
||||
+#[non_exhaustive]
|
||||
+#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
+pub enum SpawnAnimation {
|
||||
+ /// No spawn/despawn animation.
|
||||
+ #[default]
|
||||
+ None,
|
||||
+ /// Window slides in from the left.
|
||||
+ SlideLeft,
|
||||
+ /// Window slides in from the right.
|
||||
+ SlideRight,
|
||||
+ /// Window slides in from the top.
|
||||
+ SlideTop,
|
||||
+ /// Window slides in from the bottom.
|
||||
+ SlideBottom,
|
||||
+ /// Window grows from the center.
|
||||
+ Grow,
|
||||
+}
|
||||
+
|
||||
+/// Sets the spawn animation for tiled windows.
|
||||
+///
|
||||
+/// Default: `None`.
|
||||
+pub fn set_spawn_animation(animation: SpawnAnimation) {
|
||||
+ get!().set_spawn_animation(animation);
|
||||
+}
|
||||
+
|
||||
+/// Gets the current spawn animation.
|
||||
+pub fn get_spawn_animation() -> SpawnAnimation {
|
||||
+ get!(SpawnAnimation::None).get_spawn_animation()
|
||||
+}
|
||||
+
|
||||
/// Elements of the compositor whose color can be changed.
|
||||
pub mod colors {
|
||||
use {
|
||||
diff --git a/src/animation.rs b/src/animation.rs
|
||||
index 02d034a7..ed1d1f8d 100644
|
||||
--- a/src/animation.rs
|
||||
+++ b/src/animation.rs
|
||||
@@ -16,6 +16,7 @@ pub struct PositionAnimation {
|
||||
pub duration_usec: u64,
|
||||
pub mode_x: AnimationMode,
|
||||
pub mode_y: AnimationMode,
|
||||
+ pub reverse: bool,
|
||||
}
|
||||
|
||||
fn ease_out_cubic(t: f64) -> f64 {
|
||||
@@ -63,11 +64,21 @@ impl PositionAnimation {
|
||||
let t = elapsed as f64 / self.duration_usec as f64;
|
||||
let (move_remaining_x, size_remaining_x) = sequential_factors(t, self.mode_x);
|
||||
let (move_remaining_y, size_remaining_y) = sequential_factors(t, self.mode_y);
|
||||
- let dx = (self.offset_x * move_remaining_x).round() as i32;
|
||||
- let dy = (self.offset_y * move_remaining_y).round() as i32;
|
||||
- let dw = (self.offset_w * size_remaining_x).round() as i32;
|
||||
- let dh = (self.offset_h * size_remaining_y).round() as i32;
|
||||
- if dx == 0 && dy == 0 && dw == 0 && dh == 0 {
|
||||
+ let (move_factor_x, size_factor_x, move_factor_y, size_factor_y) = if self.reverse {
|
||||
+ (
|
||||
+ 1.0 - move_remaining_x,
|
||||
+ 1.0 - size_remaining_x,
|
||||
+ 1.0 - move_remaining_y,
|
||||
+ 1.0 - size_remaining_y,
|
||||
+ )
|
||||
+ } else {
|
||||
+ (move_remaining_x, size_remaining_x, move_remaining_y, size_remaining_y)
|
||||
+ };
|
||||
+ let dx = (self.offset_x * move_factor_x).round() as i32;
|
||||
+ let dy = (self.offset_y * move_factor_y).round() as i32;
|
||||
+ let dw = (self.offset_w * size_factor_x).round() as i32;
|
||||
+ let dh = (self.offset_h * size_factor_y).round() as i32;
|
||||
+ if dx == 0 && dy == 0 && dw == 0 && dh == 0 && !self.reverse {
|
||||
return None;
|
||||
}
|
||||
Some((dx, dy, dw, dh))
|
||||
diff --git a/src/config/handler.rs b/src/config/handler.rs
|
||||
index 88d801b7..5a919daa 100644
|
||||
--- a/src/config/handler.rs
|
||||
+++ b/src/config/handler.rs
|
||||
@@ -67,7 +67,9 @@ use {
|
||||
},
|
||||
keyboard::{Group, Keymap, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
- theme::{AnimationMode, BarPosition, colors::Colorable, sized::Resizable},
|
||||
+ theme::{
|
||||
+ AnimationMode, BarPosition, SpawnAnimation, colors::Colorable, sized::Resizable,
|
||||
+ },
|
||||
timer::Timer as JayTimer,
|
||||
video::{
|
||||
BlendSpace as ConfigBlendSpace, ColorSpace, Connector, DrmDevice, Eotf as ConfigEotf,
|
||||
@@ -1488,6 +1490,20 @@ impl ConfigProxyHandler {
|
||||
});
|
||||
}
|
||||
|
||||
+ fn handle_set_spawn_animation(&self, animation: SpawnAnimation) -> Result<(), CphError> {
|
||||
+ let Ok(animation) = animation.try_into() else {
|
||||
+ return Err(CphError::UnknownSpawnAnimation(animation));
|
||||
+ };
|
||||
+ self.state.theme.spawn_animation.set(animation);
|
||||
+ Ok(())
|
||||
+ }
|
||||
+
|
||||
+ fn handle_get_spawn_animation(&self) {
|
||||
+ self.respond(Response::GetSpawnAnimation {
|
||||
+ animation: self.state.theme.spawn_animation.get().into(),
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
fn handle_set_show_float_pin_icon(&self, show: bool) {
|
||||
self.state.show_pin_icon.set(show);
|
||||
for stacked in self.state.root.stacked.iter() {
|
||||
@@ -3374,6 +3390,10 @@ impl ConfigProxyHandler {
|
||||
.handle_set_animation_mode(mode)
|
||||
.wrn("set_animation_mode")?,
|
||||
ClientMessage::GetAnimationMode => self.handle_get_animation_mode(),
|
||||
+ ClientMessage::SetSpawnAnimation { animation } => self
|
||||
+ .handle_set_spawn_animation(animation)
|
||||
+ .wrn("set_spawn_animation")?,
|
||||
+ ClientMessage::GetSpawnAnimation => self.handle_get_spawn_animation(),
|
||||
ClientMessage::SeatFocusHistory { seat, timeline } => self
|
||||
.handle_seat_focus_history(seat, timeline)
|
||||
.wrn("seat_focus_history")?,
|
||||
@@ -3625,6 +3645,8 @@ enum CphError {
|
||||
UnknownBarPosition(BarPosition),
|
||||
#[error("Unknown animation mode {0:?}")]
|
||||
UnknownAnimationMode(AnimationMode),
|
||||
+ #[error("Unknown spawn animation {0:?}")]
|
||||
+ UnknownSpawnAnimation(SpawnAnimation),
|
||||
#[error("Unknown gfx API {0:?}")]
|
||||
UnknownGfxApi(GfxApi),
|
||||
#[error("Unknown fallback output mode {0:?}")]
|
||||
diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs
|
||||
index e9f4ced1..1a045731 100644
|
||||
--- a/src/ifs/wl_seat/pointer_owner.rs
|
||||
+++ b/src/ifs/wl_seat/pointer_owner.rs
|
||||
@@ -1517,6 +1517,7 @@ impl UiDragUsecase for TileDragUsecase {
|
||||
&workspace,
|
||||
src.clone(),
|
||||
ContainerSplit::Horizontal,
|
||||
+ None,
|
||||
);
|
||||
workspace.set_container(&cn);
|
||||
};
|
||||
@@ -1542,7 +1543,7 @@ impl UiDragUsecase for TileDragUsecase {
|
||||
return;
|
||||
};
|
||||
let placeholder = detach();
|
||||
- let cn = ContainerNode::new(&seat.state, &ws, node.clone(), split);
|
||||
+ let cn = ContainerNode::new(&seat.state, &ws, node.clone(), split, None);
|
||||
pn.cnode_replace_child(&*node, cn.clone());
|
||||
match before {
|
||||
true => cn.add_child_before(&*node, src),
|
||||
diff --git a/src/renderer.rs b/src/renderer.rs
|
||||
index c2622fac..93178d77 100644
|
||||
--- a/src/renderer.rs
|
||||
+++ b/src/renderer.rs
|
||||
@@ -221,6 +221,38 @@ impl Renderer<'_> {
|
||||
if let Some(node) = workspace.container.get() {
|
||||
self.render_container(&node, x, y)
|
||||
}
|
||||
+ let now_usec = Time::now_unchecked().usec();
|
||||
+ let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
+ let srgb = &srgb_srgb.linear;
|
||||
+ let border_color = self.state.theme.colors.border.get();
|
||||
+ let focused_border_color = self.state.theme.colors.focused_title_background.get();
|
||||
+ let mut dying = workspace.dying_children.borrow_mut();
|
||||
+ let mut has_dying = false;
|
||||
+ dying.retain(|dc| {
|
||||
+ let Some((dx, dy, dw, dh)) = dc.animation.current_offset(now_usec) else {
|
||||
+ return false;
|
||||
+ };
|
||||
+ has_dying = true;
|
||||
+ let anim_w = (dc.body.width() + dw).max(1);
|
||||
+ let anim_h = (dc.body.height() + dh).max(1);
|
||||
+ let anim_body = Rect::new_sized_saturating(
|
||||
+ dc.body.x1() + dx,
|
||||
+ dc.body.y1() + dy,
|
||||
+ anim_w,
|
||||
+ anim_h,
|
||||
+ );
|
||||
+ let c = if dc.border_color_is_focused {
|
||||
+ &focused_border_color
|
||||
+ } else {
|
||||
+ &border_color
|
||||
+ };
|
||||
+ self.base
|
||||
+ .fill_boxes2(std::slice::from_ref(&anim_body), c, srgb, x, y);
|
||||
+ true
|
||||
+ });
|
||||
+ if has_dying {
|
||||
+ self.state.damage(workspace.position.get());
|
||||
+ }
|
||||
}
|
||||
|
||||
pub fn render_placeholder(
|
||||
@@ -314,18 +346,75 @@ impl Renderer<'_> {
|
||||
}
|
||||
}
|
||||
if let Some(child) = container.mono_child.get() {
|
||||
- let body = container.mono_body.get().move_(x, y);
|
||||
- let body = self.base.scale_rect(body);
|
||||
- let content = container.mono_content.get();
|
||||
- child
|
||||
- .node
|
||||
- .node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
||||
+ let mb = container.mono_body.get();
|
||||
+ let now_usec = Time::now_unchecked().usec();
|
||||
+ let (anim_dx, anim_dy, anim_dw, anim_dh) = child
|
||||
+ .body_animation
|
||||
+ .get()
|
||||
+ .and_then(|a| a.current_offset(now_usec))
|
||||
+ .unwrap_or((0, 0, 0, 0));
|
||||
+ if anim_dx != 0 || anim_dy != 0 || anim_dw != 0 || anim_dh != 0 {
|
||||
+ let anim_w = (mb.width() + anim_dw).max(1);
|
||||
+ let anim_h = (mb.height() + anim_dh).max(1);
|
||||
+ if anim_w < mb.width() || anim_h < mb.height() {
|
||||
+ let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
+ let srgb = &srgb_srgb.linear;
|
||||
+ let border_color = self.state.theme.colors.background.get();
|
||||
+ let anim_body = Rect::new_sized_saturating(
|
||||
+ mb.x1() + anim_dx,
|
||||
+ mb.y1() + anim_dy,
|
||||
+ anim_w,
|
||||
+ anim_h,
|
||||
+ );
|
||||
+ self.base.fill_boxes2(
|
||||
+ std::slice::from_ref(&anim_body),
|
||||
+ &border_color,
|
||||
+ srgb,
|
||||
+ x,
|
||||
+ y,
|
||||
+ );
|
||||
+ }
|
||||
+ let clip_w = anim_w.min(mb.width());
|
||||
+ let clip_h = anim_h.min(mb.height());
|
||||
+ let body = Rect::new_sized_saturating(
|
||||
+ mb.x1() + anim_dx,
|
||||
+ mb.y1() + anim_dy,
|
||||
+ clip_w,
|
||||
+ clip_h,
|
||||
+ );
|
||||
+ let body = body.move_(x, y);
|
||||
+ let body = self.base.scale_rect(body);
|
||||
+ let content = container.mono_content.get();
|
||||
+ child.node.node_render(
|
||||
+ self,
|
||||
+ x + content.x1() + anim_dx,
|
||||
+ y + content.y1() + anim_dy,
|
||||
+ Some(&body),
|
||||
+ );
|
||||
+ let abs_x = container.abs_x1.get();
|
||||
+ let abs_y = container.abs_y1.get();
|
||||
+ self.state.damage(Rect::new_sized_saturating(
|
||||
+ abs_x,
|
||||
+ abs_y,
|
||||
+ container.width.get(),
|
||||
+ container.height.get(),
|
||||
+ ));
|
||||
+ } else {
|
||||
+ child.body_animation.set(None);
|
||||
+ let body = mb.move_(x, y);
|
||||
+ let body = self.base.scale_rect(body);
|
||||
+ let content = container.mono_content.get();
|
||||
+ child
|
||||
+ .node
|
||||
+ .node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
||||
+ }
|
||||
} else {
|
||||
let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
let srgb = &srgb_srgb.linear;
|
||||
let theme = &self.state.theme;
|
||||
let colors = &theme.colors;
|
||||
let border_color = colors.border.get();
|
||||
+ let background_color = colors.background.get();
|
||||
let focused_border_color = colors.focused_title_background.get();
|
||||
let unfocused_title_bg = colors.unfocused_title_background.get();
|
||||
let focused_title_bg = colors.focused_title_background.get();
|
||||
@@ -483,7 +572,7 @@ impl Renderer<'_> {
|
||||
if anim_w > body.width() || anim_h > body.height() {
|
||||
self.base.fill_boxes2(
|
||||
std::slice::from_ref(&anim_body),
|
||||
- &border_color,
|
||||
+ &background_color,
|
||||
srgb,
|
||||
x,
|
||||
y,
|
||||
@@ -704,7 +793,32 @@ impl Renderer<'_> {
|
||||
Some(c) => c,
|
||||
_ => return,
|
||||
};
|
||||
- let pos = floating.position.get();
|
||||
+ let now_usec = Time::now_unchecked().usec();
|
||||
+ let base_pos = floating.position.get();
|
||||
+ let (anim_dx, anim_dy, anim_dw, anim_dh) =
|
||||
+ if let Some(anim) = floating.body_animation.get() {
|
||||
+ match anim.current_offset(now_usec) {
|
||||
+ Some(offsets) => offsets,
|
||||
+ None => {
|
||||
+ floating.body_animation.set(None);
|
||||
+ self.state.damage(base_pos);
|
||||
+ (0, 0, 0, 0)
|
||||
+ }
|
||||
+ }
|
||||
+ } else {
|
||||
+ (0, 0, 0, 0)
|
||||
+ };
|
||||
+ let anim_w = (base_pos.width() + anim_dw).max(1);
|
||||
+ let anim_h = (base_pos.height() + anim_dh).max(1);
|
||||
+ let pos = Rect::new_sized_saturating(
|
||||
+ base_pos.x1() + anim_dx,
|
||||
+ base_pos.y1() + anim_dy,
|
||||
+ anim_w,
|
||||
+ anim_h,
|
||||
+ );
|
||||
+ if anim_dx != 0 || anim_dy != 0 || anim_dw != 0 || anim_dh != 0 {
|
||||
+ self.state.damage(base_pos.union(pos));
|
||||
+ }
|
||||
let theme = &self.state.theme;
|
||||
let th = theme.title_height();
|
||||
let tpuh = theme.title_plus_underline_height();
|
||||
diff --git a/src/state.rs b/src/state.rs
|
||||
index 4eee9bf7..86a42dee 100644
|
||||
--- a/src/state.rs
|
||||
+++ b/src/state.rs
|
||||
@@ -813,7 +813,7 @@ impl State {
|
||||
c.append_child(node);
|
||||
}
|
||||
} else {
|
||||
- let container = ContainerNode::new(self, ws, node, ContainerSplit::Horizontal);
|
||||
+ let container = ContainerNode::new(self, ws, node, ContainerSplit::Horizontal, None);
|
||||
ws.set_container(&container);
|
||||
}
|
||||
}
|
||||
diff --git a/src/theme.rs b/src/theme.rs
|
||||
index 0709f3d4..f705c4fd 100644
|
||||
--- a/src/theme.rs
|
||||
+++ b/src/theme.rs
|
||||
@@ -8,6 +8,7 @@ use {
|
||||
},
|
||||
jay_config::theme::{
|
||||
AnimationMode as ConfigAnimationMode, BarPosition as ConfigBarPosition,
|
||||
+ SpawnAnimation as ConfigSpawnAnimation,
|
||||
},
|
||||
linearize::Linearize,
|
||||
num_traits::Float,
|
||||
@@ -578,6 +579,47 @@ pub enum AnimationMode {
|
||||
Sequential,
|
||||
}
|
||||
|
||||
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
+pub enum SpawnAnimation {
|
||||
+ #[default]
|
||||
+ None,
|
||||
+ SlideLeft,
|
||||
+ SlideRight,
|
||||
+ SlideTop,
|
||||
+ SlideBottom,
|
||||
+ Grow,
|
||||
+}
|
||||
+
|
||||
+impl TryFrom<ConfigSpawnAnimation> for SpawnAnimation {
|
||||
+ type Error = ();
|
||||
+
|
||||
+ fn try_from(value: ConfigSpawnAnimation) -> Result<Self, Self::Error> {
|
||||
+ let v = match value {
|
||||
+ ConfigSpawnAnimation::None => Self::None,
|
||||
+ ConfigSpawnAnimation::SlideLeft => Self::SlideLeft,
|
||||
+ ConfigSpawnAnimation::SlideRight => Self::SlideRight,
|
||||
+ ConfigSpawnAnimation::SlideTop => Self::SlideTop,
|
||||
+ ConfigSpawnAnimation::SlideBottom => Self::SlideBottom,
|
||||
+ ConfigSpawnAnimation::Grow => Self::Grow,
|
||||
+ _ => return Err(()),
|
||||
+ };
|
||||
+ Ok(v)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+impl Into<ConfigSpawnAnimation> for SpawnAnimation {
|
||||
+ fn into(self) -> ConfigSpawnAnimation {
|
||||
+ match self {
|
||||
+ SpawnAnimation::None => ConfigSpawnAnimation::None,
|
||||
+ SpawnAnimation::SlideLeft => ConfigSpawnAnimation::SlideLeft,
|
||||
+ SpawnAnimation::SlideRight => ConfigSpawnAnimation::SlideRight,
|
||||
+ SpawnAnimation::SlideTop => ConfigSpawnAnimation::SlideTop,
|
||||
+ SpawnAnimation::SlideBottom => ConfigSpawnAnimation::SlideBottom,
|
||||
+ SpawnAnimation::Grow => ConfigSpawnAnimation::Grow,
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
pub struct Theme {
|
||||
pub colors: ThemeColors,
|
||||
pub sizes: ThemeSizes,
|
||||
@@ -588,6 +630,7 @@ pub struct Theme {
|
||||
pub show_titles: Cell<bool>,
|
||||
pub bar_position: Cell<BarPosition>,
|
||||
pub animation_mode: Cell<AnimationMode>,
|
||||
+ pub spawn_animation: Cell<SpawnAnimation>,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
@@ -603,6 +646,7 @@ impl Default for Theme {
|
||||
show_titles: Cell::new(true),
|
||||
bar_position: Default::default(),
|
||||
animation_mode: Default::default(),
|
||||
+ spawn_animation: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
diff --git a/src/tree/container.rs b/src/tree/container.rs
|
||||
index 85a0b97d..2d8d328d 100644
|
||||
--- a/src/tree/container.rs
|
||||
+++ b/src/tree/container.rs
|
||||
@@ -165,6 +165,12 @@ pub struct ContainerChild {
|
||||
pub border_color_is_focused: Cell<bool>,
|
||||
}
|
||||
|
||||
+pub struct DyingChild {
|
||||
+ pub body: Rect,
|
||||
+ pub animation: PositionAnimation,
|
||||
+ pub border_color_is_focused: bool,
|
||||
+}
|
||||
+
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
enum CursorType {
|
||||
Seat(SeatId),
|
||||
@@ -203,12 +209,13 @@ impl ContainerNode {
|
||||
workspace: &Rc<WorkspaceNode>,
|
||||
child: Rc<dyn ToplevelNode>,
|
||||
split: ContainerSplit,
|
||||
+ initial_child_body: Option<Rect>,
|
||||
) -> Rc<Self> {
|
||||
let children = LinkedList::new();
|
||||
let child_node = children.add_last(ContainerChild {
|
||||
node: child.clone(),
|
||||
active: Default::default(),
|
||||
- body: Default::default(),
|
||||
+ body: Cell::new(initial_child_body.unwrap_or_default()),
|
||||
content: Default::default(),
|
||||
factor: Cell::new(1.0),
|
||||
title: Default::default(),
|
||||
@@ -453,6 +460,48 @@ impl ContainerNode {
|
||||
};
|
||||
if let Some(child) = self.mono_child.get() {
|
||||
self.perform_mono_layout(&child);
|
||||
+ if animation_duration > 0 && child.body.get().is_empty() {
|
||||
+ let mb = self.mono_body.get();
|
||||
+ if !mb.is_empty() {
|
||||
+ let spawn = self.state.theme.spawn_animation.get();
|
||||
+ let (dx, dy, dw, dh): (f64, f64, f64, f64) = match spawn {
|
||||
+ crate::theme::SpawnAnimation::None => (0.0, 0.0, 0.0, 0.0),
|
||||
+ crate::theme::SpawnAnimation::SlideLeft => {
|
||||
+ (-(mb.width() as f64), 0.0, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideRight => {
|
||||
+ (mb.width() as f64, 0.0, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideTop => {
|
||||
+ (0.0, -(mb.height() as f64), 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideBottom => {
|
||||
+ (0.0, mb.height() as f64, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::Grow => (
|
||||
+ mb.width() as f64 / 2.0,
|
||||
+ mb.height() as f64 / 2.0,
|
||||
+ -(mb.width() as f64),
|
||||
+ -(mb.height() as f64),
|
||||
+ ),
|
||||
+ };
|
||||
+ if dx != 0.0 || dy != 0.0 || dw != 0.0 || dh != 0.0 {
|
||||
+ let now_usec = Time::now_unchecked().usec();
|
||||
+ child.body_animation.set(Some(PositionAnimation {
|
||||
+ offset_x: dx,
|
||||
+ offset_y: dy,
|
||||
+ offset_w: dw,
|
||||
+ offset_h: dh,
|
||||
+ start_usec: now_usec,
|
||||
+ duration_usec: (animation_duration as u64) * 1000,
|
||||
+ mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
+ mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
+ reverse: false,
|
||||
+ }));
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ child.body.set(self.mono_body.get());
|
||||
} else {
|
||||
self.perform_split_layout();
|
||||
}
|
||||
@@ -498,7 +547,46 @@ impl ContainerNode {
|
||||
let child_id = child.node.node_id();
|
||||
let old = match old_bodies.iter().find(|(id, _)| *id == child_id) {
|
||||
Some((_, rect)) if !rect.is_empty() => *rect,
|
||||
- _ => continue,
|
||||
+ _ => {
|
||||
+ let new_body = child.body.get();
|
||||
+ if new_body.is_empty() {
|
||||
+ continue;
|
||||
+ }
|
||||
+ let spawn = self.state.theme.spawn_animation.get();
|
||||
+ let (dx, dy, dw, dh): (f64, f64, f64, f64) = match spawn {
|
||||
+ crate::theme::SpawnAnimation::None => continue,
|
||||
+ crate::theme::SpawnAnimation::SlideLeft => {
|
||||
+ (-(new_body.width() as f64), 0.0, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideRight => {
|
||||
+ (new_body.width() as f64, 0.0, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideTop => {
|
||||
+ (0.0, -(new_body.height() as f64), 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideBottom => {
|
||||
+ (0.0, new_body.height() as f64, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::Grow => (
|
||||
+ new_body.width() as f64 / 2.0,
|
||||
+ new_body.height() as f64 / 2.0,
|
||||
+ -(new_body.width() as f64),
|
||||
+ -(new_body.height() as f64),
|
||||
+ ),
|
||||
+ };
|
||||
+ child.body_animation.set(Some(PositionAnimation {
|
||||
+ offset_x: dx,
|
||||
+ offset_y: dy,
|
||||
+ offset_w: dw,
|
||||
+ offset_h: dh,
|
||||
+ start_usec: now_usec,
|
||||
+ duration_usec: (animation_duration as u64) * 1000,
|
||||
+ mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
+ mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
+ reverse: false,
|
||||
+ }));
|
||||
+ continue;
|
||||
+ }
|
||||
};
|
||||
let new_body = child.body.get();
|
||||
let dx = old.x1() - new_body.x1();
|
||||
@@ -2158,24 +2246,75 @@ impl ContainingNode for ContainerNode {
|
||||
self.activate_child2(child, preserve_focus);
|
||||
}
|
||||
}
|
||||
+ } else if !preserve_focus {
|
||||
+ if let Some(next) = node.next().or_else(|| node.prev()) {
|
||||
+ let mut seats = SmallVec::<[_; 3]>::new();
|
||||
+ collect_kb_foci2(node.node.clone(), &mut seats);
|
||||
+ for seat in seats {
|
||||
+ next.node.clone().node_do_focus(&seat, Direction::Unspecified);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
+ let child_body = node.body.get();
|
||||
+ let child_focused = node.border_color_is_focused.get();
|
||||
let node = {
|
||||
let node = node;
|
||||
node.focus_history.set(None);
|
||||
node.to_ref()
|
||||
};
|
||||
+ let animation_duration = self.state.theme.sizes.animation_duration.get();
|
||||
+ let spawn = self.state.theme.spawn_animation.get();
|
||||
+ if animation_duration > 0
|
||||
+ && spawn != crate::theme::SpawnAnimation::None
|
||||
+ && !child_body.is_empty()
|
||||
+ && !preserve_focus
|
||||
+ {
|
||||
+ let (dx, dy, dw, dh): (f64, f64, f64, f64) = match spawn {
|
||||
+ crate::theme::SpawnAnimation::None => unreachable!(),
|
||||
+ crate::theme::SpawnAnimation::SlideLeft => {
|
||||
+ (-(child_body.width() as f64), 0.0, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideRight => {
|
||||
+ (child_body.width() as f64, 0.0, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideTop => {
|
||||
+ (0.0, -(child_body.height() as f64), 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::SlideBottom => {
|
||||
+ (0.0, child_body.height() as f64, 0.0, 0.0)
|
||||
+ }
|
||||
+ crate::theme::SpawnAnimation::Grow => (
|
||||
+ child_body.width() as f64 / 2.0,
|
||||
+ child_body.height() as f64 / 2.0,
|
||||
+ -(child_body.width() as f64),
|
||||
+ -(child_body.height() as f64),
|
||||
+ ),
|
||||
+ };
|
||||
+ let now_usec = Time::now_unchecked().usec();
|
||||
+ let ws_pos = self.workspace.get().position.get();
|
||||
+ let ws_child_body = child_body.move_(
|
||||
+ self.abs_x1.get() - ws_pos.x1(),
|
||||
+ self.abs_y1.get() - ws_pos.y1(),
|
||||
+ );
|
||||
+ self.workspace.get().dying_children.borrow_mut().push(DyingChild {
|
||||
+ body: ws_child_body,
|
||||
+ animation: PositionAnimation {
|
||||
+ offset_x: dx,
|
||||
+ offset_y: dy,
|
||||
+ offset_w: dw,
|
||||
+ offset_h: dh,
|
||||
+ start_usec: now_usec,
|
||||
+ duration_usec: (animation_duration as u64) * 1000,
|
||||
+ mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
+ mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
+ reverse: true,
|
||||
+ },
|
||||
+ border_color_is_focused: child_focused,
|
||||
+ });
|
||||
+ self.state.damage(self.workspace.get().position.get());
|
||||
+ }
|
||||
let num_children = self.num_children.fetch_sub(1) - 1;
|
||||
if num_children == 0 {
|
||||
- let gap = self.state.theme.sizes.gap.get();
|
||||
- if gap > 0 {
|
||||
- let bw = self.state.theme.sizes.border_width.get();
|
||||
- self.state.damage(Rect::new_sized_saturating(
|
||||
- self.abs_x1.get() - bw,
|
||||
- self.abs_y1.get() - bw,
|
||||
- self.width.get() + 2 * bw,
|
||||
- self.height.get() + 2 * bw,
|
||||
- ));
|
||||
- }
|
||||
self.tl_destroy();
|
||||
return;
|
||||
}
|
||||
diff --git a/src/tree/float.rs b/src/tree/float.rs
|
||||
index ccad4860..7d610898 100644
|
||||
--- a/src/tree/float.rs
|
||||
+++ b/src/tree/float.rs
|
||||
@@ -1,5 +1,6 @@
|
||||
use {
|
||||
crate::{
|
||||
+ animation::PositionAnimation,
|
||||
backend::ButtonState,
|
||||
cursor::KnownCursor,
|
||||
cursor_user::CursorUser,
|
||||
@@ -56,6 +57,7 @@ pub struct FloatNode {
|
||||
pub title_textures: RefCell<SmallMapMut<Scale, TextTexture, 2>>,
|
||||
cursors: RefCell<AHashMap<CursorType, CursorState>>,
|
||||
pub attention_requested: Cell<bool>,
|
||||
+ pub body_animation: Cell<Option<PositionAnimation>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -136,6 +138,7 @@ impl FloatNode {
|
||||
title_textures: Default::default(),
|
||||
cursors: Default::default(),
|
||||
attention_requested: Cell::new(false),
|
||||
+ body_animation: Default::default(),
|
||||
});
|
||||
floater.pull_child_properties();
|
||||
*floater.display_link.borrow_mut() = Some(state.root.stacked.add_last(floater.clone()));
|
||||
diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs
|
||||
index 8ec1e105..2e48b2fd 100644
|
||||
--- a/src/tree/toplevel.rs
|
||||
+++ b/src/tree/toplevel.rs
|
||||
@@ -994,8 +994,10 @@ pub fn toplevel_create_split(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, axis:
|
||||
_ => return,
|
||||
};
|
||||
if let Some(pn) = pn.node_into_containing_node() {
|
||||
- let cn = ContainerNode::new(state, &ws, tl.clone(), axis);
|
||||
- pn.cnode_replace_child(&*tl, cn);
|
||||
+ let abs_body = tl.node_absolute_position();
|
||||
+ let cn = ContainerNode::new(state, &ws, tl.clone(), axis, None);
|
||||
+ pn.cnode_replace_child(&*tl, cn.clone());
|
||||
+ cn.seed_child_body(tl.node_id(), abs_body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,12 +1014,49 @@ pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floati
|
||||
_ => return,
|
||||
};
|
||||
if !floating {
|
||||
+ // Float-to-tile: capture float position before removal
|
||||
+ let float_pos = data.float.get().map(|f| f.position.get());
|
||||
parent.cnode_remove_child2(&*tl, true);
|
||||
- state.map_tiled(tl);
|
||||
+ state.map_tiled(tl.clone());
|
||||
+ // Seed the child's body from the float position so perform_layout
|
||||
+ // computes a meaningful delta (slide from float pos to tiled pos)
|
||||
+ if let Some(float_rect) = float_pos {
|
||||
+ if let Some(cn) = data.parent.get().and_then(|p| p.node_into_container()) {
|
||||
+ cn.seed_child_body(tl.node_id(), float_rect);
|
||||
+ }
|
||||
+ }
|
||||
} else if let Some(ws) = data.workspace.get() {
|
||||
- parent.cnode_remove_child2(&*tl, true);
|
||||
+ // Tile-to-float: capture tiled position and size before removal
|
||||
+ let tiled_pos = tl.node_absolute_position();
|
||||
let (width, height) = data.float_size(&ws);
|
||||
- state.map_floating(tl, width, height, &ws, None);
|
||||
+ let animation_duration = state.theme.sizes.animation_duration.get();
|
||||
+ let _ = data;
|
||||
+ parent.cnode_remove_child2(&*tl, true);
|
||||
+ state.map_floating(tl.clone(), width, height, &ws, None);
|
||||
+ // Set animation on the new FloatNode
|
||||
+ let data = tl.tl_data();
|
||||
+ if let Some(float) = data.float.get() {
|
||||
+ let target = float.position.get();
|
||||
+ if !tiled_pos.is_empty() && !target.is_empty() && animation_duration > 0 {
|
||||
+ let dx = (tiled_pos.x1() - target.x1()) as f64;
|
||||
+ let dy = (tiled_pos.y1() - target.y1()) as f64;
|
||||
+ let dw = (tiled_pos.width() - target.width()) as f64;
|
||||
+ let dh = (tiled_pos.height() - target.height()) as f64;
|
||||
+ let now_usec = crate::time::Time::now_unchecked().usec();
|
||||
+ float.body_animation.set(Some(crate::animation::PositionAnimation {
|
||||
+ offset_x: dx,
|
||||
+ offset_y: dy,
|
||||
+ offset_w: dw,
|
||||
+ offset_h: dh,
|
||||
+ start_usec: now_usec,
|
||||
+ duration_usec: (animation_duration as u64) * 1000,
|
||||
+ mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
+ mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
+ reverse: false,
|
||||
+ }));
|
||||
+ state.damage(tiled_pos.union(target));
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs
|
||||
index f60354a4..5378e71c 100644
|
||||
--- a/src/tree/workspace.rs
|
||||
+++ b/src/tree/workspace.rs
|
||||
@@ -23,7 +23,7 @@ use {
|
||||
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node,
|
||||
NodeId, NodeLayerLink, NodeLocation, NodeVisitorBase, OutputNode, OutputNodeId,
|
||||
PlaceholderNode, StackedNode, ToplevelNode, WorkspaceDisplayOrder,
|
||||
- container::ContainerNode, walker::NodeVisitor,
|
||||
+ container::{ContainerNode, DyingChild}, walker::NodeVisitor,
|
||||
},
|
||||
utils::{
|
||||
clonecell::CloneCell,
|
||||
@@ -68,6 +68,7 @@ pub struct WorkspaceNode {
|
||||
pub attention_requests: ThresholdCounter,
|
||||
pub render_highlight: NumCell<u32>,
|
||||
pub ext_workspaces: CopyHashMap<WorkspaceManagerId, Rc<ExtWorkspaceHandleV1>>,
|
||||
+ pub dying_children: RefCell<Vec<DyingChild>>,
|
||||
pub opt: Rc<Opt<WorkspaceNode>>,
|
||||
}
|
||||
|
||||
@@ -96,6 +97,7 @@ impl WorkspaceNode {
|
||||
attention_requests: Default::default(),
|
||||
render_highlight: Default::default(),
|
||||
ext_workspaces: Default::default(),
|
||||
+ dying_children: Default::default(),
|
||||
opt: Default::default(),
|
||||
});
|
||||
slf.seat_state.disable_focus_history();
|
||||
@@ -104,6 +106,7 @@ impl WorkspaceNode {
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.container.set(None);
|
||||
+ self.dying_children.borrow_mut().clear();
|
||||
*self.output_link.borrow_mut() = None;
|
||||
self.fullscreen.set(None);
|
||||
self.jay_workspaces.clear();
|
||||
@@ -231,6 +234,9 @@ impl WorkspaceNode {
|
||||
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
self.visible.set(visible);
|
||||
+ if !visible {
|
||||
+ self.dying_children.borrow_mut().clear();
|
||||
+ }
|
||||
for jw in self.jay_workspaces.lock().values() {
|
||||
jw.send_visible(visible);
|
||||
}
|
||||
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
|
||||
index cedcf78f..39b9fab4 100644
|
||||
--- a/toml-config/src/config.rs
|
||||
+++ b/toml-config/src/config.rs
|
||||
@@ -32,7 +32,7 @@ use {
|
||||
keyboard::{Keymap, ModifiedKeySym, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
status::MessageFormat,
|
||||
- theme::{AnimationMode, BarPosition, Color},
|
||||
+ theme::{AnimationMode, BarPosition, Color, SpawnAnimation},
|
||||
video::{BlendSpace, ColorSpace, Eotf, Format, GfxApi, TearingMode, Transform, VrrMode},
|
||||
window::{ContentType, TileState, WindowType},
|
||||
workspace::WorkspaceDisplayOrder,
|
||||
@@ -213,6 +213,7 @@ pub struct Theme {
|
||||
pub gap: Option<i32>,
|
||||
pub animation_duration: Option<i32>,
|
||||
pub animation_mode: Option<AnimationMode>,
|
||||
+ pub spawn_animation: Option<SpawnAnimation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs
|
||||
index ae9b7ccd..bcfa7cf4 100644
|
||||
--- a/toml-config/src/config/parsers/theme.rs
|
||||
+++ b/toml-config/src/config/parsers/theme.rs
|
||||
@@ -13,7 +13,7 @@ use {
|
||||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
- jay_config::theme::{AnimationMode, BarPosition},
|
||||
+ jay_config::theme::{AnimationMode, BarPosition, SpawnAnimation},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Parser for ThemeParser<'_> {
|
||||
font,
|
||||
title_font,
|
||||
),
|
||||
- (bar_font, bar_position_val, bar_separator_width, gap, animation_duration, animation_mode_val),
|
||||
+ (bar_font, bar_position_val, bar_separator_width, gap, animation_duration, animation_mode_val, spawn_animation_val),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("attention-requested-bg-color")),
|
||||
@@ -96,6 +96,7 @@ impl Parser for ThemeParser<'_> {
|
||||
recover(opt(s32("gap"))),
|
||||
recover(opt(s32("animation-duration"))),
|
||||
recover(opt(str("animation-mode"))),
|
||||
+ recover(opt(str("spawn-animation"))),
|
||||
),
|
||||
))?;
|
||||
macro_rules! color {
|
||||
@@ -138,6 +139,23 @@ impl Parser for ThemeParser<'_> {
|
||||
None
|
||||
}
|
||||
});
|
||||
+ let spawn_animation =
|
||||
+ spawn_animation_val.and_then(|value| match value.value.to_lowercase().as_str() {
|
||||
+ "none" => Some(SpawnAnimation::None),
|
||||
+ "slide-left" => Some(SpawnAnimation::SlideLeft),
|
||||
+ "slide-right" => Some(SpawnAnimation::SlideRight),
|
||||
+ "slide-top" => Some(SpawnAnimation::SlideTop),
|
||||
+ "slide-bottom" => Some(SpawnAnimation::SlideBottom),
|
||||
+ "grow" => Some(SpawnAnimation::Grow),
|
||||
+ _ => {
|
||||
+ log::warn!(
|
||||
+ "Unknown spawn animation '{}': {}",
|
||||
+ value.value,
|
||||
+ self.0.error3(value.span)
|
||||
+ );
|
||||
+ None
|
||||
+ }
|
||||
+ });
|
||||
Ok(Theme {
|
||||
attention_requested_bg_color: color!(attention_requested_bg_color),
|
||||
bg_color: color!(bg_color),
|
||||
@@ -165,6 +183,7 @@ impl Parser for ThemeParser<'_> {
|
||||
gap: gap.despan(),
|
||||
animation_duration: animation_duration.despan(),
|
||||
animation_mode,
|
||||
+ spawn_animation,
|
||||
})
|
||||
}
|
||||
}
|
||||
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
|
||||
index 34c69903..177e6581 100644
|
||||
--- a/toml-config/src/lib.rs
|
||||
+++ b/toml-config/src/lib.rs
|
||||
@@ -45,7 +45,7 @@ use {
|
||||
tasks::{self, JoinHandle},
|
||||
theme::{
|
||||
reset_colors, reset_font, reset_sizes, set_animation_mode, set_bar_font,
|
||||
- set_bar_position, set_font, set_title_font,
|
||||
+ set_bar_position, set_font, set_spawn_animation, set_title_font,
|
||||
},
|
||||
toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles,
|
||||
video::{
|
||||
@@ -1623,6 +1623,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||
if let Some(v) = config.theme.animation_mode {
|
||||
set_animation_mode(v);
|
||||
}
|
||||
+ if let Some(v) = config.theme.spawn_animation {
|
||||
+ set_spawn_animation(v);
|
||||
+ }
|
||||
if let Some(v) = config.focus_history {
|
||||
if let Some(v) = v.only_visible {
|
||||
persistent.seat.focus_history_set_only_visible(v);
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
From 0197b7939940df165d0123e49a274572dbedf827 Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Sat, 7 Mar 2026 19:03:12 +1100
|
||||
Subject: [PATCH 08/10] add directional focus navigation for floating windows
|
||||
|
||||
Map left/down to previous and right/up to next in the workspace
|
||||
stacking order, with wrapping. Does not cross to the tiled layer.
|
||||
---
|
||||
src/ifs/wl_seat.rs | 32 ++++++++++++++++++++++++++++++++
|
||||
src/renderer.rs | 6 +++++-
|
||||
src/tree/float.rs | 3 +++
|
||||
3 files changed, 40 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs
|
||||
index 31084948..4bb5542f 100644
|
||||
--- a/src/ifs/wl_seat.rs
|
||||
+++ b/src/ifs/wl_seat.rs
|
||||
@@ -796,6 +796,38 @@ impl WlSeatGlobal {
|
||||
&& let Some(c) = p.node_into_container()
|
||||
{
|
||||
c.move_focus_from_child(self, tl.deref(), direction);
|
||||
+ } else if let Some(float) = data.float.get()
|
||||
+ {
|
||||
+ let ws = float.workspace.get();
|
||||
+ let floats: Vec<_> = ws
|
||||
+ .stacked
|
||||
+ .iter()
|
||||
+ .filter_map(|node| (*node).clone().node_into_float())
|
||||
+ .filter(|f| f.child.get().is_some())
|
||||
+ .collect();
|
||||
+ if let Some(pos) = floats.iter().position(|f| f.id == float.id) {
|
||||
+ let target = match direction {
|
||||
+ Direction::Left | Direction::Down => {
|
||||
+ if pos == 0 {
|
||||
+ floats.last()
|
||||
+ } else {
|
||||
+ floats.get(pos - 1)
|
||||
+ }
|
||||
+ }
|
||||
+ _ => {
|
||||
+ if pos + 1 >= floats.len() {
|
||||
+ floats.first()
|
||||
+ } else {
|
||||
+ floats.get(pos + 1)
|
||||
+ }
|
||||
+ }
|
||||
+ };
|
||||
+ if let Some(f) = target
|
||||
+ && f.id != float.id
|
||||
+ {
|
||||
+ f.clone().node_do_focus(self, Direction::Unspecified);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
self.warp_cursor_to_focus();
|
||||
diff --git a/src/renderer.rs b/src/renderer.rs
|
||||
index 93178d77..1d59f364 100644
|
||||
--- a/src/renderer.rs
|
||||
+++ b/src/renderer.rs
|
||||
@@ -824,7 +824,11 @@ impl Renderer<'_> {
|
||||
let tpuh = theme.title_plus_underline_height();
|
||||
let tuh = theme.title_underline_height();
|
||||
let bw = theme.sizes.border_width.get();
|
||||
- let bc = theme.colors.border.get();
|
||||
+ let bc = if floating.active.get() {
|
||||
+ theme.colors.focused_title_background.get()
|
||||
+ } else {
|
||||
+ theme.colors.border.get()
|
||||
+ };
|
||||
let tc = if floating.active.get() {
|
||||
theme.colors.focused_title_background.get()
|
||||
} else if floating.attention_requested.get() {
|
||||
diff --git a/src/tree/float.rs b/src/tree/float.rs
|
||||
index 7d610898..858bdcc9 100644
|
||||
--- a/src/tree/float.rs
|
||||
+++ b/src/tree/float.rs
|
||||
@@ -486,6 +486,9 @@ impl FloatNode {
|
||||
fn update_child_active(self: &Rc<Self>, active: bool) {
|
||||
if self.active.replace(active) != active {
|
||||
self.schedule_render_titles();
|
||||
+ if active {
|
||||
+ self.restack();
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
From f3aa1b1577a71792bc566989aad7b4319516495e Mon Sep 17 00:00:00 2001
|
||||
From: atagen <boss@atagen.co>
|
||||
Date: Mon, 9 Mar 2026 01:04:12 +1100
|
||||
Subject: [PATCH 09/10] add configurable animation easing curves
|
||||
|
||||
Adds an animation-curve setting that controls the easing function used
|
||||
for all window animations. Supports presets (linear, ease-in, ease-out,
|
||||
ease-in-out) and custom cubic-bezier curves with 4 control points.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||||
---
|
||||
jay-config/src/_private/client.rs | 13 +++-
|
||||
jay-config/src/_private/ipc.rs | 10 +++-
|
||||
jay-config/src/theme.rs | 32 ++++++++++
|
||||
src/animation.rs | 80 +++++++++++++++++++++----
|
||||
src/config/handler.rs | 15 ++++-
|
||||
src/theme.rs | 45 +++++++++++++-
|
||||
src/tree/container.rs | 4 ++
|
||||
src/tree/toplevel.rs | 1 +
|
||||
toml-config/src/config.rs | 3 +-
|
||||
toml-config/src/config/parsers/theme.rs | 59 +++++++++++++++++-
|
||||
toml-config/src/lib.rs | 7 ++-
|
||||
11 files changed, 248 insertions(+), 21 deletions(-)
|
||||
|
||||
diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs
|
||||
index 2a1ee047..a260813a 100644
|
||||
--- a/jay-config/src/_private/client.rs
|
||||
+++ b/jay-config/src/_private/client.rs
|
||||
@@ -27,7 +27,8 @@ use {
|
||||
logging::LogLevel,
|
||||
tasks::{JoinHandle, JoinSlot},
|
||||
theme::{
|
||||
- AnimationMode, BarPosition, Color, SpawnAnimation, colors::Colorable, sized::Resizable,
|
||||
+ AnimationCurve, AnimationMode, BarPosition, Color, SpawnAnimation,
|
||||
+ colors::Colorable, sized::Resizable,
|
||||
},
|
||||
timer::Timer,
|
||||
video::{
|
||||
@@ -1065,6 +1066,16 @@ impl ConfigClient {
|
||||
animation
|
||||
}
|
||||
|
||||
+ pub fn set_animation_curve(&self, curve: AnimationCurve) {
|
||||
+ self.send(&ClientMessage::SetAnimationCurve { curve });
|
||||
+ }
|
||||
+
|
||||
+ pub fn get_animation_curve(&self) -> AnimationCurve {
|
||||
+ let res = self.send_with_response(&ClientMessage::GetAnimationCurve);
|
||||
+ get_response!(res, AnimationCurve::EaseOut, GetAnimationCurve { curve });
|
||||
+ curve
|
||||
+ }
|
||||
+
|
||||
pub fn set_middle_click_paste_enabled(&self, enabled: bool) {
|
||||
self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled });
|
||||
}
|
||||
diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs
|
||||
index 9ed55666..282f37cf 100644
|
||||
--- a/jay-config/src/_private/ipc.rs
|
||||
+++ b/jay-config/src/_private/ipc.rs
|
||||
@@ -11,7 +11,8 @@ use {
|
||||
keyboard::{Group, Keymap, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
theme::{
|
||||
- AnimationMode, BarPosition, Color, SpawnAnimation, colors::Colorable, sized::Resizable,
|
||||
+ AnimationCurve, AnimationMode, BarPosition, Color, SpawnAnimation, colors::Colorable,
|
||||
+ sized::Resizable,
|
||||
},
|
||||
timer::Timer,
|
||||
video::{
|
||||
@@ -840,6 +841,10 @@ pub enum ClientMessage<'a> {
|
||||
animation: SpawnAnimation,
|
||||
},
|
||||
GetSpawnAnimation,
|
||||
+ SetAnimationCurve {
|
||||
+ curve: AnimationCurve,
|
||||
+ },
|
||||
+ GetAnimationCurve,
|
||||
ConnectorSetUseNativeGamut {
|
||||
connector: Connector,
|
||||
use_native_gamut: bool,
|
||||
@@ -1117,6 +1122,9 @@ pub enum Response {
|
||||
GetSpawnAnimation {
|
||||
animation: SpawnAnimation,
|
||||
},
|
||||
+ GetAnimationCurve {
|
||||
+ curve: AnimationCurve,
|
||||
+ },
|
||||
GetCursorFollowsFocus {
|
||||
enabled: bool,
|
||||
},
|
||||
diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs
|
||||
index 18cb2ff9..b1db8767 100644
|
||||
--- a/jay-config/src/theme.rs
|
||||
+++ b/jay-config/src/theme.rs
|
||||
@@ -252,6 +252,38 @@ pub fn get_spawn_animation() -> SpawnAnimation {
|
||||
get!(SpawnAnimation::None).get_spawn_animation()
|
||||
}
|
||||
|
||||
+/// The easing curve used for animations.
|
||||
+#[non_exhaustive]
|
||||
+#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Default)]
|
||||
+pub enum AnimationCurve {
|
||||
+ /// Linear interpolation (no easing).
|
||||
+ Linear,
|
||||
+ /// Cubic ease-in (slow start, fast end).
|
||||
+ EaseIn,
|
||||
+ /// Cubic ease-out (fast start, slow end).
|
||||
+ #[default]
|
||||
+ EaseOut,
|
||||
+ /// Cubic ease-in-out (slow start and end).
|
||||
+ EaseInOut,
|
||||
+ /// Custom cubic bezier curve with control points (x1, y1, x2, y2).
|
||||
+ ///
|
||||
+ /// The curve goes from (0, 0) to (1, 1). The two control points define
|
||||
+ /// the shape of the curve, similar to the CSS `cubic-bezier()` function.
|
||||
+ CubicBezier(f64, f64, f64, f64),
|
||||
+}
|
||||
+
|
||||
+/// Sets the easing curve used for animations.
|
||||
+///
|
||||
+/// Default: `EaseOut`.
|
||||
+pub fn set_animation_curve(curve: AnimationCurve) {
|
||||
+ get!().set_animation_curve(curve);
|
||||
+}
|
||||
+
|
||||
+/// Gets the current animation curve.
|
||||
+pub fn get_animation_curve() -> AnimationCurve {
|
||||
+ get!(AnimationCurve::EaseOut).get_animation_curve()
|
||||
+}
|
||||
+
|
||||
/// Elements of the compositor whose color can be changed.
|
||||
pub mod colors {
|
||||
use {
|
||||
diff --git a/src/animation.rs b/src/animation.rs
|
||||
index ed1d1f8d..09ca0f12 100644
|
||||
--- a/src/animation.rs
|
||||
+++ b/src/animation.rs
|
||||
@@ -1,3 +1,5 @@
|
||||
+use crate::theme::AnimationCurve;
|
||||
+
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub enum AnimationMode {
|
||||
#[default]
|
||||
@@ -17,34 +19,88 @@ pub struct PositionAnimation {
|
||||
pub mode_x: AnimationMode,
|
||||
pub mode_y: AnimationMode,
|
||||
pub reverse: bool,
|
||||
+ pub curve: AnimationCurve,
|
||||
+}
|
||||
+
|
||||
+fn apply_curve(t: f64, curve: AnimationCurve) -> f64 {
|
||||
+ match curve {
|
||||
+ AnimationCurve::Linear => t,
|
||||
+ AnimationCurve::EaseIn => t * t * t,
|
||||
+ AnimationCurve::EaseOut => {
|
||||
+ let t = 1.0 - t;
|
||||
+ 1.0 - t * t * t
|
||||
+ }
|
||||
+ AnimationCurve::EaseInOut => {
|
||||
+ if t < 0.5 {
|
||||
+ 4.0 * t * t * t
|
||||
+ } else {
|
||||
+ let t = 1.0 - t;
|
||||
+ 1.0 - 4.0 * t * t * t
|
||||
+ }
|
||||
+ }
|
||||
+ AnimationCurve::CubicBezier(x1, y1, x2, y2) => cubic_bezier(t, x1, y1, x2, y2),
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/// Evaluates a cubic bezier easing curve at time `t`.
|
||||
+///
|
||||
+/// The curve is defined by two control points (x1, y1) and (x2, y2),
|
||||
+/// with implicit start (0, 0) and end (1, 1).
|
||||
+fn cubic_bezier(t: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
|
||||
+ // Find the parameter u such that bezier_x(u) = t using Newton's method.
|
||||
+ let mut u = t;
|
||||
+ for _ in 0..8 {
|
||||
+ let x = bezier_component(u, x1, x2) - t;
|
||||
+ if x.abs() < 1e-7 {
|
||||
+ break;
|
||||
+ }
|
||||
+ let dx = bezier_derivative(u, x1, x2);
|
||||
+ if dx.abs() < 1e-7 {
|
||||
+ break;
|
||||
+ }
|
||||
+ u -= x / dx;
|
||||
+ }
|
||||
+ u = u.clamp(0.0, 1.0);
|
||||
+ bezier_component(u, y1, y2)
|
||||
+}
|
||||
+
|
||||
+/// Evaluates one component of a cubic bezier at parameter u.
|
||||
+/// The bezier goes from 0 to 1 with control points c1 and c2.
|
||||
+fn bezier_component(u: f64, c1: f64, c2: f64) -> f64 {
|
||||
+ let u2 = u * u;
|
||||
+ let u3 = u2 * u;
|
||||
+ let inv = 1.0 - u;
|
||||
+ let inv2 = inv * inv;
|
||||
+ 3.0 * inv2 * u * c1 + 3.0 * inv * u2 * c2 + u3
|
||||
}
|
||||
|
||||
-fn ease_out_cubic(t: f64) -> f64 {
|
||||
- let t = 1.0 - t;
|
||||
- 1.0 - t * t * t
|
||||
+/// Derivative of one component of a cubic bezier.
|
||||
+fn bezier_derivative(u: f64, c1: f64, c2: f64) -> f64 {
|
||||
+ let inv = 1.0 - u;
|
||||
+ 3.0 * inv * inv * c1 + 6.0 * inv * u * (c2 - c1) + 3.0 * u * u * (1.0 - c2)
|
||||
}
|
||||
|
||||
-fn sequential_factors(t: f64, mode: AnimationMode) -> (f64, f64) {
|
||||
+fn sequential_factors(t: f64, mode: AnimationMode, curve: AnimationCurve) -> (f64, f64) {
|
||||
match mode {
|
||||
AnimationMode::Concurrent => {
|
||||
- let r = 1.0 - ease_out_cubic(t);
|
||||
+ let r = 1.0 - apply_curve(t, curve);
|
||||
(r, r)
|
||||
}
|
||||
AnimationMode::MoveFirst => {
|
||||
if t < 0.5 {
|
||||
- let r = 1.0 - ease_out_cubic(t * 2.0);
|
||||
+ let r = 1.0 - apply_curve(t * 2.0, curve);
|
||||
(r, 1.0)
|
||||
} else {
|
||||
- let r = 1.0 - ease_out_cubic((t - 0.5) * 2.0);
|
||||
+ let r = 1.0 - apply_curve((t - 0.5) * 2.0, curve);
|
||||
(0.0, r)
|
||||
}
|
||||
}
|
||||
AnimationMode::ResizeFirst => {
|
||||
if t < 0.5 {
|
||||
- let r = 1.0 - ease_out_cubic(t * 2.0);
|
||||
+ let r = 1.0 - apply_curve(t * 2.0, curve);
|
||||
(1.0, r)
|
||||
} else {
|
||||
- let r = 1.0 - ease_out_cubic((t - 0.5) * 2.0);
|
||||
+ let r = 1.0 - apply_curve((t - 0.5) * 2.0, curve);
|
||||
(r, 0.0)
|
||||
}
|
||||
}
|
||||
@@ -62,8 +118,10 @@ impl PositionAnimation {
|
||||
return None;
|
||||
}
|
||||
let t = elapsed as f64 / self.duration_usec as f64;
|
||||
- let (move_remaining_x, size_remaining_x) = sequential_factors(t, self.mode_x);
|
||||
- let (move_remaining_y, size_remaining_y) = sequential_factors(t, self.mode_y);
|
||||
+ let (move_remaining_x, size_remaining_x) =
|
||||
+ sequential_factors(t, self.mode_x, self.curve);
|
||||
+ let (move_remaining_y, size_remaining_y) =
|
||||
+ sequential_factors(t, self.mode_y, self.curve);
|
||||
let (move_factor_x, size_factor_x, move_factor_y, size_factor_y) = if self.reverse {
|
||||
(
|
||||
1.0 - move_remaining_x,
|
||||
diff --git a/src/config/handler.rs b/src/config/handler.rs
|
||||
index 5a919daa..15decfb8 100644
|
||||
--- a/src/config/handler.rs
|
||||
+++ b/src/config/handler.rs
|
||||
@@ -68,7 +68,8 @@ use {
|
||||
keyboard::{Group, Keymap, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
theme::{
|
||||
- AnimationMode, BarPosition, SpawnAnimation, colors::Colorable, sized::Resizable,
|
||||
+ AnimationCurve, AnimationMode, BarPosition, SpawnAnimation, colors::Colorable,
|
||||
+ sized::Resizable,
|
||||
},
|
||||
timer::Timer as JayTimer,
|
||||
video::{
|
||||
@@ -1504,6 +1505,16 @@ impl ConfigProxyHandler {
|
||||
});
|
||||
}
|
||||
|
||||
+ fn handle_set_animation_curve(&self, curve: AnimationCurve) {
|
||||
+ self.state.theme.animation_curve.set(curve.into());
|
||||
+ }
|
||||
+
|
||||
+ fn handle_get_animation_curve(&self) {
|
||||
+ self.respond(Response::GetAnimationCurve {
|
||||
+ curve: self.state.theme.animation_curve.get().into(),
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
fn handle_set_show_float_pin_icon(&self, show: bool) {
|
||||
self.state.show_pin_icon.set(show);
|
||||
for stacked in self.state.root.stacked.iter() {
|
||||
@@ -3394,6 +3405,8 @@ impl ConfigProxyHandler {
|
||||
.handle_set_spawn_animation(animation)
|
||||
.wrn("set_spawn_animation")?,
|
||||
ClientMessage::GetSpawnAnimation => self.handle_get_spawn_animation(),
|
||||
+ ClientMessage::SetAnimationCurve { curve } => self.handle_set_animation_curve(curve),
|
||||
+ ClientMessage::GetAnimationCurve => self.handle_get_animation_curve(),
|
||||
ClientMessage::SeatFocusHistory { seat, timeline } => self
|
||||
.handle_seat_focus_history(seat, timeline)
|
||||
.wrn("seat_focus_history")?,
|
||||
diff --git a/src/theme.rs b/src/theme.rs
|
||||
index f705c4fd..51d490ac 100644
|
||||
--- a/src/theme.rs
|
||||
+++ b/src/theme.rs
|
||||
@@ -7,8 +7,8 @@ use {
|
||||
utils::clonecell::CloneCell,
|
||||
},
|
||||
jay_config::theme::{
|
||||
- AnimationMode as ConfigAnimationMode, BarPosition as ConfigBarPosition,
|
||||
- SpawnAnimation as ConfigSpawnAnimation,
|
||||
+ AnimationCurve as ConfigAnimationCurve, AnimationMode as ConfigAnimationMode,
|
||||
+ BarPosition as ConfigBarPosition, SpawnAnimation as ConfigSpawnAnimation,
|
||||
},
|
||||
linearize::Linearize,
|
||||
num_traits::Float,
|
||||
@@ -620,6 +620,45 @@ impl Into<ConfigSpawnAnimation> for SpawnAnimation {
|
||||
}
|
||||
}
|
||||
|
||||
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
+pub enum AnimationCurve {
|
||||
+ Linear,
|
||||
+ EaseIn,
|
||||
+ #[default]
|
||||
+ EaseOut,
|
||||
+ EaseInOut,
|
||||
+ CubicBezier(f64, f64, f64, f64),
|
||||
+}
|
||||
+
|
||||
+impl From<ConfigAnimationCurve> for AnimationCurve {
|
||||
+ fn from(value: ConfigAnimationCurve) -> Self {
|
||||
+ match value {
|
||||
+ ConfigAnimationCurve::Linear => Self::Linear,
|
||||
+ ConfigAnimationCurve::EaseIn => Self::EaseIn,
|
||||
+ ConfigAnimationCurve::EaseOut => Self::EaseOut,
|
||||
+ ConfigAnimationCurve::EaseInOut => Self::EaseInOut,
|
||||
+ ConfigAnimationCurve::CubicBezier(x1, y1, x2, y2) => {
|
||||
+ Self::CubicBezier(x1, y1, x2, y2)
|
||||
+ }
|
||||
+ _ => Self::EaseOut,
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+impl Into<ConfigAnimationCurve> for AnimationCurve {
|
||||
+ fn into(self) -> ConfigAnimationCurve {
|
||||
+ match self {
|
||||
+ AnimationCurve::Linear => ConfigAnimationCurve::Linear,
|
||||
+ AnimationCurve::EaseIn => ConfigAnimationCurve::EaseIn,
|
||||
+ AnimationCurve::EaseOut => ConfigAnimationCurve::EaseOut,
|
||||
+ AnimationCurve::EaseInOut => ConfigAnimationCurve::EaseInOut,
|
||||
+ AnimationCurve::CubicBezier(x1, y1, x2, y2) => {
|
||||
+ ConfigAnimationCurve::CubicBezier(x1, y1, x2, y2)
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
pub struct Theme {
|
||||
pub colors: ThemeColors,
|
||||
pub sizes: ThemeSizes,
|
||||
@@ -631,6 +670,7 @@ pub struct Theme {
|
||||
pub bar_position: Cell<BarPosition>,
|
||||
pub animation_mode: Cell<AnimationMode>,
|
||||
pub spawn_animation: Cell<SpawnAnimation>,
|
||||
+ pub animation_curve: Cell<AnimationCurve>,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
@@ -647,6 +687,7 @@ impl Default for Theme {
|
||||
bar_position: Default::default(),
|
||||
animation_mode: Default::default(),
|
||||
spawn_animation: Default::default(),
|
||||
+ animation_curve: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
diff --git a/src/tree/container.rs b/src/tree/container.rs
|
||||
index 2d8d328d..a1595ea6 100644
|
||||
--- a/src/tree/container.rs
|
||||
+++ b/src/tree/container.rs
|
||||
@@ -497,6 +497,7 @@ impl ContainerNode {
|
||||
mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
reverse: false,
|
||||
+ curve: self.state.theme.animation_curve.get(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -584,6 +585,7 @@ impl ContainerNode {
|
||||
mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
reverse: false,
|
||||
+ curve: self.state.theme.animation_curve.get(),
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
@@ -656,6 +658,7 @@ impl ContainerNode {
|
||||
mode_x,
|
||||
mode_y,
|
||||
reverse: false,
|
||||
+ curve: self.state.theme.animation_curve.get(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -2308,6 +2311,7 @@ impl ContainingNode for ContainerNode {
|
||||
mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
reverse: true,
|
||||
+ curve: self.state.theme.animation_curve.get(),
|
||||
},
|
||||
border_color_is_focused: child_focused,
|
||||
});
|
||||
diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs
|
||||
index 2e48b2fd..f8ab558c 100644
|
||||
--- a/src/tree/toplevel.rs
|
||||
+++ b/src/tree/toplevel.rs
|
||||
@@ -1053,6 +1053,7 @@ pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floati
|
||||
mode_x: crate::animation::AnimationMode::Concurrent,
|
||||
mode_y: crate::animation::AnimationMode::Concurrent,
|
||||
reverse: false,
|
||||
+ curve: state.theme.animation_curve.get(),
|
||||
}));
|
||||
state.damage(tiled_pos.union(target));
|
||||
}
|
||||
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
|
||||
index 39b9fab4..b4626e62 100644
|
||||
--- a/toml-config/src/config.rs
|
||||
+++ b/toml-config/src/config.rs
|
||||
@@ -32,7 +32,7 @@ use {
|
||||
keyboard::{Keymap, ModifiedKeySym, mods::Modifiers, syms::KeySym},
|
||||
logging::LogLevel,
|
||||
status::MessageFormat,
|
||||
- theme::{AnimationMode, BarPosition, Color, SpawnAnimation},
|
||||
+ theme::{AnimationCurve, AnimationMode, BarPosition, Color, SpawnAnimation},
|
||||
video::{BlendSpace, ColorSpace, Eotf, Format, GfxApi, TearingMode, Transform, VrrMode},
|
||||
window::{ContentType, TileState, WindowType},
|
||||
workspace::WorkspaceDisplayOrder,
|
||||
@@ -214,6 +214,7 @@ pub struct Theme {
|
||||
pub animation_duration: Option<i32>,
|
||||
pub animation_mode: Option<AnimationMode>,
|
||||
pub spawn_animation: Option<SpawnAnimation>,
|
||||
+ pub animation_curve: Option<AnimationCurve>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs
|
||||
index bcfa7cf4..c1d92d23 100644
|
||||
--- a/toml-config/src/config/parsers/theme.rs
|
||||
+++ b/toml-config/src/config/parsers/theme.rs
|
||||
@@ -13,7 +13,7 @@ use {
|
||||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
- jay_config::theme::{AnimationMode, BarPosition, SpawnAnimation},
|
||||
+ jay_config::theme::{AnimationCurve, AnimationMode, BarPosition, SpawnAnimation},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Parser for ThemeParser<'_> {
|
||||
font,
|
||||
title_font,
|
||||
),
|
||||
- (bar_font, bar_position_val, bar_separator_width, gap, animation_duration, animation_mode_val, spawn_animation_val),
|
||||
+ (bar_font, bar_position_val, bar_separator_width, gap, animation_duration, animation_mode_val, spawn_animation_val, animation_curve_val),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("attention-requested-bg-color")),
|
||||
@@ -97,6 +97,7 @@ impl Parser for ThemeParser<'_> {
|
||||
recover(opt(s32("animation-duration"))),
|
||||
recover(opt(str("animation-mode"))),
|
||||
recover(opt(str("spawn-animation"))),
|
||||
+ opt(val("animation-curve")),
|
||||
),
|
||||
))?;
|
||||
macro_rules! color {
|
||||
@@ -156,6 +157,7 @@ impl Parser for ThemeParser<'_> {
|
||||
None
|
||||
}
|
||||
});
|
||||
+ let animation_curve = animation_curve_val.and_then(|v| parse_animation_curve(self.0, &v));
|
||||
Ok(Theme {
|
||||
attention_requested_bg_color: color!(attention_requested_bg_color),
|
||||
bg_color: color!(bg_color),
|
||||
@@ -184,6 +186,59 @@ impl Parser for ThemeParser<'_> {
|
||||
animation_duration: animation_duration.despan(),
|
||||
animation_mode,
|
||||
spawn_animation,
|
||||
+ animation_curve,
|
||||
})
|
||||
}
|
||||
}
|
||||
+
|
||||
+fn parse_animation_curve(ctx: &Context<'_>, v: &Spanned<&Value>) -> Option<AnimationCurve> {
|
||||
+ match v.value {
|
||||
+ Value::String(s) => match s.to_lowercase().as_str() {
|
||||
+ "linear" => Some(AnimationCurve::Linear),
|
||||
+ "ease-in" => Some(AnimationCurve::EaseIn),
|
||||
+ "ease-out" => Some(AnimationCurve::EaseOut),
|
||||
+ "ease-in-out" => Some(AnimationCurve::EaseInOut),
|
||||
+ _ => {
|
||||
+ log::warn!(
|
||||
+ "Unknown animation curve '{}': {}",
|
||||
+ s,
|
||||
+ ctx.error3(v.span)
|
||||
+ );
|
||||
+ None
|
||||
+ }
|
||||
+ },
|
||||
+ Value::Array(arr) => {
|
||||
+ if arr.len() != 4 {
|
||||
+ log::warn!(
|
||||
+ "Animation curve bezier array must have exactly 4 elements: {}",
|
||||
+ ctx.error3(v.span)
|
||||
+ );
|
||||
+ return None;
|
||||
+ }
|
||||
+ let mut points = [0.0f64; 4];
|
||||
+ for (i, elem) in arr.iter().enumerate() {
|
||||
+ match &elem.value {
|
||||
+ Value::Float(f) => points[i] = *f,
|
||||
+ Value::Integer(n) => points[i] = *n as f64,
|
||||
+ _ => {
|
||||
+ log::warn!(
|
||||
+ "Animation curve bezier element must be a number: {}",
|
||||
+ ctx.error3(elem.span)
|
||||
+ );
|
||||
+ return None;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ Some(AnimationCurve::CubicBezier(
|
||||
+ points[0], points[1], points[2], points[3],
|
||||
+ ))
|
||||
+ }
|
||||
+ _ => {
|
||||
+ log::warn!(
|
||||
+ "Animation curve must be a string or array of 4 numbers: {}",
|
||||
+ ctx.error3(v.span)
|
||||
+ );
|
||||
+ None
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
|
||||
index 177e6581..1fe98ba9 100644
|
||||
--- a/toml-config/src/lib.rs
|
||||
+++ b/toml-config/src/lib.rs
|
||||
@@ -44,8 +44,8 @@ use {
|
||||
switch_to_vt,
|
||||
tasks::{self, JoinHandle},
|
||||
theme::{
|
||||
- reset_colors, reset_font, reset_sizes, set_animation_mode, set_bar_font,
|
||||
- set_bar_position, set_font, set_spawn_animation, set_title_font,
|
||||
+ reset_colors, reset_font, reset_sizes, set_animation_curve, set_animation_mode,
|
||||
+ set_bar_font, set_bar_position, set_font, set_spawn_animation, set_title_font,
|
||||
},
|
||||
toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles,
|
||||
video::{
|
||||
@@ -1626,6 +1626,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||
if let Some(v) = config.theme.spawn_animation {
|
||||
set_spawn_animation(v);
|
||||
}
|
||||
+ if let Some(v) = config.theme.animation_curve {
|
||||
+ set_animation_curve(v);
|
||||
+ }
|
||||
if let Some(v) = config.focus_history {
|
||||
if let Some(v) = v.only_visible {
|
||||
persistent.seat.focus_history_set_only_visible(v);
|
||||
--
|
||||
2.53.0
|
||||
|
||||
File diff suppressed because it is too large
Load diff
217
graphical/desktop/patches/FEATURES.md
Normal file
217
graphical/desktop/patches/FEATURES.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# Patch Series Feature Documentation
|
||||
|
||||
This document describes each patch in the series, its user-facing impact, and
|
||||
all configuration options.
|
||||
|
||||
---
|
||||
|
||||
## 01. Configurable Gap Between Tiled Windows
|
||||
|
||||
Adds a configurable pixel gap between tiled windows. When set to a value
|
||||
greater than 0, windows are visually separated by empty space instead of
|
||||
touching at their borders.
|
||||
|
||||
**Config option:**
|
||||
|
||||
| Key | Type | Default | Range |
|
||||
|-----|------|---------|-------|
|
||||
| `theme.gap` | integer | `0` | 0–1000 |
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
gap = 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 02. Position and Size Animations for Tiled Windows
|
||||
|
||||
Adds smooth animations when tiled windows change position or size (e.g. when
|
||||
a window is opened, closed, or resized). Window transitions interpolate from
|
||||
old to new geometry over a configurable duration.
|
||||
|
||||
**Config option:**
|
||||
|
||||
| Key | Type | Default | Range |
|
||||
|-----|------|---------|-------|
|
||||
| `theme.animation-duration` | integer (ms) | `0` | 0–5000 |
|
||||
|
||||
Set to `0` to disable animations.
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
animation-duration = 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 03. Window Border Frames When Gaps Are Enabled
|
||||
|
||||
When gaps are enabled (`gap > 0`), each tiled window gets an individual border
|
||||
frame drawn around it, rather than sharing borders with adjacent windows. This
|
||||
gives each window a distinct visual boundary within the gap space.
|
||||
|
||||
**Config option:**
|
||||
|
||||
Uses the existing `theme.border-width` setting. Borders are drawn per-window
|
||||
when `gap > 0`.
|
||||
|
||||
| Key | Type | Default |
|
||||
|-----|------|---------|
|
||||
| `theme.border-width` | integer | `4` |
|
||||
|
||||
---
|
||||
|
||||
## 04. Sequential Animation Mode for Tiled Windows
|
||||
|
||||
Adds a sequential animation mode where position and size changes animate in
|
||||
two phases rather than simultaneously. When a window shrinks, it resizes first
|
||||
then moves; when it grows, it moves first then resizes. This prevents windows
|
||||
from overlapping during animations.
|
||||
|
||||
**Config option:**
|
||||
|
||||
| Key | Type | Default | Values |
|
||||
|-----|------|---------|--------|
|
||||
| `theme.animation-mode` | string | `"concurrent"` | `"concurrent"`, `"sequential"` |
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
animation-mode = "sequential"
|
||||
animation-duration = 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 05. Cursor Follows Focus
|
||||
|
||||
When enabled, the mouse cursor warps to the center of the focused window on
|
||||
keyboard-driven focus changes. This is useful with multi-monitor setups or
|
||||
large screens where the cursor may be far from the focused window.
|
||||
|
||||
**Config option:**
|
||||
|
||||
| Key | Type | Default |
|
||||
|-----|------|---------|
|
||||
| `cursor-follows-focus` | boolean | `false` |
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
cursor-follows-focus = true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 06. Toggle Focus Between Floating and Tiled Layers
|
||||
|
||||
Adds an action to toggle keyboard focus between the floating and tiled window
|
||||
layers. Useful for quickly switching attention between a floating terminal and
|
||||
tiled editor, for example.
|
||||
|
||||
**Config action:**
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| `toggle-float-focus` | Toggles focus between floating and tiled windows |
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
[shortcuts]
|
||||
alt-space = "toggle-float-focus"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 07. Open/Close Animations for Tiled Windows
|
||||
|
||||
Adds spawn and despawn animations for tiled windows. When a window is opened
|
||||
or closed, it can slide in from a screen edge or grow/shrink from center.
|
||||
|
||||
**Config option:**
|
||||
|
||||
| Key | Type | Default | Values |
|
||||
|-----|------|---------|--------|
|
||||
| `theme.spawn-animation` | string | `"none"` | `"none"`, `"slide-left"`, `"slide-right"`, `"slide-top"`, `"slide-bottom"`, `"grow"` |
|
||||
|
||||
Requires `animation-duration > 0` to be visible.
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
spawn-animation = "grow"
|
||||
animation-duration = 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 08. Directional Focus Navigation for Floating Windows
|
||||
|
||||
Extends the existing directional focus commands (`focus-left`, `focus-right`,
|
||||
`focus-up`, `focus-down`) to work with floating windows. When focus is on a
|
||||
floating window, left/down moves to the previous float in stacking order and
|
||||
right/up moves to the next. Navigation wraps around at the ends.
|
||||
|
||||
**No new config options.** Uses existing directional focus key bindings.
|
||||
|
||||
---
|
||||
|
||||
## 09. Configurable Animation Easing Curves
|
||||
|
||||
Adds configurable easing curves for all animations. Choose from preset curves
|
||||
or define a custom cubic bezier curve (same syntax as CSS `cubic-bezier()`).
|
||||
|
||||
**Config option:**
|
||||
|
||||
| Key | Type | Default | Values |
|
||||
|-----|------|---------|--------|
|
||||
| `theme.animation-curve` | string or array | `"ease-out"` | `"linear"`, `"ease-in"`, `"ease-out"`, `"ease-in-out"`, `[x1, y1, x2, y2]` |
|
||||
|
||||
For custom bezier curves, specify an array of 4 floats representing the two
|
||||
control points, matching CSS `cubic-bezier(x1, y1, x2, y2)`.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
animation-curve = "ease-in-out"
|
||||
```
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
# CSS "ease" equivalent
|
||||
animation-curve = [0.25, 0.1, 0.25, 1.0]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Rounded Corners for Window Frames
|
||||
|
||||
Adds rounded corners to window frames. All frame elements (borders, title bar,
|
||||
content) are clipped to a rounded rectangle using anti-aliased SDF clipping in
|
||||
the fragment shaders. Works for both tiled and floating windows. Best paired
|
||||
with gaps for clean visuals.
|
||||
|
||||
**Config option:**
|
||||
|
||||
| Key | Type | Default | Range |
|
||||
|-----|------|---------|-------|
|
||||
| `theme.corner-radius` | integer (px) | `0` | 0–100 |
|
||||
|
||||
Set to `0` for square corners.
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
gap = 10
|
||||
corner-radius = 8
|
||||
```
|
||||
|
|
@ -88,155 +88,161 @@ ShellRoot {
|
|||
}
|
||||
|
||||
// centre main
|
||||
Variants {
|
||||
model: Quickshell.screens.filter(screen => screen.name != "DP-1")
|
||||
delegate: PanelWindow {
|
||||
id: windowTitle
|
||||
property var modelData
|
||||
anchors {
|
||||
top: true
|
||||
}
|
||||
// Variants {
|
||||
// model: Quickshell.screens.filter(screen => screen.name != "DP-1")
|
||||
// delegate: PanelWindow {
|
||||
// id: windowTitle
|
||||
// property var modelData
|
||||
// anchors {
|
||||
// top: true
|
||||
// left: true
|
||||
// right: true
|
||||
// }
|
||||
// margins {
|
||||
// left: 300
|
||||
// right: 300
|
||||
// }
|
||||
|
||||
TextMetrics {
|
||||
id: textInfo
|
||||
font {
|
||||
family: "MS W98 UI"
|
||||
pointSize: 12
|
||||
}
|
||||
elideWidth: 550
|
||||
elide: Qt.ElideMiddle
|
||||
text: Title.currentWindow
|
||||
}
|
||||
// TextMetrics {
|
||||
// id: textInfo
|
||||
// font {
|
||||
// family: "MS W98 UI"
|
||||
// pointSize: 12
|
||||
// }
|
||||
// elideWidth: 550
|
||||
// elide: Qt.ElideMiddle
|
||||
// text: Title.currentWindow
|
||||
// }
|
||||
|
||||
implicitHeight: textInfo.height + 6
|
||||
implicitWidth: (textInfo.width > textInfo.elideWidth ? textInfo.elideWidth : textInfo.width) + 24
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
color: "transparent"
|
||||
// implicitHeight: textInfo.height + 6
|
||||
// implicitWidth: (textInfo.width > textInfo.elideWidth ? textInfo.elideWidth : textInfo.width) + 24
|
||||
// Behavior on implicitWidth {
|
||||
// NumberAnimation {
|
||||
// duration: 150
|
||||
// easing.type: Easing.InOutQuad
|
||||
// }
|
||||
// }
|
||||
// color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
color: Colours.c.black
|
||||
bottomLeftRadius: 10
|
||||
bottomRightRadius: 10
|
||||
// Rectangle {
|
||||
// anchors {
|
||||
// fill: parent
|
||||
// }
|
||||
// color: Colours.c.black
|
||||
// bottomLeftRadius: 10
|
||||
// bottomRightRadius: 10
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: 12
|
||||
rightMargin: 12
|
||||
topMargin: 4
|
||||
}
|
||||
color: "transparent"
|
||||
Text {
|
||||
font {
|
||||
family: "MS W98 UI"
|
||||
pointSize: 12
|
||||
}
|
||||
color: Colours.c.yellow_b
|
||||
text: textInfo.elidedText
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rectangle {
|
||||
// anchors {
|
||||
// fill: parent
|
||||
// leftMargin: 12
|
||||
// rightMargin: 12
|
||||
// topMargin: 4
|
||||
// }
|
||||
// color: "transparent"
|
||||
// Text {
|
||||
// font {
|
||||
// family: "MS W98 UI"
|
||||
// pointSize: 12
|
||||
// }
|
||||
// color: Colours.c.yellow_b
|
||||
// text: textInfo.elidedText
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
screen: modelData
|
||||
}
|
||||
}
|
||||
// exclusionMode: ExclusionMode.Ignore
|
||||
// WlrLayershell.layer: WlrLayer.Top
|
||||
// screen: modelData
|
||||
// }
|
||||
// }
|
||||
|
||||
// bottom middle main
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: PanelWindow {
|
||||
id: tags
|
||||
property var modelData
|
||||
anchors {
|
||||
bottom: true
|
||||
}
|
||||
// // bottom middle main
|
||||
// Variants {
|
||||
// model: Quickshell.screens
|
||||
// delegate: PanelWindow {
|
||||
// id: tags
|
||||
// property var modelData
|
||||
// anchors {
|
||||
// bottom: true
|
||||
// }
|
||||
|
||||
implicitHeight: 22
|
||||
implicitWidth: (Tags.keys.length !== undefined) ? Tags.keys.length * 13 + 24 : 37;
|
||||
color: "transparent"
|
||||
// implicitHeight: 22
|
||||
// implicitWidth: (Tags.keys.length !== undefined) ? Tags.keys.length * 13 + 24 : 37;
|
||||
// color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
color: Colours.c.black
|
||||
topLeftRadius: 10
|
||||
topRightRadius: 10
|
||||
// Rectangle {
|
||||
// anchors {
|
||||
// fill: parent
|
||||
// }
|
||||
// color: Colours.c.black
|
||||
// topLeftRadius: 10
|
||||
// topRightRadius: 10
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: 12
|
||||
rightMargin: 12
|
||||
topMargin: 3
|
||||
}
|
||||
Row {
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
spacing: 1
|
||||
Repeater {
|
||||
model: Tags.keys
|
||||
delegate: Column {
|
||||
id: baseCol
|
||||
required property var modelData
|
||||
spacing: 3
|
||||
Rectangle {
|
||||
property var tag: Tags.tags[baseCol.modelData]
|
||||
width: 12
|
||||
height: width
|
||||
radius: width
|
||||
color: tag.urgent ? Colours.c.red_b : (tag.enabled) ? Colours.c.yellow_b : Colours.c.black
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
border {
|
||||
width: 1
|
||||
color: tag.urgent ? Colours.c.red_b : Colours.c.yellow_b
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
property var tag: Tags.tags[baseCol.modelData]
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 1
|
||||
height: 1
|
||||
color: tag.urgent ? Colours.c.red_b : (tag.occupied) ? Colours.c.yellow_b : Colours.c.black
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
color: "transparent"
|
||||
} // inner container
|
||||
// Rectangle {
|
||||
// anchors {
|
||||
// fill: parent
|
||||
// leftMargin: 12
|
||||
// rightMargin: 12
|
||||
// topMargin: 3
|
||||
// }
|
||||
// Row {
|
||||
// anchors {
|
||||
// fill: parent
|
||||
// }
|
||||
// spacing: 1
|
||||
// Repeater {
|
||||
// model: Tags.keys
|
||||
// delegate: Column {
|
||||
// id: baseCol
|
||||
// required property var modelData
|
||||
// spacing: 3
|
||||
// Rectangle {
|
||||
// property var tag: Tags.tags[baseCol.modelData]
|
||||
// width: 12
|
||||
// height: width
|
||||
// radius: width
|
||||
// color: tag.urgent ? Colours.c.red_b : (tag.enabled) ? Colours.c.yellow_b : Colours.c.black
|
||||
// Behavior on color {
|
||||
// ColorAnimation {
|
||||
// duration: 300
|
||||
// }
|
||||
// }
|
||||
// border {
|
||||
// width: 1
|
||||
// color: tag.urgent ? Colours.c.red_b : Colours.c.yellow_b
|
||||
// Behavior on color {
|
||||
// ColorAnimation {
|
||||
// duration: 300
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Rectangle {
|
||||
// property var tag: Tags.tags[baseCol.modelData]
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// width: 1
|
||||
// height: 1
|
||||
// color: tag.urgent ? Colours.c.red_b : (tag.occupied) ? Colours.c.yellow_b : Colours.c.black
|
||||
// Behavior on color {
|
||||
// ColorAnimation {
|
||||
// duration: 300
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// color: "transparent"
|
||||
// } // inner container
|
||||
|
||||
}// outer visible rect
|
||||
// }// outer visible rect
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
screen: modelData
|
||||
}//invisible rect
|
||||
}
|
||||
// exclusionMode: ExclusionMode.Ignore
|
||||
// WlrLayershell.layer: WlrLayer.Top
|
||||
// screen: modelData
|
||||
// }//invisible rect
|
||||
// }
|
||||
|
||||
// pops up on current monitor
|
||||
Launcher.Launcher {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ in
|
|||
inputs.stash.nixosModules.default
|
||||
];
|
||||
services.stasis = {
|
||||
enable = true;
|
||||
enable = false;
|
||||
extraPathPackages = [ (config.programs.niri.package) ];
|
||||
extraConfig = ''
|
||||
default:
|
||||
|
|
@ -44,32 +44,35 @@ in
|
|||
};
|
||||
|
||||
quick.services = {
|
||||
noti = "${getExe pkgs.swaynotificationcenter}";
|
||||
shell = "${getExe pkgs.quickshell}";
|
||||
pwManager = "${getExe config.apps.passwordManager}";
|
||||
noti.cmd = "${getExe pkgs.swaynotificationcenter}";
|
||||
shell = {
|
||||
cmd = "${getExe pkgs.quickshell}";
|
||||
restart = true;
|
||||
};
|
||||
# pwManager.cmd = "${getExe config.apps.passwordManager}";
|
||||
# music = "${getExe config.apps.streamPlayer}";
|
||||
};
|
||||
|
||||
user.systemd.services.music = {
|
||||
environment.PATH = lib.mkForce "/run/current-system/sw/bin:/run/current-system/sw/sbin:/etc/profiles/per-user/${mainUser}/bin:/etc/profiles/per-user/${mainUser}/sbin";
|
||||
unitConfig = {
|
||||
Description = "airdrome";
|
||||
Requires = [
|
||||
"graphical-session.target"
|
||||
];
|
||||
After = [
|
||||
"graphical-session.target"
|
||||
"niri.target"
|
||||
];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${getExe config.apps.streamPlayer}";
|
||||
Type = "forking";
|
||||
};
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
# user.systemd.services.music = {
|
||||
# environment.PATH = lib.mkForce "/run/current-system/sw/bin:/run/current-system/sw/sbin:/etc/profiles/per-user/${mainUser}/bin:/etc/profiles/per-user/${mainUser}/sbin";
|
||||
# unitConfig = {
|
||||
# Description = "airdrome";
|
||||
# Requires = [
|
||||
# "graphical-session.target"
|
||||
# ];
|
||||
# After = [
|
||||
# "graphical-session.target"
|
||||
# "niri.target"
|
||||
# ];
|
||||
# PartOf = [ "graphical-session.target" ];
|
||||
# };
|
||||
# serviceConfig = {
|
||||
# ExecStart = "${getExe config.apps.streamPlayer}";
|
||||
# Type = "forking";
|
||||
# };
|
||||
# wantedBy = [ "graphical-session.target" ];
|
||||
|
||||
};
|
||||
# };
|
||||
|
||||
environment.files."/home/${mainUser}/.config/quickshell" = {
|
||||
source = "/home/${mainUser}/.nix/graphical/desktop/quickshell";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
mainUser,
|
||||
inputs,
|
||||
getFlakePkg',
|
||||
lib,
|
||||
|
|
@ -8,14 +7,19 @@
|
|||
...
|
||||
}:
|
||||
let
|
||||
niri = (getFlakePkg' inputs.niri "niri-unstable");
|
||||
xwayland-satellite = (getFlakePkg' inputs.niri "xwayland-satellite-unstable");
|
||||
inherit (config) rice;
|
||||
niri = getFlakePkg' inputs.niri "niri-unstable";
|
||||
xwayland-satellite = getFlakePkg' inputs.niri "xwayland-satellite-unstable";
|
||||
niri-session-direct = pkgs.writeShellScript "niri-session-direct" ''
|
||||
systemctl --user import-environment
|
||||
dbus-update-activation-environment --all
|
||||
systemctl --user start niri-session-bridge.service &
|
||||
exec ${lib.getExe niri} --session
|
||||
'';
|
||||
in
|
||||
{
|
||||
|
||||
imports = [
|
||||
inputs.niri.nixosModules.niri
|
||||
inputs.niri-tag.nixosModules.niri-tag
|
||||
inputs.niri-s76.nixosModules.default
|
||||
];
|
||||
|
|
@ -51,34 +55,19 @@ in
|
|||
baseConfig;
|
||||
};
|
||||
|
||||
user.packages = [
|
||||
environment.systemPackages = [
|
||||
niri
|
||||
xwayland-satellite
|
||||
];
|
||||
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
restart = false;
|
||||
settings =
|
||||
let
|
||||
session = {
|
||||
command =
|
||||
let
|
||||
niri-session-direct = pkgs.writeShellScript "niri-session-direct" ''
|
||||
systemctl --user import-environment
|
||||
dbus-update-activation-environment --all
|
||||
exec ${lib.getExe niri} --session
|
||||
'';
|
||||
in
|
||||
"${niri-session-direct}";
|
||||
user = "${mainUser}";
|
||||
};
|
||||
in
|
||||
{
|
||||
default_session = session;
|
||||
initial_session = session;
|
||||
};
|
||||
};
|
||||
environment.etc."greetd/wayland-sessions/niri.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Name=Niri
|
||||
Comment=A scrollable-tiling Wayland compositor
|
||||
Exec=${niri-session-direct}
|
||||
Type=Application
|
||||
DesktopNames=niri
|
||||
'';
|
||||
|
||||
programs.niri = {
|
||||
enable = true;
|
||||
|
|
@ -97,8 +86,26 @@ in
|
|||
|
||||
services.niri-s76-bridge.enable = true;
|
||||
|
||||
# niri runs directly from greetd (session-1.scope), not as a user service
|
||||
# niri runs directly from greetd (not as a user service),
|
||||
# so that it stays inside the logind session scope for proper polkit/dbus access.
|
||||
systemd.user.services.niri.wantedBy = lib.mkForce [ ];
|
||||
systemd.user.services.niri.enable = lib.mkForce false;
|
||||
|
||||
# bridge service to activate graphical-session.target for the direct-launched niri.
|
||||
# waits for niri IPC readiness before pulling in the target.
|
||||
systemd.user.services.niri-session-bridge = {
|
||||
unitConfig = {
|
||||
Description = "Activate graphical-session.target for direct-launched niri";
|
||||
BindsTo = [ "graphical-session.target" ];
|
||||
Before = [ "graphical-session.target" ];
|
||||
Wants = [ "graphical-session-pre.target" ];
|
||||
After = [ "graphical-session-pre.target" ];
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "${lib.getExe niri} msg version";
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
{
|
||||
inputs,
|
||||
localPkgs,
|
||||
getFlakePkg',
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
scope "user" {
|
||||
programs = {
|
||||
direnv = {
|
||||
enable = true;
|
||||
integrations.fish.enable = true;
|
||||
};
|
||||
direnv.enable = true;
|
||||
|
||||
git = {
|
||||
enable = true;
|
||||
|
|
@ -34,4 +29,6 @@ scope "user" {
|
|||
};
|
||||
};
|
||||
|
||||
packages = [ pkgs.jujutsu ];
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,12 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
xdg.autostart.enable = lib.mkForce false;
|
||||
xdg.portal = {
|
||||
enable = true;
|
||||
config = {
|
||||
common = {
|
||||
default = [
|
||||
"gnome"
|
||||
];
|
||||
};
|
||||
};
|
||||
extraPortals = [
|
||||
pkgs.xdg-desktop-portal-gnome
|
||||
];
|
||||
};
|
||||
|
||||
environment.pathsToLink = [
|
||||
"/share/xdg-desktop-portal"
|
||||
"/share/applications"
|
||||
];
|
||||
|
||||
security.polkit.enable = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
bunker.kernel = {
|
||||
enable = true;
|
||||
cpuArch = "MZEN3";
|
||||
version = "6.18";
|
||||
version = "6.19";
|
||||
hardened = false;
|
||||
lto = "none";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,24 +4,44 @@
|
|||
mainUser,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkOption types mkIf;
|
||||
inherit (types)
|
||||
attrsOf
|
||||
str
|
||||
bool
|
||||
submodule
|
||||
;
|
||||
in
|
||||
scope "options.quick" {
|
||||
services =
|
||||
with lib;
|
||||
mkOption {
|
||||
type = with types; attrsOf str;
|
||||
default = { };
|
||||
};
|
||||
oneShots =
|
||||
with lib;
|
||||
mkOption {
|
||||
type = with types; attrsOf str;
|
||||
default = { };
|
||||
};
|
||||
services = mkOption {
|
||||
type = attrsOf (
|
||||
submodule (
|
||||
{ ... }:
|
||||
{
|
||||
options = {
|
||||
cmd = mkOption {
|
||||
type = str;
|
||||
};
|
||||
restart = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
oneShots = mkOption {
|
||||
type = attrsOf str;
|
||||
default = { };
|
||||
};
|
||||
}
|
||||
// scope "config.user.systemd" {
|
||||
enable = true;
|
||||
services =
|
||||
builtins.mapAttrs (name: cmd: {
|
||||
builtins.mapAttrs (name: opts: {
|
||||
environment.PATH = lib.mkForce "/run/current-system/sw/bin:/run/current-system/sw/sbin:/etc/profiles/per-user/${mainUser}/bin:/etc/profiles/per-user/${mainUser}/sbin";
|
||||
unitConfig = {
|
||||
Description = "${name}";
|
||||
|
|
@ -35,8 +55,8 @@ scope "options.quick" {
|
|||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = cmd;
|
||||
Restart = "always";
|
||||
ExecStart = opts.cmd;
|
||||
Restart = mkIf (opts.restart) "always";
|
||||
};
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
}) config.quick.services
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
...
|
||||
}:
|
||||
{
|
||||
environment.sessionVariables.XCURSOR_THEME = config.rice.cursor.name;
|
||||
|
||||
user.packages =
|
||||
let
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
scope "user.systemd.services.startup-sound"
|
||||
<| {
|
||||
unitConfig = {
|
||||
Description = "startup sound";
|
||||
Requires = [
|
||||
"graphical-session.target"
|
||||
];
|
||||
After = [
|
||||
"graphical-session.target"
|
||||
"niri.target"
|
||||
"sound.target"
|
||||
"shell.service"
|
||||
];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${lib.getExe' pkgs.alsa-utils "aplay"} " + ../assets/startup.wav;
|
||||
Type = "oneshot";
|
||||
};
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
}
|
||||
_: { }
|
||||
# {
|
||||
# pkgs,
|
||||
# lib,
|
||||
# ...
|
||||
# }:
|
||||
# scope "user.systemd.services.startup-sound"
|
||||
# <| {
|
||||
# unitConfig = {
|
||||
# Description = "startup sound";
|
||||
# Requires = [
|
||||
# "graphical-session.target"
|
||||
# ];
|
||||
# After = [
|
||||
# "graphical-session.target"
|
||||
# "niri.target"
|
||||
# "sound.target"
|
||||
# "shell.service"
|
||||
# ];
|
||||
# PartOf = [ "graphical-session.target" ];
|
||||
# };
|
||||
# serviceConfig = {
|
||||
# ExecStart = "${lib.getExe' pkgs.alsa-utils "aplay"} " + ../assets/startup.wav;
|
||||
# Type = "oneshot";
|
||||
# };
|
||||
# wantedBy = [ "graphical-session.target" ];
|
||||
# }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
|
@ -18,6 +19,12 @@
|
|||
font-bold = font;
|
||||
font-italic = font;
|
||||
};
|
||||
bell = {
|
||||
system = true;
|
||||
urgent = true;
|
||||
notify = true;
|
||||
visual = true;
|
||||
};
|
||||
colors =
|
||||
let
|
||||
pal = config.rice.palette.shortHex;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
nixpkgs.config.cudaSupport = true;
|
||||
|
||||
hardware.nvidia = {
|
||||
package = config.boot.kernelPackages.nvidiaPackages.beta;
|
||||
package = config.boot.kernelPackages.nvidiaPackages.production;
|
||||
modesetting.enable = true;
|
||||
powerManagement.enable = true;
|
||||
open = true;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ in
|
|||
};
|
||||
|
||||
quick.services = {
|
||||
ckb-next = "${lib.getExe ckb-next} -c -b";
|
||||
ckb-next.cmd = "${lib.getExe ckb-next} -c -b";
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue