mirror of
https://github.com/moons-14/dotfiles.git
synced 2026-10-07 08:14:08 +09:00
window-overview
This commit is contained in:
Generated
+22
-1
@@ -1227,7 +1227,8 @@
|
|||||||
"systems": "systems_8",
|
"systems": "systems_8",
|
||||||
"treefmt-nix": "treefmt-nix_3",
|
"treefmt-nix": "treefmt-nix_3",
|
||||||
"vicinae": "vicinae",
|
"vicinae": "vicinae",
|
||||||
"vicinae-extensions": "vicinae-extensions"
|
"vicinae-extensions": "vicinae-extensions",
|
||||||
|
"window-overview": "window-overview"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
@@ -1683,6 +1684,26 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"window-overview": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1786126873,
|
||||||
|
"narHash": "sha256-ALECKOKBXGfzfTkZuGIAKKwkFII3NWpf9mnmKIfGOG4=",
|
||||||
|
"owner": "moons-14",
|
||||||
|
"repo": "window-overview",
|
||||||
|
"rev": "7565da153bd7cb1bddce87c23667a41775f7ca74",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "moons-14",
|
||||||
|
"repo": "window-overview",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"xwayland-satellite-stable": {
|
"xwayland-satellite-stable": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
# Desktop
|
# Desktop
|
||||||
niri-flake.url = "github:sodiboo/niri-flake";
|
niri-flake.url = "github:sodiboo/niri-flake";
|
||||||
|
|
||||||
|
window-overview = {
|
||||||
|
url = "github:moons-14/window-overview";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
nix-hazkey = {
|
nix-hazkey = {
|
||||||
url = "github:aster-void/nix-hazkey";
|
url = "github:aster-void/nix-hazkey";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ diff --git a/src/shell/bar/widgets/taskbar_widget.cpp b/src/shell/bar/widgets/ta
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
+ [[nodiscard]] bool launchTaskbarOverview(const std::string& appId) {
|
+ [[nodiscard]] bool launchTaskbarOverview(const std::string& appId) {
|
||||||
+ constexpr const char* command = "noctalia-taskbar-overview";
|
+ constexpr const char* command = "window-overview";
|
||||||
+ if (appId.empty() || !process::commandExists(command)) {
|
+ if (appId.empty() || !process::commandExists(command)) {
|
||||||
+ return false;
|
+ return false;
|
||||||
+ }
|
+ }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{ pkgs, ... }:
|
{ inputs, pkgs, ... }:
|
||||||
let
|
|
||||||
windowOverview = pkgs.callPackage ../package.nix { };
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
home.packages = [ windowOverview ];
|
home.packages = [
|
||||||
|
inputs.window-overview.packages.${pkgs.stdenv.hostPlatform.system}.window-overview
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
fetchCrate,
|
|
||||||
libgbm,
|
|
||||||
libglvnd,
|
|
||||||
libxkbcommon,
|
|
||||||
jq,
|
|
||||||
makeWrapper,
|
|
||||||
pkg-config,
|
|
||||||
rustPlatform,
|
|
||||||
wayland,
|
|
||||||
wl-clipboard,
|
|
||||||
}:
|
|
||||||
rustPlatform.buildRustPackage rec {
|
|
||||||
pname = "window-overview";
|
|
||||||
version = "1.5.0";
|
|
||||||
|
|
||||||
src = fetchCrate {
|
|
||||||
pname = "wlr-chooser";
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-Jb59m1z+2G5istcbC0sg9jRVcC/bQh/OY0ny9OdTsdw=";
|
|
||||||
};
|
|
||||||
|
|
||||||
patches = [
|
|
||||||
./patches/window-overview.patch
|
|
||||||
./patches/niri-backend.patch
|
|
||||||
./patches/niri-hold-release.patch
|
|
||||||
];
|
|
||||||
|
|
||||||
cargoHash = "sha256-KBLgtS8ULrmsOf6BN5SlkVQyXB12utvr/KonnKnCTCM=";
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
makeWrapper
|
|
||||||
pkg-config
|
|
||||||
];
|
|
||||||
|
|
||||||
buildInputs = [
|
|
||||||
libgbm
|
|
||||||
libglvnd
|
|
||||||
libxkbcommon
|
|
||||||
wayland
|
|
||||||
];
|
|
||||||
|
|
||||||
cargoBuildFlags = [
|
|
||||||
"--bin"
|
|
||||||
"wlr-switcher"
|
|
||||||
];
|
|
||||||
|
|
||||||
postInstall = ''
|
|
||||||
mv "$out/bin/wlr-switcher" "$out/bin/.window-overview-wrapped"
|
|
||||||
makeWrapper "$out/bin/.window-overview-wrapped" "$out/bin/window-overview" \
|
|
||||||
--add-flags "--layout grid" \
|
|
||||||
--prefix PATH : "${
|
|
||||||
lib.makeBinPath [
|
|
||||||
jq
|
|
||||||
wl-clipboard
|
|
||||||
]
|
|
||||||
}" \
|
|
||||||
--prefix LD_LIBRARY_PATH : "${
|
|
||||||
lib.makeLibraryPath [
|
|
||||||
libgbm
|
|
||||||
libglvnd
|
|
||||||
]
|
|
||||||
}:/run/opengl-driver/lib"
|
|
||||||
ln -s window-overview "$out/bin/noctalia-taskbar-overview"
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "Fullscreen Wayland window overview with captured previews";
|
|
||||||
homepage = "https://github.com/sjourdois/wlr-utils";
|
|
||||||
license = with lib.licenses; [
|
|
||||||
asl20
|
|
||||||
mit
|
|
||||||
];
|
|
||||||
mainProgram = "window-overview";
|
|
||||||
platforms = lib.platforms.linux;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
--- a/src/chooser_cli.rs
|
|
||||||
+++ b/src/chooser_cli.rs
|
|
||||||
@@ -81,6 +81,7 @@
|
|
||||||
initial_cycle: None,
|
|
||||||
snapshot: false,
|
|
||||||
cycle_socket: None,
|
|
||||||
+ niri_backend: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
match run_overlay(opts, t0) {
|
|
||||||
--- a/src/lib.rs
|
|
||||||
+++ b/src/lib.rs
|
|
||||||
@@ -65,7 +65,14 @@
|
|
||||||
// must connect, enumerate, and open sessions before any thumbnail appears.
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
let snapshot = opts.snapshot;
|
|
||||||
- std::thread::spawn(move || ui::capture_thread(tx, snapshot));
|
|
||||||
+ let niri_backend = opts.niri_backend;
|
|
||||||
+ std::thread::spawn(move || {
|
|
||||||
+ if niri_backend {
|
|
||||||
+ ui::niri_capture_thread(tx);
|
|
||||||
+ } else {
|
|
||||||
+ ui::capture_thread(tx, snapshot);
|
|
||||||
+ }
|
|
||||||
+ });
|
|
||||||
shell::tlog(t0, "capture-thread spawned");
|
|
||||||
|
|
||||||
let out: ui::Outcome = Arc::new(Mutex::new(None));
|
|
||||||
--- a/src/switcher_cli.rs
|
|
||||||
+++ b/src/switcher_cli.rs
|
|
||||||
@@ -88,6 +88,7 @@
|
|
||||||
let t0 = Instant::now();
|
|
||||||
let cli = Cli::parse();
|
|
||||||
i18n::init();
|
|
||||||
+ let niri_backend = std::env::var_os("NIRI_SOCKET").is_some();
|
|
||||||
|
|
||||||
if cli.doctor {
|
|
||||||
if let Err(e) = wlr_capture::doctor::report("wlr-switcher", env!("CARGO_PKG_VERSION")) {
|
|
||||||
@@ -129,32 +130,50 @@
|
|
||||||
initial_cycle: Some(!cli.reverse),
|
|
||||||
snapshot: true,
|
|
||||||
cycle_socket: Some(cycle_socket),
|
|
||||||
+ niri_backend,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pre-flight: wlr-switcher switches *windows*, which need the foreign-toplevel
|
|
||||||
// capture source (wlroots >= 0.20 / Sway >= 1.12). On older compositors connect()
|
|
||||||
// now succeeds for screen-only capture, but there are no windows to offer — so say
|
|
||||||
// so clearly and exit, instead of showing an empty dimmed overlay (issue #1).
|
|
||||||
- match wl::Client::connect() {
|
|
||||||
- Ok(client) if !client.can_capture_windows() => {
|
|
||||||
- eprintln!("{}", tr!("capture-no-window"));
|
|
||||||
- std::process::exit(2);
|
|
||||||
- }
|
|
||||||
- Ok(_) => {}
|
|
||||||
- Err(e) => {
|
|
||||||
- eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
|
||||||
- std::process::exit(2);
|
|
||||||
+ if !niri_backend {
|
|
||||||
+ match wl::Client::connect() {
|
|
||||||
+ Ok(client) if !client.can_capture_windows() => {
|
|
||||||
+ eprintln!("{}", tr!("capture-no-window"));
|
|
||||||
+ std::process::exit(2);
|
|
||||||
+ }
|
|
||||||
+ Ok(_) => {}
|
|
||||||
+ Err(e) => {
|
|
||||||
+ eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
|
||||||
+ std::process::exit(2);
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match run_overlay(opts, t0) {
|
|
||||||
Ok(Some(sel)) => {
|
|
||||||
// Focus the picked window (outputs aren't focusable, so ignore them).
|
|
||||||
- if sel.is_window
|
|
||||||
- && let Err(e) = wl::activate_window(&sel.app_id, &sel.title, sel.dup_index)
|
|
||||||
- {
|
|
||||||
- eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
|
||||||
- std::process::exit(2);
|
|
||||||
+ if sel.is_window {
|
|
||||||
+ let result: anyhow::Result<()> = if niri_backend {
|
|
||||||
+ let output = std::process::Command::new("niri")
|
|
||||||
+ .args(["msg", "action", "focus-window", "--id", &sel.identifier])
|
|
||||||
+ .output();
|
|
||||||
+ match output {
|
|
||||||
+ Ok(output) if output.status.success() => Ok(()),
|
|
||||||
+ Ok(output) => Err(anyhow::anyhow!(
|
|
||||||
+ "{}",
|
|
||||||
+ String::from_utf8_lossy(&output.stderr).trim()
|
|
||||||
+ )),
|
|
||||||
+ Err(e) => Err(e.into()),
|
|
||||||
+ }
|
|
||||||
+ } else {
|
|
||||||
+ wl::activate_window(&sel.app_id, &sel.title, sel.dup_index).map_err(Into::into)
|
|
||||||
+ };
|
|
||||||
+ if let Err(e) = result {
|
|
||||||
+ eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
|
||||||
+ std::process::exit(2);
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => std::process::exit(1), // cancelled
|
|
||||||
--- a/src/ui.rs
|
|
||||||
+++ b/src/ui.rs
|
|
||||||
@@ -6,7 +6,9 @@
|
|
||||||
|
|
||||||
use crate::tr;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
+use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
+use std::process::{Command, Stdio};
|
|
||||||
use std::sync::mpsc::{Receiver, Sender};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
@@ -366,6 +368,288 @@
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+#[derive(Clone)]
|
|
||||||
+struct NiriWindow {
|
|
||||||
+ id: String,
|
|
||||||
+ app_id: String,
|
|
||||||
+ title: String,
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+enum ClipboardSnapshot {
|
|
||||||
+ Empty,
|
|
||||||
+ Content { mime: String, data: Vec<u8> },
|
|
||||||
+ Unavailable,
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+fn save_clipboard() -> ClipboardSnapshot {
|
|
||||||
+ let Ok(types) = Command::new("wl-paste").arg("--list-types").output() else {
|
|
||||||
+ return ClipboardSnapshot::Unavailable;
|
|
||||||
+ };
|
|
||||||
+ if !types.status.success() {
|
|
||||||
+ return ClipboardSnapshot::Empty;
|
|
||||||
+ }
|
|
||||||
+ let types = String::from_utf8_lossy(&types.stdout);
|
|
||||||
+ let offered: Vec<&str> = types.lines().filter(|line| !line.is_empty()).collect();
|
|
||||||
+ let Some(mime) = offered
|
|
||||||
+ .iter()
|
|
||||||
+ .copied()
|
|
||||||
+ .find(|mime| *mime == "text/plain;charset=utf-8")
|
|
||||||
+ .or_else(|| offered.first().copied())
|
|
||||||
+ else {
|
|
||||||
+ return ClipboardSnapshot::Empty;
|
|
||||||
+ };
|
|
||||||
+ let Ok(data) = Command::new("wl-paste")
|
|
||||||
+ .args(["--type", mime])
|
|
||||||
+ .output()
|
|
||||||
+ else {
|
|
||||||
+ return ClipboardSnapshot::Unavailable;
|
|
||||||
+ };
|
|
||||||
+ if !data.status.success() {
|
|
||||||
+ return ClipboardSnapshot::Unavailable;
|
|
||||||
+ }
|
|
||||||
+ ClipboardSnapshot::Content {
|
|
||||||
+ mime: mime.to_owned(),
|
|
||||||
+ data: data.stdout,
|
|
||||||
+ }
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+fn restore_clipboard(snapshot: ClipboardSnapshot) {
|
|
||||||
+ match snapshot {
|
|
||||||
+ ClipboardSnapshot::Empty => {
|
|
||||||
+ let _ = Command::new("wl-copy").arg("--clear").status();
|
|
||||||
+ }
|
|
||||||
+ ClipboardSnapshot::Content { mime, data } => {
|
|
||||||
+ let Ok(mut copy) = Command::new("wl-copy")
|
|
||||||
+ .args(["--type", &mime])
|
|
||||||
+ .stdin(Stdio::piped())
|
|
||||||
+ .spawn()
|
|
||||||
+ else {
|
|
||||||
+ return;
|
|
||||||
+ };
|
|
||||||
+ if let Some(mut stdin) = copy.stdin.take() {
|
|
||||||
+ let _ = stdin.write_all(&data);
|
|
||||||
+ }
|
|
||||||
+ let _ = copy.wait();
|
|
||||||
+ }
|
|
||||||
+ ClipboardSnapshot::Unavailable => {}
|
|
||||||
+ }
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+/// Decode one field emitted by jq's `@tsv` formatter. It escapes the only
|
|
||||||
+/// characters that would otherwise make the line-oriented transport ambiguous.
|
|
||||||
+fn unescape_tsv(value: &str) -> String {
|
|
||||||
+ let mut decoded = String::with_capacity(value.len());
|
|
||||||
+ let mut chars = value.chars();
|
|
||||||
+ while let Some(ch) = chars.next() {
|
|
||||||
+ if ch != '\\' {
|
|
||||||
+ decoded.push(ch);
|
|
||||||
+ continue;
|
|
||||||
+ }
|
|
||||||
+ match chars.next() {
|
|
||||||
+ Some('t') => decoded.push('\t'),
|
|
||||||
+ Some('r') => decoded.push('\r'),
|
|
||||||
+ Some('n') => decoded.push('\n'),
|
|
||||||
+ Some('\\') => decoded.push('\\'),
|
|
||||||
+ Some(other) => {
|
|
||||||
+ decoded.push('\\');
|
|
||||||
+ decoded.push(other);
|
|
||||||
+ }
|
|
||||||
+ None => decoded.push('\\'),
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ decoded
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+/// Ask niri for its toplevel list. niri does not implement the
|
|
||||||
+/// ext-image-copy-capture window protocol, so its IPC is the source of both the
|
|
||||||
+/// stable window id and the metadata used by this backend.
|
|
||||||
+fn niri_windows() -> Result<Vec<NiriWindow>, String> {
|
|
||||||
+ let response = Command::new("niri")
|
|
||||||
+ .args(["msg", "-j", "windows"])
|
|
||||||
+ .output()
|
|
||||||
+ .map_err(|e| format!("could not run niri msg: {e}"))?;
|
|
||||||
+ if !response.status.success() {
|
|
||||||
+ return Err(String::from_utf8_lossy(&response.stderr).trim().to_owned());
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ let mut jq = Command::new("jq")
|
|
||||||
+ .args([
|
|
||||||
+ "-r",
|
|
||||||
+ ".[] | [(.id | tostring), (.app_id // \"\"), (.title // \"\")] | @tsv",
|
|
||||||
+ ])
|
|
||||||
+ .stdin(Stdio::piped())
|
|
||||||
+ .stdout(Stdio::piped())
|
|
||||||
+ .stderr(Stdio::piped())
|
|
||||||
+ .spawn()
|
|
||||||
+ .map_err(|e| format!("could not run jq: {e}"))?;
|
|
||||||
+ jq.stdin
|
|
||||||
+ .take()
|
|
||||||
+ .ok_or_else(|| String::from("jq stdin was unavailable"))?
|
|
||||||
+ .write_all(&response.stdout)
|
|
||||||
+ .map_err(|e| format!("could not pass niri window list to jq: {e}"))?;
|
|
||||||
+ let output = jq
|
|
||||||
+ .wait_with_output()
|
|
||||||
+ .map_err(|e| format!("could not read jq output: {e}"))?;
|
|
||||||
+ if !output.status.success() {
|
|
||||||
+ return Err(String::from_utf8_lossy(&output.stderr).trim().to_owned());
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ let text = String::from_utf8(output.stdout)
|
|
||||||
+ .map_err(|e| format!("niri window list was not UTF-8: {e}"))?;
|
|
||||||
+ let mut windows = Vec::new();
|
|
||||||
+ for line in text.lines() {
|
|
||||||
+ let mut fields = line.splitn(3, '\t');
|
|
||||||
+ let Some(id) = fields.next() else { continue };
|
|
||||||
+ let Some(app_id) = fields.next() else { continue };
|
|
||||||
+ let Some(title) = fields.next() else { continue };
|
|
||||||
+ windows.push(NiriWindow {
|
|
||||||
+ id: id.to_owned(),
|
|
||||||
+ app_id: unescape_tsv(app_id),
|
|
||||||
+ title: unescape_tsv(title),
|
|
||||||
+ });
|
|
||||||
+ }
|
|
||||||
+ windows.sort_by(|a, b| {
|
|
||||||
+ a.app_id
|
|
||||||
+ .to_lowercase()
|
|
||||||
+ .cmp(&b.app_id.to_lowercase())
|
|
||||||
+ .then_with(|| a.title.to_lowercase().cmp(&b.title.to_lowercase()))
|
|
||||||
+ });
|
|
||||||
+ Ok(windows)
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+fn niri_window_source(w: &NiriWindow, dup_index: usize) -> Source {
|
|
||||||
+ let is_system = w.app_id.is_empty();
|
|
||||||
+ let (title, subtitle) = if is_system {
|
|
||||||
+ (w.title.clone(), String::new())
|
|
||||||
+ } else {
|
|
||||||
+ (w.app_id.clone(), w.title.clone())
|
|
||||||
+ };
|
|
||||||
+ Source {
|
|
||||||
+ key: w.id.clone(),
|
|
||||||
+ token: format!("Window: {}", w.id),
|
|
||||||
+ filter: format!("{} {}", w.app_id, w.title).to_lowercase(),
|
|
||||||
+ title,
|
|
||||||
+ subtitle,
|
|
||||||
+ is_window: true,
|
|
||||||
+ is_system,
|
|
||||||
+ app_id: w.app_id.clone(),
|
|
||||||
+ win_title: w.title.clone(),
|
|
||||||
+ dup_index,
|
|
||||||
+ }
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+/// Capture a static overview through niri's IPC. This is deliberately separate
|
|
||||||
+/// from the generic Wayland backend: niri can render any toplevel by id (even an
|
|
||||||
+/// occluded one), but does not advertise ext-image-copy-capture.
|
|
||||||
+pub fn niri_capture_thread(tx: Sender<Msg>) {
|
|
||||||
+ let windows = match niri_windows() {
|
|
||||||
+ Ok(windows) => windows,
|
|
||||||
+ Err(e) => {
|
|
||||||
+ eprintln!("niri overview backend: {e}");
|
|
||||||
+ return;
|
|
||||||
+ }
|
|
||||||
+ };
|
|
||||||
+
|
|
||||||
+ let mut dup: HashMap<(String, String), usize> = HashMap::new();
|
|
||||||
+ let mut current = Vec::with_capacity(windows.len());
|
|
||||||
+ for window in windows {
|
|
||||||
+ let e = dup
|
|
||||||
+ .entry((window.app_id.clone(), window.title.clone()))
|
|
||||||
+ .or_insert(0);
|
|
||||||
+ let source = niri_window_source(&window, *e);
|
|
||||||
+ *e += 1;
|
|
||||||
+ current.push((source, window));
|
|
||||||
+ }
|
|
||||||
+ if tx
|
|
||||||
+ .send(Msg::Sources(
|
|
||||||
+ current.iter().map(|(source, _)| source.clone()).collect(),
|
|
||||||
+ ))
|
|
||||||
+ .is_err()
|
|
||||||
+ {
|
|
||||||
+ return;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ let capture_dir = wlr_capture::paths::runtime_dir().join(format!(
|
|
||||||
+ "window-overview-niri-{}",
|
|
||||||
+ std::process::id()
|
|
||||||
+ ));
|
|
||||||
+ if let Err(e) = std::fs::create_dir_all(&capture_dir) {
|
|
||||||
+ eprintln!("niri overview backend: could not create capture directory: {e}");
|
|
||||||
+ return;
|
|
||||||
+ }
|
|
||||||
+ // niri's screenshot action also sets the clipboard. Preserve the existing
|
|
||||||
+ // selection so opening the overview does not unexpectedly replace it.
|
|
||||||
+ let clipboard = save_clipboard();
|
|
||||||
+
|
|
||||||
+ for (source, window) in current {
|
|
||||||
+ if let Some(path) = icons::resolve(&window.app_id)
|
|
||||||
+ && let Some((w, h, rgba)) = icons::load(&path, 128)
|
|
||||||
+ && tx
|
|
||||||
+ .send(Msg::Icon {
|
|
||||||
+ key: source.key.clone(),
|
|
||||||
+ w: w as usize,
|
|
||||||
+ h: h as usize,
|
|
||||||
+ rgba,
|
|
||||||
+ })
|
|
||||||
+ .is_err()
|
|
||||||
+ {
|
|
||||||
+ break;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ let path = capture_dir.join(format!("{}.png", window.id));
|
|
||||||
+ let command = Command::new("niri")
|
|
||||||
+ .args(["msg", "action", "screenshot-window", "--id"])
|
|
||||||
+ .arg(&window.id)
|
|
||||||
+ .args(["--write-to-disk", "true", "--show-pointer", "false", "--path"])
|
|
||||||
+ .arg(&path)
|
|
||||||
+ .output();
|
|
||||||
+ let Ok(output) = command else {
|
|
||||||
+ continue;
|
|
||||||
+ };
|
|
||||||
+ if !output.status.success() {
|
|
||||||
+ eprintln!(
|
|
||||||
+ "niri overview backend: could not capture window {}: {}",
|
|
||||||
+ window.id,
|
|
||||||
+ String::from_utf8_lossy(&output.stderr).trim()
|
|
||||||
+ );
|
|
||||||
+ continue;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ // niri encodes PNGs off the compositor thread, after acknowledging the
|
|
||||||
+ // IPC action. Wait briefly for that completion without delaying the UI.
|
|
||||||
+ let mut bytes = None;
|
|
||||||
+ for _ in 0..200 {
|
|
||||||
+ match std::fs::read(&path) {
|
|
||||||
+ Ok(data) => {
|
|
||||||
+ bytes = Some(data);
|
|
||||||
+ break;
|
|
||||||
+ }
|
|
||||||
+ Err(_) => std::thread::sleep(Duration::from_millis(10)),
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ let Some(bytes) = bytes else { continue };
|
|
||||||
+ let Ok(image) = image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) else {
|
|
||||||
+ continue;
|
|
||||||
+ };
|
|
||||||
+ let image = image.into_rgba8();
|
|
||||||
+ let (w, h, rgba) = thumbnail_rgba(image.width(), image.height(), image.into_raw());
|
|
||||||
+ if tx
|
|
||||||
+ .send(Msg::Thumb {
|
|
||||||
+ key: source.key,
|
|
||||||
+ w,
|
|
||||||
+ h,
|
|
||||||
+ rgba,
|
|
||||||
+ })
|
|
||||||
+ .is_err()
|
|
||||||
+ {
|
|
||||||
+ break;
|
|
||||||
+ }
|
|
||||||
+ let _ = std::fs::remove_file(path);
|
|
||||||
+ }
|
|
||||||
+ let _ = std::fs::remove_dir(capture_dir);
|
|
||||||
+ restore_clipboard(clipboard);
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
/// Cheap content fingerprint of a frame (subsampled FNV-1a), to tell whether a
|
|
||||||
/// capture actually changed between rounds — used by the headless bench.
|
|
||||||
fn quick_hash(rgba: &[u8]) -> u64 {
|
|
||||||
@@ -515,11 +799,14 @@
|
|
||||||
|
|
||||||
/// Downscale a capture to a thumbnail (max side `THUMB_MAX`), never upscaling.
|
|
||||||
fn thumbnail(img: wl::CapturedImage) -> (usize, usize, Vec<u8>) {
|
|
||||||
- let (w, h) = (img.width, img.height);
|
|
||||||
+ thumbnail_rgba(img.width, img.height, img.rgba)
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+fn thumbnail_rgba(w: u32, h: u32, rgba: Vec<u8>) -> (usize, usize, Vec<u8>) {
|
|
||||||
let scale = (THUMB_MAX as f32 / w as f32)
|
|
||||||
.min(THUMB_MAX as f32 / h as f32)
|
|
||||||
.min(1.0);
|
|
||||||
- let src = match image::RgbaImage::from_raw(w, h, img.rgba) {
|
|
||||||
+ let src = match image::RgbaImage::from_raw(w, h, rgba) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return (0, 0, Vec::new()),
|
|
||||||
};
|
|
||||||
@@ -557,6 +844,9 @@
|
|
||||||
pub snapshot: bool,
|
|
||||||
/// Receives next/previous commands from repeated compositor keybind launches.
|
|
||||||
pub cycle_socket: Option<std::os::unix::net::UnixDatagram>,
|
|
||||||
+ /// Use niri's IPC for window enumeration and snapshots instead of the
|
|
||||||
+ /// ext-image-copy-capture protocol that niri does not implement.
|
|
||||||
+ pub niri_backend: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct App {
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
diff --git a/src/shell.rs b/src/shell.rs
|
|
||||||
index 54fc2f0..dee4def 100644
|
|
||||||
--- a/src/shell.rs
|
|
||||||
+++ b/src/shell.rs
|
|
||||||
@@ -83,2 +83 @@ struct State {
|
|
||||||
- /// We've painted at least one frame with the modifier genuinely held; gates the
|
|
||||||
- /// confirm-on-release so the launching chord's tail can't trigger it.
|
|
||||||
+ /// A rendered hold after sources arrived, enabling immediate confirmation on release.
|
|
||||||
@@ -85,0 +85,2 @@ struct State {
|
|
||||||
+ /// A release received before source enumeration completes, to confirm after render.
|
|
||||||
+ pending_release: bool,
|
|
||||||
@@ -179,0 +181 @@ pub fn run(app: App, t0: Instant) -> anyhow::Result<()> {
|
|
||||||
+ pending_release: false,
|
|
||||||
@@ -205,5 +207,2 @@ impl State {
|
|
||||||
- // Record that we've shown at least one frame with the modifier held; this
|
|
||||||
- // gates confirm-on-release (see `reconcile`).
|
|
||||||
- if self.armed && self.any_armed_held() {
|
|
||||||
- self.armed_rendered = true;
|
|
||||||
- }
|
|
||||||
+ // Record a frame with the modifier held after source enumeration completes.
|
|
||||||
+ let armed_held = self.armed && self.any_armed_held();
|
|
||||||
@@ -234,0 +234,7 @@ impl State {
|
|
||||||
+ if armed_held && self.app.release_ready() {
|
|
||||||
+ self.armed_rendered = true;
|
|
||||||
+ self.pending_release = false;
|
|
||||||
+ } else if self.pending_release && self.app.release_ready() {
|
|
||||||
+ self.pending_release = false;
|
|
||||||
+ self.app.confirm_release();
|
|
||||||
+ }
|
|
||||||
@@ -485,2 +491,2 @@ impl State {
|
|
||||||
- // Confirm on release — but only after we've painted a frame with the
|
|
||||||
- // modifier genuinely held, so the launching chord's tail can't fire it.
|
|
||||||
+ // Confirm on release. Before the UI is ready, latch the edge and consume it
|
|
||||||
+ // only after `run_ui` has received sources and the frame has rendered.
|
|
||||||
@@ -488,2 +494,12 @@ impl State {
|
|
||||||
- if self.armed && self.armed_rendered && self.prev_held && !held {
|
|
||||||
- self.app.confirm_release();
|
|
||||||
+ // A new hold represents a new switching gesture, so do not consume a
|
|
||||||
+ // release that belonged to an earlier gesture.
|
|
||||||
+ if held {
|
|
||||||
+ self.pending_release = false;
|
|
||||||
+ }
|
|
||||||
+ if self.armed && self.prev_held && !held {
|
|
||||||
+ if self.armed_rendered && self.app.release_ready() {
|
|
||||||
+ self.pending_release = false;
|
|
||||||
+ self.app.confirm_release();
|
|
||||||
+ } else {
|
|
||||||
+ self.pending_release = true;
|
|
||||||
+ }
|
|
||||||
diff --git a/src/ui.rs b/src/ui.rs
|
|
||||||
index c821d66..854f6ad 100644
|
|
||||||
--- a/src/ui.rs
|
|
||||||
+++ b/src/ui.rs
|
|
||||||
@@ -854,0 +855 @@ pub struct App {
|
|
||||||
+ sources_received: bool,
|
|
||||||
@@ -899,0 +901 @@ impl App {
|
|
||||||
+ sources_received: false,
|
|
||||||
@@ -1008 +1010,4 @@ impl App {
|
|
||||||
- Msg::Sources(s) => self.sources = s,
|
|
||||||
+ Msg::Sources(s) => {
|
|
||||||
+ self.sources = s;
|
|
||||||
+ self.sources_received = true;
|
|
||||||
+ }
|
|
||||||
@@ -1080,0 +1086,5 @@ impl App {
|
|
||||||
+ /// True once `run_ui` has received the initial source enumeration.
|
|
||||||
+ pub fn release_ready(&self) -> bool {
|
|
||||||
+ self.sources_received
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
--- a/src/chooser_cli.rs
|
|
||||||
+++ b/src/chooser_cli.rs
|
|
||||||
@@ -79,0 +80,4 @@
|
|
||||||
+ app_id_filter: None,
|
|
||||||
+ initial_cycle: None,
|
|
||||||
+ snapshot: false,
|
|
||||||
+ cycle_socket: None,
|
|
||||||
--- a/src/lib.rs
|
|
||||||
+++ b/src/lib.rs
|
|
||||||
@@ -33,3 +33,3 @@
|
|
||||||
-/// Returns the held lock file (keep it alive), or `None` if another instance owns
|
|
||||||
-/// it — sway processes its own keybinding even over our exclusive keyboard grab,
|
|
||||||
-/// so re-pressing the bind would otherwise stack overlays.
|
|
||||||
+/// Returns the held lock and command socket for the first instance. Later
|
|
||||||
+/// invocations forward their requested cycle direction through the socket and
|
|
||||||
+/// return `None`, which also supports compositors that keep handling the binding.
|
|
||||||
@@ -36 +36,3 @@
|
|
||||||
-pub fn acquire_switch_lock() -> Option<std::fs::File> {
|
|
||||||
+pub fn acquire_switch_lock(
|
|
||||||
+ forward: bool,
|
|
||||||
+) -> Option<(std::fs::File, std::os::unix::net::UnixDatagram)> {
|
|
||||||
@@ -38,0 +41 @@
|
|
||||||
+ let socket_path = dir.join("wlr-switcher.sock");
|
|
||||||
@@ -45,2 +49,10 @@
|
|
||||||
- flock(&f, FlockOperation::NonBlockingLockExclusive).ok()?;
|
|
||||||
- Some(f)
|
|
||||||
+ if flock(&f, FlockOperation::NonBlockingLockExclusive).is_err() {
|
|
||||||
+ let socket = std::os::unix::net::UnixDatagram::unbound().ok()?;
|
|
||||||
+ let command = if forward { b"next" } else { b"prev" };
|
|
||||||
+ let _ = socket.send_to(command, socket_path);
|
|
||||||
+ return None;
|
|
||||||
+ }
|
|
||||||
+ let _ = std::fs::remove_file(&socket_path);
|
|
||||||
+ let socket = std::os::unix::net::UnixDatagram::bind(socket_path).ok()?;
|
|
||||||
+ socket.set_nonblocking(true).ok()?;
|
|
||||||
+ Some((f, socket))
|
|
||||||
@@ -56 +68,2 @@
|
|
||||||
- std::thread::spawn(move || ui::capture_thread(tx));
|
|
||||||
+ let snapshot = opts.snapshot;
|
|
||||||
+ std::thread::spawn(move || ui::capture_thread(tx, snapshot));
|
|
||||||
--- a/src/shell.rs
|
|
||||||
+++ b/src/shell.rs
|
|
||||||
@@ -617,0 +618,3 @@
|
|
||||||
+ Keysym::plus | Keysym::KP_Add => Key::Plus,
|
|
||||||
+ Keysym::equal => Key::Equals,
|
|
||||||
+ Keysym::minus | Keysym::KP_Subtract => Key::Minus,
|
|
||||||
--- a/src/switcher_cli.rs
|
|
||||||
+++ b/src/switcher_cli.rs
|
|
||||||
@@ -75,0 +76,6 @@
|
|
||||||
+ /// Show only windows whose Wayland app-id exactly matches this value.
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ app_id: Option<String>,
|
|
||||||
+ /// Select the previous window when the overview first opens.
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ reverse: bool,
|
|
||||||
@@ -96,2 +102,2 @@
|
|
||||||
- let _lock = match acquire_switch_lock() {
|
|
||||||
- Some(lock) => lock,
|
|
||||||
+ let (_lock, cycle_socket) = match acquire_switch_lock(!cli.reverse) {
|
|
||||||
+ Some(instance) => instance,
|
|
||||||
@@ -121,0 +128,4 @@
|
|
||||||
+ app_id_filter: cli.app_id,
|
|
||||||
+ initial_cycle: Some(!cli.reverse),
|
|
||||||
+ snapshot: true,
|
|
||||||
+ cycle_socket: Some(cycle_socket),
|
|
||||||
--- a/src/ui.rs
|
|
||||||
+++ b/src/ui.rs
|
|
||||||
@@ -8,0 +9 @@
|
|
||||||
+use std::path::PathBuf;
|
|
||||||
@@ -41,0 +43,32 @@
|
|
||||||
+const DEFAULT_OVERVIEW_SCALE: f32 = 0.85;
|
|
||||||
+const MIN_OVERVIEW_SCALE: f32 = 0.55;
|
|
||||||
+const MAX_OVERVIEW_SCALE: f32 = 1.0;
|
|
||||||
+
|
|
||||||
+fn overview_scale_path() -> Option<PathBuf> {
|
|
||||||
+ if let Some(path) = std::env::var_os("XDG_STATE_HOME") {
|
|
||||||
+ return Some(PathBuf::from(path).join("window-overview/scale"));
|
|
||||||
+ }
|
|
||||||
+ std::env::var_os("HOME")
|
|
||||||
+ .map(PathBuf::from)
|
|
||||||
+ .map(|path| path.join(".local/state/window-overview/scale"))
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+fn load_overview_scale() -> f32 {
|
|
||||||
+ overview_scale_path()
|
|
||||||
+ .and_then(|path| std::fs::read_to_string(path).ok())
|
|
||||||
+ .and_then(|value| value.trim().parse::<f32>().ok())
|
|
||||||
+ .unwrap_or(DEFAULT_OVERVIEW_SCALE)
|
|
||||||
+ .clamp(MIN_OVERVIEW_SCALE, MAX_OVERVIEW_SCALE)
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+fn save_overview_scale(scale: f32) {
|
|
||||||
+ let Some(path) = overview_scale_path() else {
|
|
||||||
+ return;
|
|
||||||
+ };
|
|
||||||
+ let Some(parent) = path.parent() else {
|
|
||||||
+ return;
|
|
||||||
+ };
|
|
||||||
+ if std::fs::create_dir_all(parent).is_ok() {
|
|
||||||
+ let _ = std::fs::write(path, format!("{scale:.2}\n"));
|
|
||||||
+ }
|
|
||||||
+}
|
|
||||||
@@ -177 +210 @@
|
|
||||||
-pub fn capture_thread(tx: Sender<Msg>) {
|
|
||||||
+pub fn capture_thread(tx: Sender<Msg>, snapshot: bool) {
|
|
||||||
@@ -189,0 +223 @@
|
|
||||||
+ let mut captured: HashSet<String> = HashSet::new();
|
|
||||||
@@ -236,0 +271 @@
|
|
||||||
+ captured.retain(|key| present.contains(key.as_str()));
|
|
||||||
@@ -255 +290 @@
|
|
||||||
- if sessions.contains_key(&s.key) {
|
|
||||||
+ if sessions.contains_key(&s.key) || (snapshot && captured.contains(&s.key)) {
|
|
||||||
@@ -295 +330,3 @@
|
|
||||||
- let Some(key) = by_id.get(&id) else { continue };
|
|
||||||
+ let Some(key) = by_id.get(&id).cloned() else {
|
|
||||||
+ continue;
|
|
||||||
+ };
|
|
||||||
@@ -313,0 +351,6 @@
|
|
||||||
+ if snapshot {
|
|
||||||
+ captured.insert(key.clone());
|
|
||||||
+ sessions.remove(&key);
|
|
||||||
+ by_id.remove(&id);
|
|
||||||
+ client.close_session(&id);
|
|
||||||
+ }
|
|
||||||
@@ -508,0 +552,8 @@
|
|
||||||
+ /// Exact Wayland app-id filter, used by taskbar launches.
|
|
||||||
+ pub app_id_filter: Option<String>,
|
|
||||||
+ /// Initial cycle direction. `None` leaves the first item selected.
|
|
||||||
+ pub initial_cycle: Option<bool>,
|
|
||||||
+ /// Capture each window once instead of continuously refreshing previews.
|
|
||||||
+ pub snapshot: bool,
|
|
||||||
+ /// Receives next/previous commands from repeated compositor keybind launches.
|
|
||||||
+ pub cycle_socket: Option<std::os::unix::net::UnixDatagram>,
|
|
||||||
@@ -533,0 +585,5 @@
|
|
||||||
+ app_id_filter: Option<String>,
|
|
||||||
+ initial_forward: bool,
|
|
||||||
+ overview_scale: f32,
|
|
||||||
+ cycle_socket: Option<std::os::unix::net::UnixDatagram>,
|
|
||||||
+ queued_cycles: isize,
|
|
||||||
@@ -567 +623 @@
|
|
||||||
- pending_initial_select: false,
|
|
||||||
+ pending_initial_select: opts.initial_cycle.is_some(),
|
|
||||||
@@ -571,0 +628,5 @@
|
|
||||||
+ app_id_filter: opts.app_id_filter,
|
|
||||||
+ initial_forward: opts.initial_cycle.unwrap_or(true),
|
|
||||||
+ overview_scale: load_overview_scale(),
|
|
||||||
+ cycle_socket: opts.cycle_socket,
|
|
||||||
+ queued_cycles: 0,
|
|
||||||
@@ -604 +665,4 @@
|
|
||||||
- return; // nothing to cycle yet; keep the pending initial jump
|
|
||||||
+ self.queued_cycles = self
|
|
||||||
+ .queued_cycles
|
|
||||||
+ .saturating_add(if forward { 1 } else { -1 });
|
|
||||||
+ return;
|
|
||||||
@@ -639,0 +704,12 @@
|
|
||||||
+ loop {
|
|
||||||
+ let command = self.cycle_socket.as_ref().and_then(|socket| {
|
|
||||||
+ let mut buf = [0_u8; 8];
|
|
||||||
+ socket.recv(&mut buf).ok().map(|len| (buf, len))
|
|
||||||
+ });
|
|
||||||
+ match command {
|
|
||||||
+ Some((buf, len)) if &buf[..len] == b"next" => self.cycle(true),
|
|
||||||
+ Some((buf, len)) if &buf[..len] == b"prev" => self.cycle(false),
|
|
||||||
+ Some(_) => {}
|
|
||||||
+ None => break,
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
@@ -698,0 +775,5 @@
|
|
||||||
+ .filter(|s| {
|
|
||||||
+ self.app_id_filter
|
|
||||||
+ .as_ref()
|
|
||||||
+ .is_none_or(|app_id| s.app_id.eq_ignore_ascii_case(app_id))
|
|
||||||
+ })
|
|
||||||
@@ -713,2 +793,0 @@
|
|
||||||
- // Exposé covers the whole screen: dim almost to opaque so the real windows
|
|
||||||
- // behind are hidden (a client can't move them; this hides them instead).
|
|
||||||
@@ -716 +795 @@
|
|
||||||
- c[3] = c[3].max(0.96);
|
|
||||||
+ c[3] = c[3].max(0.88);
|
|
||||||
@@ -734 +813,5 @@
|
|
||||||
- self.selected = if n > 1 { 1 } else { 0 };
|
|
||||||
+ self.selected = if n > 1 {
|
|
||||||
+ if self.initial_forward { 1 } else { n - 1 }
|
|
||||||
+ } else {
|
|
||||||
+ 0
|
|
||||||
+ };
|
|
||||||
@@ -737,0 +821,8 @@
|
|
||||||
+ if self.queued_cycles != 0 {
|
|
||||||
+ let n = self.visible().len();
|
|
||||||
+ if n > 0 {
|
|
||||||
+ self.selected = (self.selected as isize + self.queued_cycles)
|
|
||||||
+ .rem_euclid(n as isize) as usize;
|
|
||||||
+ self.queued_cycles = 0;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
@@ -745 +836 @@
|
|
||||||
- let (esc, next, prev, enter) = ctx.input(|i| {
|
|
||||||
+ let (esc, next, prev, enter, zoom_in, zoom_out, wheel_zoom) = ctx.input(|i| {
|
|
||||||
@@ -746,0 +838,8 @@
|
|
||||||
+ let wheel_zoom = i
|
|
||||||
+ .events
|
|
||||||
+ .iter()
|
|
||||||
+ .filter_map(|event| match event {
|
|
||||||
+ egui::Event::MouseWheel { delta, .. } => Some(delta.y),
|
|
||||||
+ _ => None,
|
|
||||||
+ })
|
|
||||||
+ .sum::<f32>();
|
|
||||||
@@ -755,0 +855,3 @@
|
|
||||||
+ i.key_pressed(egui::Key::Plus) || i.key_pressed(egui::Key::Equals),
|
|
||||||
+ i.key_pressed(egui::Key::Minus),
|
|
||||||
+ wheel_zoom,
|
|
||||||
@@ -771,0 +874,12 @@
|
|
||||||
+ if self.view == View::Grid && (zoom_in || zoom_out || wheel_zoom != 0.0) {
|
|
||||||
+ let delta = if zoom_in {
|
|
||||||
+ 0.05
|
|
||||||
+ } else if zoom_out {
|
|
||||||
+ -0.05
|
|
||||||
+ } else {
|
|
||||||
+ wheel_zoom.signum() * 0.05
|
|
||||||
+ };
|
|
||||||
+ self.overview_scale =
|
|
||||||
+ (self.overview_scale + delta).clamp(MIN_OVERVIEW_SCALE, MAX_OVERVIEW_SCALE);
|
|
||||||
+ save_overview_scale(self.overview_scale);
|
|
||||||
+ }
|
|
||||||
@@ -1020 +1134,5 @@
|
|
||||||
- let area = ctx.content_rect().shrink(24.0);
|
|
||||||
+ let full_area = ctx.content_rect().shrink(24.0);
|
|
||||||
+ let area = egui::Rect::from_center_size(
|
|
||||||
+ full_area.center(),
|
|
||||||
+ full_area.size() * self.overview_scale,
|
|
||||||
+ );
|
|
||||||
@@ -1048,0 +1167 @@
|
|
||||||
+ let selected = *i == self.selected;
|
|
||||||
@@ -1062,9 +1181 @@
|
|
||||||
- rect.size() * (0.86 + 0.14 * ease),
|
|
||||||
- );
|
|
||||||
- self.paint_expose_tile(
|
|
||||||
- ui,
|
|
||||||
- s,
|
|
||||||
- scaled,
|
|
||||||
- *i == self.selected,
|
|
||||||
- resp.hovered(),
|
|
||||||
- ease,
|
|
||||||
+ rect.size() * (0.86 + 0.14 * ease),
|
|
||||||
@@ -1071,0 +1185 @@
|
|
||||||
+ self.paint_expose_tile(ui, s, scaled, selected, resp.hovered(), ease);
|
|
||||||
@@ -1107 +1229,3 @@
|
|
||||||
- let white = egui::Color32::WHITE.gamma_multiply(a);
|
|
||||||
+ let white = egui::Color32::WHITE.gamma_multiply(
|
|
||||||
+ a * if selected || hovered { 1.0 } else { 0.7 },
|
|
||||||
+ );
|
|
||||||
Reference in New Issue
Block a user