Compare commits
No commits in common. "main" and "side" have entirely different histories.
|
|
@ -1,2 +0,0 @@
|
|||
[alias]
|
||||
xtask = "run --release --package xtask --"
|
||||
12
Cargo.toml
12
Cargo.toml
|
|
@ -1,8 +1,3 @@
|
|||
[workspace]
|
||||
members = [".", "xtask"]
|
||||
default-members = ["."]
|
||||
resolver = "2"
|
||||
|
||||
[package]
|
||||
name = "layers"
|
||||
version = "0.1.0-dev"
|
||||
|
|
@ -70,12 +65,7 @@ pretty_assertions = "1"
|
|||
tempfile = "3"
|
||||
|
||||
[features]
|
||||
default = ["x11"]
|
||||
debug = []
|
||||
# Linux front-end providers — exactly one selected at build time.
|
||||
# build.sh detects the running WM and sets this for you.
|
||||
x11 = ["dep:x11-dl"]
|
||||
wayland = []
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
|
|
@ -85,4 +75,4 @@ windows = { version = "0.58", features = [
|
|||
] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
x11-dl = { version = "2", optional = true }
|
||||
x11-dl = "2"
|
||||
|
|
|
|||
45
README.md
45
README.md
|
|
@ -1,21 +1,18 @@
|
|||
# Layers
|
||||
|
||||
An image maniplulation program-style logical-layer panel for KiCad 10.
|
||||
|
||||
You can add or subtract your selection to and from the layer, or to and from the board. You can hide and unhide each layer cleanly. You can rename/colour/lock/annotate, sort and organize the layers, a layer can also have childen.
|
||||
|
||||
There are also boolean operations (merge/subtract/intersect) between layers (logical), snap to grid, and more to come. Operations are all composed together as one commit and can easily be undone with Edit->Undo or equivalent.
|
||||
A Photoshop-style logical-layer panel for KiCad 10. Group items into named logical layers,
|
||||
hide them as a unit, rename/colour/lock/annotate, run set ops (merge/subtract/intersect),
|
||||
snap to grid, snapshot and restore — all live against an open `pcbnew` session over KiCad's
|
||||
IPC API.
|
||||
|
||||
Rendering of in-panel board previews is powered by [Siphon](https://git.else-if.org/jess/Siphon),
|
||||
a pure-Rust KiCad-board-to-vectors crate. You would not technically need KiCad installed to produce the vectors, it is a completely isolated utility which makes no calls at all to kicad-cli.
|
||||
a pure-Rust KiCad-board-to-vectors crate. No shell-outs, no `kicad-cli`.
|
||||
|
||||
## Install
|
||||
|
||||
Every platform installs into KiCad 10's *10.0/plugins* directory (on Linux it'll also drop into 11.0, 12.0, etc. if you've already got those, native or flatpak, both). I'm 90% sure that's where they are supposed to go.
|
||||
|
||||
`cargo xtask install` picks the right script and runs it. `cargo xtask build` stages without installing. `install-linux` / `install-macos` / `install-windows` force a specific platform if you ever need that.
|
||||
|
||||
Close KiCad completely prior to running the install script or KiCad might crash, and you might lose work. Close KiCad completely when you update the plugin too. Turns out there's a lot of differences between python and Rust plugins, though maybe the better distiction is IPC plugins in general. I am yet to make a python IPC plugin though, so I can't say for certain.
|
||||
Every platform installs into KiCad 10's 3rd-party plugin directory. After install, open
|
||||
KiCad's **Plugin and Content Manager** and rescan, or relaunch KiCad; the Layers toolbar
|
||||
button appears in `pcbnew`.
|
||||
|
||||
### macOS
|
||||
|
||||
|
|
@ -25,14 +22,16 @@ to regenerate icons from `resources/Layers.svg`).
|
|||
```sh
|
||||
git clone https://git.else-if.org/jess/Layers
|
||||
cd Layers
|
||||
cargo xtask install
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
Installs to `~/Documents/KiCad/10.0/plugins/com.jesshunter.layers/`
|
||||
Installs to `~/Documents/KiCad/10.0/plugins/com.jesshunter.layers/`.
|
||||
|
||||
### Windows (10 / 11, ARM64 or x86_64)
|
||||
|
||||
Grrr. It works. I'm still too tickled to talk about it. It did some real spiteful shit to me. I refused to install visual studio on my VM, and it never forgave me for that.
|
||||
A single native Rust binary — no .NET, no WinUI, no Visual Studio. Mica backdrop
|
||||
on Windows 11 22H2+ via `DwmSetWindowAttribute`, falls back gracefully on older
|
||||
Windows. Both ARM64 and x86_64 hosts work the same way.
|
||||
|
||||
#### 1. Install MSYS2
|
||||
|
||||
|
|
@ -44,7 +43,7 @@ Download and run the installer from <https://www.msys2.org>. Accept the defaults
|
|||
In the MSYS2 shell:
|
||||
|
||||
```sh
|
||||
# ARM64 (Apple M[x], Snapdragon X, Surface Pro 11, etc.)
|
||||
# ARM64 (Snapdragon X, Surface Pro 11, etc.)
|
||||
pacman -Syu
|
||||
pacman -S --needed \
|
||||
mingw-w64-clang-aarch64-toolchain \
|
||||
|
|
@ -109,12 +108,14 @@ Close and reopen every PowerShell / cmd window after this so they pick up the ch
|
|||
```bat
|
||||
git clone https://git.else-if.org/jess/Layers
|
||||
cd Layers
|
||||
cargo xtask install
|
||||
install.bat
|
||||
```
|
||||
|
||||
Installs to `%USERPROFILE%\Documents\KiCad\10.0\plugins\com.jesshunter.layers\`
|
||||
(or the OneDrive-redirected equivalent).
|
||||
|
||||
No external runtime required — the exe is statically linked against the MSYS2
|
||||
clangarm64 / ucrt64 toolchain. Just ship the folder.
|
||||
|
||||
### Linux — Debian / Ubuntu / Pop!_OS (apt)
|
||||
|
||||
|
|
@ -130,10 +131,10 @@ source "$HOME/.cargo/env"
|
|||
|
||||
git clone https://git.else-if.org/jess/Layers
|
||||
cd Layers
|
||||
cargo xtask install
|
||||
./scripts/install-linux.sh
|
||||
```
|
||||
|
||||
Installs to `~/.local/share/kicad/10.0/plugins/com.jesshunter.layers/`
|
||||
Installs to `~/.local/share/kicad/10.0/3rdparty/plugins/com.jesshunter.layers/`.
|
||||
|
||||
### Linux — Arch / Manjaro (pacman)
|
||||
|
||||
|
|
@ -148,14 +149,10 @@ source "$HOME/.cargo/env"
|
|||
|
||||
git clone https://git.else-if.org/jess/Layers
|
||||
cd Layers
|
||||
cargo xtask install
|
||||
./scripts/install-linux.sh
|
||||
```
|
||||
|
||||
Installs to `~/.local/share/kicad/10.0/plugins/com.jesshunter.layers/`
|
||||
|
||||
In both cases, it checks both the standard directory and the Flatpak directory. If you for some reason have both, it places it in both. If for some reason you have both but only want it in one of them, you can just delete it from the one you don't want it in.
|
||||
|
||||
The Flatpak plugins dir is ` ~/.var/app/org.kicad.KiCad/data/kicad/10.0/plugins/com.jesshunter.layers/`
|
||||
Installs to `~/.local/share/kicad/10.0/3rdparty/plugins/com.jesshunter.layers/`.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
render_plugin_json() {
|
||||
|
|
@ -30,26 +30,7 @@ if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/resources/Layers.svg"
|
|||
done
|
||||
fi
|
||||
|
||||
# Detect the running window-manager / display protocol to pick the front-end
|
||||
# provider that gets compiled into the binary. Override with LAYERS_FEATURES.
|
||||
detect_wm_feature() {
|
||||
if [ -n "${LAYERS_FEATURES:-}" ]; then
|
||||
echo "$LAYERS_FEATURES"
|
||||
return
|
||||
fi
|
||||
# Native Wayland session — use the wayland provider (winit xdg-toplevel,
|
||||
# render transparency, user pins on top via compositor).
|
||||
if [ -n "${WAYLAND_DISPLAY:-}" ]; then
|
||||
echo "wayland"
|
||||
return
|
||||
fi
|
||||
# X11 session or no WAYLAND_DISPLAY: use the X11 provider.
|
||||
echo "x11"
|
||||
}
|
||||
|
||||
FEATURE="$(detect_wm_feature)"
|
||||
echo "build: front-end provider = $FEATURE (XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP:-<unset>}, WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-<unset>})"
|
||||
cargo build --release --bin layers --no-default-features --features "$FEATURE"
|
||||
cargo build --release --bin layers
|
||||
|
||||
cp "$ROOT/target/release/layers" "$APPDIR/Layers"
|
||||
chmod +x "$APPDIR/Layers"
|
||||
|
|
@ -6,7 +6,7 @@ rem ARM64 -> aarch64-pc-windows-gnullvm (clangarm64 llvm-mingw toolchain)
|
|||
rem x86_64 -> x86_64-pc-windows-gnu (ucrt64 gcc-mingw toolchain)
|
||||
rem LAYERS_RUST_TARGET overrides.
|
||||
|
||||
pushd %~dp0..\..
|
||||
pushd %~dp0
|
||||
set "ROOT=%CD%"
|
||||
|
||||
if defined LAYERS_RUST_TARGET (
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
pushd %~dp0..\..
|
||||
pushd %~dp0
|
||||
set "ROOT=%CD%"
|
||||
|
||||
call "%ROOT%\scripts\windows\build.bat"
|
||||
call "%ROOT%\build.bat"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo build failed
|
||||
popd
|
||||
|
|
@ -15,8 +15,11 @@ rem Resolve the real Documents folder (respects OneDrive / folder redirection).
|
|||
for /f "usebackq delims=" %%i in (`powershell -NoProfile -Command "[Environment]::GetFolderPath('MyDocuments')"`) do set "DOCS=%%i"
|
||||
if not defined DOCS set "DOCS=%USERPROFILE%\Documents"
|
||||
|
||||
set "INSTALL_DIR=%DOCS%\KiCad\10.0\plugins\com.jesshunter.layers"
|
||||
set "INSTALL_ROOT=%DOCS%\KiCad\10.0\plugins"
|
||||
set "INSTALL_DIR=%INSTALL_ROOT%\com.jesshunter.layers"
|
||||
if not exist "%INSTALL_ROOT%" mkdir "%INSTALL_ROOT%" >nul 2>&1
|
||||
|
||||
rem Preserve user state (state/, cache/, logs/, settings.json) across reinstall.
|
||||
if exist "%INSTALL_DIR%\bin" rmdir /s /q "%INSTALL_DIR%\bin"
|
||||
if exist "%INSTALL_DIR%\resources" rmdir /s /q "%INSTALL_DIR%\resources"
|
||||
if exist "%INSTALL_DIR%\plugin.json" del /q "%INSTALL_DIR%\plugin.json"
|
||||
|
|
@ -25,6 +28,8 @@ if exist "%INSTALL_DIR%\LICENCE" del /q "%INSTALL_DIR%\LICENCE"
|
|||
set "STAGE=%ROOT%\build\bin\com.jesshunter.layers"
|
||||
xcopy /e /i /y /q "%STAGE%" "%INSTALL_DIR%" >nul
|
||||
|
||||
echo.
|
||||
echo installed: %INSTALL_DIR%
|
||||
echo.
|
||||
popd
|
||||
endlocal
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) : ;;
|
||||
*) echo "use scripts/install.sh on macOS or install.bat on Windows" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
bash "$ROOT/build-linux.sh"
|
||||
|
||||
INSTALL_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/kicad/10.0/3rdparty/plugins"
|
||||
INSTALL_DIR="$INSTALL_ROOT/com.jesshunter.layers"
|
||||
mkdir -p "$INSTALL_ROOT"
|
||||
|
||||
# Preserve user state (state/, cache/, logs/, settings.json) across reinstall.
|
||||
rm -rf "$INSTALL_DIR/bin" "$INSTALL_DIR/resources" "$INSTALL_DIR/plugin.json" "$INSTALL_DIR/LICENCE"
|
||||
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/resources"
|
||||
|
||||
STAGE="$ROOT/build/bin/com.jesshunter.layers"
|
||||
cp -R "$STAGE/bin/." "$INSTALL_DIR/bin/"
|
||||
[ -f "$STAGE/plugin.json" ] && cp "$STAGE/plugin.json" "$INSTALL_DIR/plugin.json"
|
||||
[ -f "$STAGE/LICENCE" ] && cp "$STAGE/LICENCE" "$INSTALL_DIR/LICENCE"
|
||||
cp -R "$STAGE/resources/." "$INSTALL_DIR/resources/"
|
||||
|
||||
echo "installed: $INSTALL_DIR"
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask install" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
STAGE="$ROOT/build/bin/com.jesshunter.layers"
|
||||
PLUGIN_ID="com.jesshunter.layers"
|
||||
|
||||
# Installs into every existing KiCad >=10 plugins dir — native and flatpak.
|
||||
# Does not mkdir kicad/<ver>/plugins; KiCad creates it on first launch.
|
||||
install_to_kicad_root() {
|
||||
local kicad_root="$1"
|
||||
[ -d "$kicad_root" ] || return 1
|
||||
local installed=0
|
||||
for ver_dir in "$kicad_root"/*/; do
|
||||
local ver
|
||||
ver="$(basename "$ver_dir")"
|
||||
[[ "$ver" =~ ^([0-9]+)\.0$ ]] || continue
|
||||
(( ${BASH_REMATCH[1]} >= 10 )) || continue
|
||||
local plugins_dir="${ver_dir}plugins"
|
||||
[ -d "$plugins_dir" ] || continue
|
||||
|
||||
local target="$plugins_dir/$PLUGIN_ID"
|
||||
rm -rf "$target/bin" "$target/resources" "$target/plugin.json" "$target/LICENCE"
|
||||
mkdir -p "$target/bin" "$target/resources"
|
||||
cp -R "$STAGE/bin/." "$target/bin/"
|
||||
[ -f "$STAGE/plugin.json" ] && cp "$STAGE/plugin.json" "$target/plugin.json"
|
||||
[ -f "$STAGE/LICENCE" ] && cp "$STAGE/LICENCE" "$target/LICENCE"
|
||||
cp -R "$STAGE/resources/." "$target/resources/"
|
||||
echo "installed: $target"
|
||||
installed=1
|
||||
done
|
||||
return $((1 - installed))
|
||||
}
|
||||
|
||||
NATIVE_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/kicad"
|
||||
FLATPAK_ROOT="$HOME/.var/app/org.kicad.KiCad/data/kicad"
|
||||
|
||||
ok=0
|
||||
|
||||
# Native install: build with the front-end matching the host's WM (build.sh
|
||||
# autodetects unless LAYERS_FEATURES is set).
|
||||
if [ -d "$NATIVE_ROOT" ]; then
|
||||
bash "$ROOT/scripts/linux/build.sh"
|
||||
install_to_kicad_root "$NATIVE_ROOT" && ok=1 || true
|
||||
fi
|
||||
|
||||
# Flatpak install: force the X11 front-end.
|
||||
if [ -d "$FLATPAK_ROOT" ]; then
|
||||
LAYERS_FEATURES=x11 bash "$ROOT/scripts/linux/build.sh"
|
||||
install_to_kicad_root "$FLATPAK_ROOT" && ok=1 || true
|
||||
fi
|
||||
|
||||
if [ "$ok" -eq 0 ]; then
|
||||
echo "no KiCad 10+ install detected. checked:" >&2
|
||||
echo " $NATIVE_ROOT/<ver>/plugins/" >&2
|
||||
echo " $FLATPAK_ROOT/<ver>/plugins/" >&2
|
||||
echo "launch KiCad once first so it creates its data dirs, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
# Renders plugin.json.in -> $2 with @ENTRYPOINT@ replaced by $1.
|
||||
render_plugin_json() {
|
||||
local entrypoint="$1"
|
||||
local out="$2"
|
||||
local in="$ROOT/plugin.json.in"
|
||||
if [ ! -f "$in" ]; then
|
||||
echo "ERROR: $in not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
sed "s|@ENTRYPOINT@|$entrypoint|g" "$in" > "$out"
|
||||
}
|
||||
|
||||
STAGE="$ROOT/build/bin/com.jesshunter.layers"
|
||||
APP="$STAGE/bin/Layers.app"
|
||||
CONTENTS="$APP/Contents"
|
||||
MACOS_DIR="$CONTENTS/MacOS"
|
||||
RESOURCES_DIR="$CONTENTS/Resources"
|
||||
|
||||
rm -rf "$STAGE"
|
||||
mkdir -p "$STAGE/bin" "$STAGE/resources" "$MACOS_DIR" "$RESOURCES_DIR"
|
||||
|
||||
# Icon PNGs for the KiCad toolbar — rendered straight from Layers.svg.
|
||||
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/resources/Layers.svg" ]; then
|
||||
for size in 24 48; do
|
||||
rsvg-convert --width "$size" --height "$size" \
|
||||
"$ROOT/resources/Layers.svg" -o "$ROOT/resources/icon-${size}.png"
|
||||
done
|
||||
fi
|
||||
|
||||
# Local path overrides for sibling checkouts (kicad-ipc-rs).
|
||||
# Injected only while this script runs; Cargo.toml stays clean for commits.
|
||||
CARGO_TOML="$ROOT/Cargo.toml"
|
||||
CARGO_BAK=""
|
||||
PATCH_APPLIED=0
|
||||
if [ -d "$ROOT/../kicad-ipc-rs" ] || [ -d "$ROOT/../Siphon" ]; then
|
||||
CARGO_BAK="$(mktemp "${TMPDIR:-/tmp}/Cargo.toml.orig.XXXXXX")"
|
||||
cp "$CARGO_TOML" "$CARGO_BAK"
|
||||
{
|
||||
if [ -d "$ROOT/../kicad-ipc-rs" ]; then
|
||||
printf '\n[patch."https://git.else-if.org/jess/kicad-ipc-rs.git"]\n'
|
||||
printf 'kicad-ipc-rs = { path = "../kicad-ipc-rs" }\n'
|
||||
echo "using local ../kicad-ipc-rs (patch injected transiently)" >&2
|
||||
fi
|
||||
if [ -d "$ROOT/../Siphon" ]; then
|
||||
printf '\n[patch."https://git.else-if.org/jess/Siphon.git"]\n'
|
||||
printf 'siphon = { path = "../Siphon" }\n'
|
||||
echo "using local ../Siphon (patch injected transiently)" >&2
|
||||
fi
|
||||
} >> "$CARGO_TOML"
|
||||
PATCH_APPLIED=1
|
||||
trap 'if [ $PATCH_APPLIED -eq 1 ] && [ -f "$CARGO_BAK" ]; then cp "$CARGO_BAK" "$CARGO_TOML"; rm -f "$CARGO_BAK"; fi' EXIT
|
||||
fi
|
||||
|
||||
# Rust staticlib.
|
||||
cargo build --release
|
||||
RUST_LIB="$ROOT/target/release"
|
||||
if [ ! -f "$RUST_LIB/liblayers.a" ]; then
|
||||
echo "ERROR: liblayers.a not found at $RUST_LIB" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SDK="$(xcrun --show-sdk-path)"
|
||||
RUST_FLAGS=(-import-objc-header "$ROOT/include/layers.h" -L "$RUST_LIB" -llayers)
|
||||
|
||||
# Swift shell → Layers.app/Contents/MacOS/Layers
|
||||
swiftc \
|
||||
-target arm64-apple-macosx14.0 \
|
||||
-sdk "$SDK" \
|
||||
"${RUST_FLAGS[@]}" \
|
||||
-framework Cocoa \
|
||||
-framework Metal \
|
||||
-framework MetalKit \
|
||||
-framework QuartzCore \
|
||||
-framework CoreGraphics \
|
||||
-framework CoreFoundation \
|
||||
-framework CoreVideo \
|
||||
-O \
|
||||
-o "$MACOS_DIR/Layers" \
|
||||
"$ROOT"/src/*.swift
|
||||
|
||||
cp "$ROOT/Info.plist" "$CONTENTS/Info.plist"
|
||||
|
||||
# Adhoc sign so Gatekeeper lets it launch.
|
||||
codesign --force --sign - "$APP"
|
||||
|
||||
# KiCad plugin payload alongside the .app bundle.
|
||||
render_plugin_json "bin/Layers.app/Contents/MacOS/Layers" "$STAGE/plugin.json"
|
||||
cp -R "$ROOT/resources/." "$STAGE/resources/"
|
||||
if [ -f "$ROOT/LICENCE" ]; then
|
||||
cp "$ROOT/LICENCE" "$STAGE/LICENCE"
|
||||
fi
|
||||
|
||||
echo "staged: $STAGE"
|
||||
echo "app: $APP"
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask install" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
INSTALL_DIR="$HOME/Documents/KiCad/10.0/plugins/com.jesshunter.layers"
|
||||
|
||||
bash "$ROOT/scripts/macos/build.sh"
|
||||
|
||||
rm -rf "$INSTALL_DIR/bin" "$INSTALL_DIR/resources" "$INSTALL_DIR/plugin.json" "$INSTALL_DIR/LICENCE"
|
||||
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/resources"
|
||||
|
||||
STAGE="$ROOT/build/bin/com.jesshunter.layers"
|
||||
cp -R "$STAGE/bin/Layers.app" "$INSTALL_DIR/bin/Layers.app"
|
||||
cp "$STAGE/plugin.json" "$INSTALL_DIR/plugin.json"
|
||||
cp -R "$STAGE/resources/." "$INSTALL_DIR/resources/"
|
||||
[ -f "$STAGE/LICENCE" ] && cp "$STAGE/LICENCE" "$INSTALL_DIR/LICENCE"
|
||||
|
||||
echo "installed: $INSTALL_DIR"
|
||||
|
|
@ -29,7 +29,7 @@ struct LayersColors {
|
|||
return result
|
||||
}
|
||||
|
||||
/// key=value extractor for the subset.
|
||||
/// Minimal key=value extractor for the flat subset we care about.
|
||||
/// Returns dotted "section.key" → numeric value.
|
||||
private static func flatten(_ source: String) -> [String: Double] {
|
||||
var out: [String: Double] = [:]
|
||||
|
|
|
|||
18
src/app.rs
18
src/app.rs
|
|
@ -264,21 +264,8 @@ impl App {
|
|||
}
|
||||
crate::ipc::Event::OpenDocuments(paths) => {
|
||||
if let Some(pcb) = paths.into_iter().next() {
|
||||
let canonical = pcb
|
||||
.canonicalize()
|
||||
.ok()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|| "<canonicalize-failed>".to_string());
|
||||
let hash = crate::paths::project_hash(&pcb);
|
||||
let state_path = crate::paths::state_path(&hash);
|
||||
tracing::info!(
|
||||
pcb_raw = %pcb.display(),
|
||||
pcb_canonical = %canonical,
|
||||
project_hash = %hash,
|
||||
state_path = %state_path.display(),
|
||||
state_exists = state_path.exists(),
|
||||
"open document: resolving project state"
|
||||
);
|
||||
self.current_project = Some(ProjectContext {
|
||||
project_hash: hash,
|
||||
kicad_pcb_path: pcb,
|
||||
|
|
@ -287,11 +274,6 @@ impl App {
|
|||
self.has_pruned_orphans = false;
|
||||
match crate::layers::persistence::load(&state_path) {
|
||||
Ok(tree) => {
|
||||
tracing::info!(
|
||||
state_path = %state_path.display(),
|
||||
layer_count = tree.layers.len(),
|
||||
"loaded layer tree from disk"
|
||||
);
|
||||
self.layer_tree = tree;
|
||||
self.rebuild_panel_content();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,80 +27,15 @@ fn main() {
|
|||
|
||||
tracing::info!(
|
||||
plugin_root = ?plugin_root,
|
||||
data_dir = ?layers::paths::data_dir(),
|
||||
log_dir = ?layers::paths::log_dir(),
|
||||
"layers shell: starting native event loop",
|
||||
);
|
||||
|
||||
log_session_env();
|
||||
tracing::info!(provider = PROVIDER_NAME, "shell route: provider chosen at build time");
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
tracing::info!(provider = PROVIDER_NAME, "wayland: using winit xdg-toplevel (compositor provides pin-on-top via title bar)");
|
||||
|
||||
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)) {
|
||||
tracing::error!("layers shell panicked: {e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
const PROVIDER_NAME: &str = "linux/wayland (winit + xdg-toplevel)";
|
||||
#[cfg(all(target_os = "linux", feature = "x11", not(feature = "wayland")))]
|
||||
const PROVIDER_NAME: &str = "linux/x11 (winit + xlib)";
|
||||
#[cfg(all(target_os = "linux", not(feature = "x11"), not(feature = "wayland")))]
|
||||
compile_error!("Linux build requires exactly one of: --features x11, --features wayland");
|
||||
#[cfg(target_os = "macos")]
|
||||
const PROVIDER_NAME: &str = "macos (winit + appkit)";
|
||||
#[cfg(target_os = "windows")]
|
||||
const PROVIDER_NAME: &str = "windows (winit + dwm)";
|
||||
|
||||
fn log_window_kind(handle: &raw_window_handle::RawWindowHandle) -> &'static str {
|
||||
use raw_window_handle::RawWindowHandle::*;
|
||||
match handle {
|
||||
Wayland(_) => "wayland",
|
||||
Xlib(_) => "xlib",
|
||||
Xcb(_) => "xcb",
|
||||
Win32(_) => "win32",
|
||||
AppKit(_) => "appkit",
|
||||
UiKit(_) => "uikit",
|
||||
_ => "other",
|
||||
}
|
||||
}
|
||||
|
||||
fn log_display_kind(handle: &raw_window_handle::RawDisplayHandle) -> &'static str {
|
||||
use raw_window_handle::RawDisplayHandle::*;
|
||||
match handle {
|
||||
Wayland(_) => "wayland",
|
||||
Xlib(_) => "xlib",
|
||||
Xcb(_) => "xcb",
|
||||
Windows(_) => "windows",
|
||||
AppKit(_) => "appkit",
|
||||
UiKit(_) => "uikit",
|
||||
_ => "other",
|
||||
}
|
||||
}
|
||||
|
||||
fn log_session_env() {
|
||||
let keys = [
|
||||
"WAYLAND_DISPLAY",
|
||||
"DISPLAY",
|
||||
"XDG_SESSION_TYPE",
|
||||
"XDG_CURRENT_DESKTOP",
|
||||
"XDG_SESSION_DESKTOP",
|
||||
"DESKTOP_SESSION",
|
||||
"WINIT_UNIX_BACKEND",
|
||||
"GDK_BACKEND",
|
||||
"QT_QPA_PLATFORM",
|
||||
];
|
||||
for k in keys {
|
||||
match std::env::var(k) {
|
||||
Ok(v) => tracing::info!(env = k, value = %v, "session env"),
|
||||
Err(_) => tracing::info!(env = k, value = "<unset>", "session env"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() {
|
||||
let event_loop = EventLoop::new().expect("winit: create event loop");
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
|
|
@ -152,21 +87,8 @@ impl ApplicationHandler for ShellApp {
|
|||
.with_inner_size(LogicalSize::new(DEFAULT_LOGICAL_SIZE.0, DEFAULT_LOGICAL_SIZE.1))
|
||||
.with_min_inner_size(LogicalSize::new(MIN_LOGICAL_SIZE.0, MIN_LOGICAL_SIZE.1))
|
||||
.with_transparent(true)
|
||||
.with_decorations(true);
|
||||
|
||||
// On Wayland, always-on-top is the user's choice (only) I mulled it over. Why fight intent?
|
||||
// Who am I to decide what's important? I have thought of a way to do it though, I think. So if any
|
||||
// wayland users DO end up asking me for this feature, I'll add it.
|
||||
// I'd add a cfg to the board change poll that only exists on wayland detected builds, it would use
|
||||
// xdg activation to steal back top level. I think it would work, but it might be too
|
||||
// invasive if it has to take focus as well as return to top. All around, wayland just isn't
|
||||
// meant to do this. Sorry Ubuntu/Gnome/Pop_OS! users. If you use wayland and have any
|
||||
// ideas,
|
||||
// please do let me know.
|
||||
|
||||
// On other platforms it is set programmatically.
|
||||
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
|
||||
let attrs = attrs.with_window_level(WindowLevel::AlwaysOnTop);
|
||||
.with_decorations(true)
|
||||
.with_window_level(WindowLevel::AlwaysOnTop);
|
||||
|
||||
let window = event_loop
|
||||
.create_window(attrs)
|
||||
|
|
@ -188,12 +110,6 @@ impl ApplicationHandler for ShellApp {
|
|||
.expect("winit: display handle")
|
||||
.as_raw();
|
||||
|
||||
tracing::info!(
|
||||
window_handle = log_window_kind(&raw_window),
|
||||
display_handle = log_display_kind(&raw_display),
|
||||
"winit window up"
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"creating viewport handle: {}x{} @ {}",
|
||||
inner.width,
|
||||
|
|
@ -412,7 +328,7 @@ fn set_window_alpha(window: &Window, alpha: f32) {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "x11"))]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_window_alpha(window: &Window, alpha: f32) {
|
||||
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||
use std::ffi::CString;
|
||||
|
|
@ -463,12 +379,7 @@ fn set_window_alpha(window: &Window, alpha: f32) {
|
|||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[cfg(any(
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
all(target_os = "linux", not(feature = "x11")),
|
||||
))]
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
fn set_window_alpha(_window: &Window, _alpha: f32) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
|
|||
60
src/ffi.rs
60
src/ffi.rs
|
|
@ -427,6 +427,7 @@ fn finalise_handle(
|
|||
let caps = surface.get_capabilities(&adapter);
|
||||
let format = caps.formats.first().copied()?;
|
||||
|
||||
// PostMultiplied so the Metal layer's alpha blends against the transparent NSWindow.
|
||||
let alpha_mode = if caps
|
||||
.alpha_modes
|
||||
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
|
||||
|
|
@ -444,27 +445,6 @@ fn finalise_handle(
|
|||
.unwrap_or(wgpu::CompositeAlphaMode::Auto)
|
||||
};
|
||||
|
||||
let adapter_info = adapter.get_info();
|
||||
tracing::info!(
|
||||
backend = ?adapter_info.backend,
|
||||
device_name = %adapter_info.name,
|
||||
format = ?format,
|
||||
all_formats = ?caps.formats,
|
||||
all_alpha_modes = ?caps.alpha_modes,
|
||||
chosen_alpha_mode = ?alpha_mode,
|
||||
"wgpu surface configured"
|
||||
);
|
||||
if matches!(
|
||||
alpha_mode,
|
||||
wgpu::CompositeAlphaMode::Opaque | wgpu::CompositeAlphaMode::Auto
|
||||
) {
|
||||
tracing::warn!(
|
||||
"wgpu chose {alpha_mode:?} — surface won't be transparent. \
|
||||
X11 needs PreMultiplied/PostMultiplied. Try a different wgpu backend (set WGPU_BACKEND=gl or =vulkan) \
|
||||
or check that the WM has a compositor running."
|
||||
);
|
||||
}
|
||||
|
||||
surface.configure(
|
||||
&device,
|
||||
&wgpu::SurfaceConfiguration {
|
||||
|
|
@ -609,12 +589,8 @@ fn render(handle: &mut ViewportHandle) {
|
|||
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
|
||||
handle.cache = ui.into_cache();
|
||||
|
||||
// macOS/Windows/X11: the OS fades the composited surface via window-level alpha.
|
||||
// Wayland: no window-level opacity; clear with alpha=0 for transparent background.
|
||||
// background; the UI content renders opaque on top.
|
||||
let mut background = crate::ui::theme::compositor_clear();
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
{ background.a = 0.0; }
|
||||
// NSWindow.alphaValue in Swift fades the composited surface; keep this opaque.
|
||||
let background = crate::ui::theme::compositor_clear();
|
||||
handle.renderer.present(
|
||||
Some(background),
|
||||
handle.format,
|
||||
|
|
@ -642,23 +618,13 @@ fn persist_if_due(app: &mut App) {
|
|||
return;
|
||||
}
|
||||
let state_path = paths::state_path(&project.project_hash);
|
||||
let layer_count = app.layer_tree.layers.len();
|
||||
match crate::layers::persistence::save(&app.layer_tree, &state_path) {
|
||||
Ok(()) => {
|
||||
tracing::debug!(
|
||||
state_path = %state_path.display(),
|
||||
layer_count,
|
||||
"saved layer tree"
|
||||
);
|
||||
app.dirty = false;
|
||||
app.last_save = Some(std::time::Instant::now());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
state_path = %state_path.display(),
|
||||
layer_count,
|
||||
"layer state save failed: {e}"
|
||||
);
|
||||
tracing::warn!("layer state save failed: {e}");
|
||||
app.last_save = Some(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
|
|
@ -705,28 +671,10 @@ pub fn init_native_shell(plugin_root: Option<&std::path::Path>) -> anyhow::Resul
|
|||
paths::create_dirs_if_missing()?;
|
||||
let _ = init_logging_file_only();
|
||||
dump_invocation_context_file_only();
|
||||
probe_data_dir_writable();
|
||||
crate::ui::colors::init(plugin_root);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn probe_data_dir_writable() {
|
||||
let dir = paths::data_dir();
|
||||
let probe = dir.join(".write-probe");
|
||||
match std::fs::write(&probe, b"ok") {
|
||||
Ok(()) => {
|
||||
let _ = std::fs::remove_file(&probe);
|
||||
tracing::info!(data_dir = %dir.display(), "data dir is writable");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
data_dir = %dir.display(),
|
||||
"data dir NOT writable: {e} — state and logs won't persist; check flatpak --filesystem=home"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logging_file_only() -> anyhow::Result<()> {
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
|
|
|
|||
13
src/paths.rs
13
src/paths.rs
|
|
@ -5,7 +5,6 @@ use sha2::{Digest, Sha256};
|
|||
|
||||
const PLUGIN_ID: &str = "com.jesshunter.layers";
|
||||
const KICAD_MAJOR_DIR: &str = "KiCad/10.0/plugins";
|
||||
const USER_DATA_DOTDIR: &str = ".layers";
|
||||
|
||||
pub fn plugin_data_dir() -> PathBuf {
|
||||
plugin_root()
|
||||
|
|
@ -15,18 +14,8 @@ pub fn plugin_dir() -> PathBuf {
|
|||
plugin_root()
|
||||
}
|
||||
|
||||
/// User-writable runtime data root: logs, state, cache, settings, runtime sockets.
|
||||
/// Decoupled from the plugin install location so a flatpak KiCad (which sandboxes
|
||||
/// its own data dirs) still writes somewhere accessible from a normal shell.
|
||||
pub fn data_dir() -> PathBuf {
|
||||
if let Ok(v) = std::env::var("LAYERS_DATA_DIR") {
|
||||
if !v.is_empty() {
|
||||
return PathBuf::from(v);
|
||||
}
|
||||
}
|
||||
dirs::home_dir()
|
||||
.map(|h| h.join(USER_DATA_DOTDIR))
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp").join(USER_DATA_DOTDIR))
|
||||
plugin_root()
|
||||
}
|
||||
|
||||
pub fn state_dir() -> PathBuf {
|
||||
|
|
|
|||
|
|
@ -1,655 +0,0 @@
|
|||
use crate::ffi::ViewportHandle;
|
||||
use raw_window_handle::{
|
||||
RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
|
||||
};
|
||||
use smithay_client_toolkit::{
|
||||
compositor::{CompositorHandler, CompositorState},
|
||||
delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
|
||||
delegate_registry, delegate_seat,
|
||||
output::{OutputHandler, OutputState},
|
||||
registry::{ProvidesRegistryState, RegistryState},
|
||||
registry_handlers,
|
||||
seat::{
|
||||
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers},
|
||||
pointer::{PointerEvent, PointerEventKind, PointerHandler},
|
||||
Capability, SeatHandler, SeatState,
|
||||
},
|
||||
shell::{
|
||||
wlr_layer::{
|
||||
Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
|
||||
LayerSurfaceConfigure,
|
||||
},
|
||||
WaylandSurface,
|
||||
},
|
||||
};
|
||||
use std::ptr::NonNull;
|
||||
use wayland_client::{
|
||||
globals::{registry_queue_init, GlobalList},
|
||||
protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
|
||||
Connection, Proxy, QueueHandle,
|
||||
};
|
||||
|
||||
const DEFAULT_LOGICAL_SIZE: (u32, u32) = (480, 640);
|
||||
const MIN_LOGICAL_SIZE: (u32, u32) = (380, 220);
|
||||
const ANCHOR_MARGIN: i32 = 24;
|
||||
|
||||
/// Run the Wayland (sctk + wlr-layer-shell) front end. Returns `true` if it took
|
||||
/// over the session, `false` if init failed (wayland socket unreachable, no
|
||||
/// layer-shell, etc.) so the caller can fall through to the winit path. Hard
|
||||
/// exiting here would silently kill the plugin process inside KiCad — which the
|
||||
/// user sees as "didn't load."
|
||||
pub fn try_run() -> bool {
|
||||
tracing::info!("wayland: try_run entered");
|
||||
|
||||
let conn = match probe_connection() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"wayland: no reachable compositor socket; falling through to winit"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let (globals, mut event_queue) = match registry_queue_init::<State>(&conn) {
|
||||
Ok(pair) => {
|
||||
tracing::info!("wayland: registry_queue_init OK");
|
||||
pair
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("wayland: registry init failed ({e}); falling through to winit");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let qh = event_queue.handle();
|
||||
|
||||
log_globals(&globals);
|
||||
|
||||
let compositor = match CompositorState::bind(&globals, &qh) {
|
||||
Ok(c) => {
|
||||
tracing::info!("wayland: bound wl_compositor");
|
||||
c
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("wayland: wl_compositor missing ({e}); falling through to winit");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let layer_shell = match LayerShell::bind(&globals, &qh) {
|
||||
Ok(l) => {
|
||||
tracing::info!("wayland: bound zwlr_layer_shell_v1");
|
||||
l
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"wayland: zwlr_layer_shell_v1 not advertised ({e}); falling through to winit"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let surface = compositor.create_surface(&qh);
|
||||
let layer = layer_shell.create_layer_surface(
|
||||
&qh,
|
||||
surface,
|
||||
Layer::Overlay,
|
||||
Some("layers"),
|
||||
None,
|
||||
);
|
||||
layer.set_anchor(Anchor::TOP | Anchor::RIGHT);
|
||||
layer.set_size(DEFAULT_LOGICAL_SIZE.0, DEFAULT_LOGICAL_SIZE.1);
|
||||
layer.set_margin(ANCHOR_MARGIN, ANCHOR_MARGIN, 0, 0);
|
||||
layer.set_keyboard_interactivity(KeyboardInteractivity::OnDemand);
|
||||
layer.commit();
|
||||
tracing::info!(
|
||||
anchor = "TOP|RIGHT",
|
||||
size = ?(DEFAULT_LOGICAL_SIZE.0, DEFAULT_LOGICAL_SIZE.1),
|
||||
margin_top = ANCHOR_MARGIN,
|
||||
margin_right = ANCHOR_MARGIN,
|
||||
"wayland: layer surface committed (waiting for first configure)"
|
||||
);
|
||||
|
||||
let raw_display = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(
|
||||
match NonNull::new(conn.backend().display_ptr() as *mut _) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
tracing::warn!("wayland: null display ptr; falling through to winit");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
));
|
||||
let raw_window = RawWindowHandle::Wayland(WaylandWindowHandle::new(
|
||||
match NonNull::new(layer.wl_surface().id().as_ptr() as *mut _) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
tracing::warn!("wayland: null surface ptr; falling through to winit");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let mut state = State {
|
||||
registry_state: RegistryState::new(&globals),
|
||||
seat_state: SeatState::new(&globals, &qh),
|
||||
output_state: OutputState::new(&globals, &qh),
|
||||
viewport: None,
|
||||
layer,
|
||||
keyboard: None,
|
||||
pointer: None,
|
||||
modifiers: Modifiers::default(),
|
||||
last_pointer: (0.0, 0.0),
|
||||
width: DEFAULT_LOGICAL_SIZE.0,
|
||||
height: DEFAULT_LOGICAL_SIZE.1,
|
||||
scale: 1,
|
||||
first_configure: true,
|
||||
raw_display,
|
||||
raw_window,
|
||||
focused: false,
|
||||
hovered: false,
|
||||
exit: false,
|
||||
};
|
||||
|
||||
tracing::info!("wayland: layer-shell up, entering dispatch loop");
|
||||
while !state.exit {
|
||||
if let Err(e) = event_queue.blocking_dispatch(&mut state) {
|
||||
tracing::error!("wayland dispatch: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
struct State {
|
||||
registry_state: RegistryState,
|
||||
seat_state: SeatState,
|
||||
output_state: OutputState,
|
||||
/// Held above `layer` so the wgpu surface drops before the wl_surface.
|
||||
viewport: Option<ViewportHandle>,
|
||||
layer: LayerSurface,
|
||||
keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||
pointer: Option<wl_pointer::WlPointer>,
|
||||
modifiers: Modifiers,
|
||||
last_pointer: (f32, f32),
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale: i32,
|
||||
first_configure: bool,
|
||||
raw_display: RawDisplayHandle,
|
||||
raw_window: RawWindowHandle,
|
||||
focused: bool,
|
||||
hovered: bool,
|
||||
exit: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn ensure_viewport(&mut self) {
|
||||
if self.viewport.is_some() {
|
||||
return;
|
||||
}
|
||||
let scale = self.scale.max(1) as f32;
|
||||
let logical_w = self.width.max(MIN_LOGICAL_SIZE.0) as f32;
|
||||
let logical_h = self.height.max(MIN_LOGICAL_SIZE.1) as f32;
|
||||
match ViewportHandle::new_from_raw(self.raw_window, self.raw_display, logical_w, logical_h, scale) {
|
||||
Some(h) => {
|
||||
tracing::info!(
|
||||
"wayland: viewport ready ({}x{} @ {})",
|
||||
logical_w,
|
||||
logical_h,
|
||||
scale
|
||||
);
|
||||
self.viewport = Some(h);
|
||||
}
|
||||
None => tracing::error!("wayland: ViewportHandle::new_from_raw returned None"),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, qh: &QueueHandle<Self>) {
|
||||
if let Some(vp) = self.viewport.as_mut() {
|
||||
vp.set_window_alpha(crate::ui::theme::window_alpha(self.focused, self.hovered));
|
||||
vp.render_frame();
|
||||
}
|
||||
let surface = self.layer.wl_surface();
|
||||
surface.frame(qh, surface.clone());
|
||||
self.layer.commit();
|
||||
}
|
||||
}
|
||||
|
||||
impl CompositorHandler for State {
|
||||
fn scale_factor_changed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
new_factor: i32,
|
||||
) {
|
||||
if self.scale == new_factor {
|
||||
return;
|
||||
}
|
||||
self.scale = new_factor;
|
||||
self.layer.wl_surface().set_buffer_scale(new_factor);
|
||||
if let Some(vp) = self.viewport.as_mut() {
|
||||
vp.resize_px(self.width as f32, self.height as f32, new_factor as f32);
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_changed(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_surface::WlSurface,
|
||||
_: wl_output::Transform,
|
||||
) {
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
_: &wl_surface::WlSurface,
|
||||
_time: u32,
|
||||
) {
|
||||
self.render(qh);
|
||||
}
|
||||
|
||||
fn surface_enter(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_surface::WlSurface,
|
||||
_: &wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
|
||||
fn surface_leave(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_surface::WlSurface,
|
||||
_: &wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputHandler for State {
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
|
||||
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
|
||||
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
|
||||
}
|
||||
|
||||
impl LayerShellHandler for State {
|
||||
fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
|
||||
self.exit = true;
|
||||
}
|
||||
|
||||
fn configure(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
_layer: &LayerSurface,
|
||||
configure: LayerSurfaceConfigure,
|
||||
serial: u32,
|
||||
) {
|
||||
let (w, h) = configure.new_size;
|
||||
let new_w = if w == 0 { self.width } else { w };
|
||||
let new_h = if h == 0 { self.height } else { h };
|
||||
let resized = new_w != self.width || new_h != self.height;
|
||||
tracing::info!(
|
||||
serial,
|
||||
requested_w = w,
|
||||
requested_h = h,
|
||||
applied_w = new_w,
|
||||
applied_h = new_h,
|
||||
first = self.first_configure,
|
||||
"wayland: configure"
|
||||
);
|
||||
self.width = new_w;
|
||||
self.height = new_h;
|
||||
|
||||
self.ensure_viewport();
|
||||
if resized {
|
||||
if let Some(vp) = self.viewport.as_mut() {
|
||||
vp.resize_px(self.width as f32, self.height as f32, self.scale.max(1) as f32);
|
||||
}
|
||||
}
|
||||
|
||||
if self.first_configure {
|
||||
self.first_configure = false;
|
||||
tracing::info!("wayland: first configure → kicking initial render");
|
||||
self.render(qh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SeatHandler for State {
|
||||
fn seat_state(&mut self) -> &mut SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
seat: wl_seat::WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
if capability == Capability::Keyboard && self.keyboard.is_none() {
|
||||
if let Ok(kb) = self.seat_state.get_keyboard(qh, &seat, None) {
|
||||
self.keyboard = Some(kb);
|
||||
}
|
||||
}
|
||||
if capability == Capability::Pointer && self.pointer.is_none() {
|
||||
if let Ok(p) = self.seat_state.get_pointer(qh, &seat) {
|
||||
self.pointer = Some(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: wl_seat::WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
if capability == Capability::Keyboard {
|
||||
if let Some(k) = self.keyboard.take() {
|
||||
k.release();
|
||||
}
|
||||
}
|
||||
if capability == Capability::Pointer {
|
||||
if let Some(p) = self.pointer.take() {
|
||||
p.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
||||
}
|
||||
|
||||
impl KeyboardHandler for State {
|
||||
fn enter(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_: u32,
|
||||
_: &[u32],
|
||||
_keysyms: &[Keysym],
|
||||
) {
|
||||
self.focused = true;
|
||||
}
|
||||
|
||||
fn leave(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_: u32,
|
||||
) {
|
||||
self.focused = false;
|
||||
}
|
||||
|
||||
fn press_key(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: u32,
|
||||
event: KeyEvent,
|
||||
) {
|
||||
if let Some(vp) = self.viewport.as_mut() {
|
||||
let (named, utf8) = map_key_event(&event);
|
||||
vp.push_key_event(named, utf8, encode_modifiers(&self.modifiers), true);
|
||||
}
|
||||
}
|
||||
|
||||
fn release_key(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: u32,
|
||||
event: KeyEvent,
|
||||
) {
|
||||
if let Some(vp) = self.viewport.as_mut() {
|
||||
let (named, utf8) = map_key_event(&event);
|
||||
vp.push_key_event(named, utf8, encode_modifiers(&self.modifiers), false);
|
||||
}
|
||||
}
|
||||
|
||||
fn repeat_key(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: u32,
|
||||
_event: KeyEvent,
|
||||
) {
|
||||
}
|
||||
|
||||
fn update_modifiers(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_serial: u32,
|
||||
modifiers: Modifiers,
|
||||
_raw: RawModifiers,
|
||||
_layout: u32,
|
||||
) {
|
||||
self.modifiers = modifiers;
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerHandler for State {
|
||||
fn pointer_frame(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_pointer::WlPointer,
|
||||
events: &[PointerEvent],
|
||||
) {
|
||||
let Some(vp) = self.viewport.as_mut() else {
|
||||
return;
|
||||
};
|
||||
for event in events {
|
||||
if &event.surface != self.layer.wl_surface() {
|
||||
continue;
|
||||
}
|
||||
let (px, py) = event.position;
|
||||
let lx = px as f32;
|
||||
let ly = py as f32;
|
||||
self.last_pointer = (lx, ly);
|
||||
match event.kind {
|
||||
PointerEventKind::Enter { .. } => {
|
||||
self.hovered = true;
|
||||
vp.push_mouse_move(lx, ly);
|
||||
}
|
||||
PointerEventKind::Leave { .. } => {
|
||||
self.hovered = false;
|
||||
vp.push_mouse_left();
|
||||
}
|
||||
PointerEventKind::Motion { .. } => {
|
||||
vp.push_mouse_move(lx, ly);
|
||||
}
|
||||
PointerEventKind::Press { button, .. } => {
|
||||
if let Some(code) = map_button(button) {
|
||||
vp.push_mouse_button(lx, ly, code, true);
|
||||
}
|
||||
}
|
||||
PointerEventKind::Release { button, .. } => {
|
||||
if let Some(code) = map_button(button) {
|
||||
vp.push_mouse_button(lx, ly, code, false);
|
||||
}
|
||||
}
|
||||
PointerEventKind::Axis {
|
||||
horizontal,
|
||||
vertical,
|
||||
..
|
||||
} => {
|
||||
let dx = -(horizontal.absolute as f32);
|
||||
let dy = -(vertical.absolute as f32);
|
||||
vp.push_mouse_scroll(lx, ly, dx, dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a wayland socket and connect, regardless of whether `WAYLAND_DISPLAY` is set.
|
||||
/// Inside flatpak the env var is often stripped even when the socket itself is bind-
|
||||
/// mounted into the sandbox at `$XDG_RUNTIME_DIR/wayland-*`. Lists candidates
|
||||
/// and tries each in order.
|
||||
fn probe_connection() -> Option<Connection> {
|
||||
let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from);
|
||||
tracing::info!(
|
||||
xdg_runtime_dir = ?runtime_dir,
|
||||
wayland_display_env = ?std::env::var_os("WAYLAND_DISPLAY"),
|
||||
"wayland: probing for compositor socket"
|
||||
);
|
||||
|
||||
if let Ok(c) = Connection::connect_to_env() {
|
||||
tracing::info!("wayland: connected via $WAYLAND_DISPLAY");
|
||||
return Some(c);
|
||||
}
|
||||
|
||||
let Some(dir) = runtime_dir else {
|
||||
tracing::warn!("wayland: $XDG_RUNTIME_DIR is unset; cannot scan for sockets");
|
||||
return None;
|
||||
};
|
||||
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
tracing::warn!(dir = %dir.display(), "wayland: cannot list runtime dir: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let Some(name_str) = name.to_str() else { continue; };
|
||||
if !name_str.starts_with("wayland-") {
|
||||
continue;
|
||||
}
|
||||
// Skip lock files; only the socket matters.
|
||||
if name_str.ends_with(".lock") {
|
||||
continue;
|
||||
}
|
||||
candidates.push(name_str.to_string());
|
||||
}
|
||||
candidates.sort();
|
||||
tracing::info!(?candidates, "wayland: socket candidates in $XDG_RUNTIME_DIR");
|
||||
|
||||
for name in &candidates {
|
||||
// Connection::connect_to_env reads $WAYLAND_DISPLAY; set it for one attempt.
|
||||
std::env::set_var("WAYLAND_DISPLAY", name);
|
||||
match Connection::connect_to_env() {
|
||||
Ok(c) => {
|
||||
tracing::info!(socket = name, "wayland: connected via probed socket");
|
||||
return Some(c);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!(socket = name, "wayland: probe failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn log_globals(globals: &GlobalList) {
|
||||
let list = globals.contents().clone_list();
|
||||
tracing::info!("wayland: registry advertises {} globals", list.len());
|
||||
let mut seen_layer_shell = false;
|
||||
let mut seen_compositor = false;
|
||||
let mut seen_seat = false;
|
||||
for g in &list {
|
||||
tracing::debug!(
|
||||
interface = %g.interface,
|
||||
name = g.name,
|
||||
version = g.version,
|
||||
"wayland global"
|
||||
);
|
||||
match g.interface.as_str() {
|
||||
"zwlr_layer_shell_v1" => seen_layer_shell = true,
|
||||
"wl_compositor" => seen_compositor = true,
|
||||
"wl_seat" => seen_seat = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
tracing::info!(
|
||||
layer_shell = seen_layer_shell,
|
||||
compositor = seen_compositor,
|
||||
seat = seen_seat,
|
||||
"wayland: protocol summary"
|
||||
);
|
||||
}
|
||||
|
||||
fn map_button(button: u32) -> Option<u32> {
|
||||
match button {
|
||||
0x110 => Some(0), // BTN_LEFT
|
||||
0x111 => Some(1), // BTN_RIGHT
|
||||
0x112 => Some(2), // BTN_MIDDLE
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_key_event(event: &KeyEvent) -> (u32, Option<String>) {
|
||||
let named = match event.keysym {
|
||||
Keysym::Return => 1,
|
||||
Keysym::Escape => 2,
|
||||
Keysym::BackSpace => 3,
|
||||
Keysym::Tab => 4,
|
||||
Keysym::Left => 5,
|
||||
Keysym::Right => 6,
|
||||
Keysym::Up => 7,
|
||||
Keysym::Down => 8,
|
||||
Keysym::Delete => 9,
|
||||
Keysym::Home => 10,
|
||||
Keysym::End => 11,
|
||||
_ => 0,
|
||||
};
|
||||
let utf8 = if named == 0 { event.utf8.clone() } else { None };
|
||||
(named, utf8)
|
||||
}
|
||||
|
||||
fn encode_modifiers(mods: &Modifiers) -> u32 {
|
||||
let mut bits = 0u32;
|
||||
if mods.shift {
|
||||
bits |= 1;
|
||||
}
|
||||
if mods.ctrl {
|
||||
bits |= 2;
|
||||
}
|
||||
if mods.alt {
|
||||
bits |= 4;
|
||||
}
|
||||
if mods.logo {
|
||||
bits |= 8;
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
delegate_compositor!(State);
|
||||
delegate_output!(State);
|
||||
delegate_seat!(State);
|
||||
delegate_keyboard!(State);
|
||||
delegate_pointer!(State);
|
||||
delegate_layer!(State);
|
||||
delegate_registry!(State);
|
||||
|
||||
impl ProvidesRegistryState for State {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
registry_handlers![OutputState, SeatState];
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[package]
|
||||
name = "xtask"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "xtask"
|
||||
path = "src/main.rs"
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, ExitCode};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
let cmd = args.first().map(String::as_str).unwrap_or("");
|
||||
|
||||
let (action, platform) = match cmd {
|
||||
"build" => ("build", current_platform()),
|
||||
"install" => ("install", current_platform()),
|
||||
"build-linux" => ("build", "linux"),
|
||||
"install-linux" => ("install", "linux"),
|
||||
"build-macos" => ("build", "macos"),
|
||||
"install-macos" => ("install", "macos"),
|
||||
"build-windows" => ("build", "windows"),
|
||||
"install-windows" => ("install", "windows"),
|
||||
_ => {
|
||||
eprintln!("usage: cargo xtask <command>");
|
||||
eprintln!();
|
||||
eprintln!("commands:");
|
||||
eprintln!(" build build for the current platform");
|
||||
eprintln!(" install build + install for the current platform");
|
||||
eprintln!(" build-<platform> force a specific platform (linux | macos | windows)");
|
||||
eprintln!(" install-<platform> force a specific platform");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
|
||||
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("xtask manifest must have a parent")
|
||||
.to_path_buf();
|
||||
|
||||
let (script, runner) = match platform {
|
||||
"windows" => (
|
||||
repo_root.join(format!("scripts/windows/{action}.bat")),
|
||||
vec!["cmd", "/c"],
|
||||
),
|
||||
"linux" | "macos" => (
|
||||
repo_root.join(format!("scripts/{platform}/{action}.sh")),
|
||||
vec!["bash"],
|
||||
),
|
||||
other => {
|
||||
eprintln!("unknown platform: {other}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
|
||||
if !script.exists() {
|
||||
eprintln!("script not found: {}", script.display());
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
|
||||
eprintln!("→ {} {}", runner.join(" "), script.display());
|
||||
|
||||
let mut command = Command::new(runner[0]);
|
||||
for arg in &runner[1..] {
|
||||
command.arg(arg);
|
||||
}
|
||||
command.arg(&script);
|
||||
command.current_dir(&repo_root);
|
||||
|
||||
match command.status() {
|
||||
Ok(status) if status.success() => ExitCode::SUCCESS,
|
||||
Ok(status) => ExitCode::from(status.code().unwrap_or(1) as u8),
|
||||
Err(e) => {
|
||||
eprintln!("failed to run {}: {e}", script.display());
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_platform() -> &'static str {
|
||||
match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "macos",
|
||||
"windows" => "windows",
|
||||
other => {
|
||||
eprintln!("unsupported OS: {other}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue