Windows - dot net for shell
This commit is contained in:
parent
49a87ca309
commit
1ea7cfc632
|
|
@ -1,2 +1,5 @@
|
|||
/target
|
||||
.DS_Store
|
||||
|
||||
# Generated at build time from plugin.json.in by each platform's build script.
|
||||
/plugin.json
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ rust-version = "1.75"
|
|||
[lib]
|
||||
name = "layers"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
# staticlib: macOS Swift shell links it directly.
|
||||
# cdylib: Windows (layers.dll, loaded by WinUI 3 shell) and Linux (liblayers.so, loaded by GTK shell).
|
||||
# rlib: in-tree bin target.
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "layers"
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -29,8 +29,9 @@ Installs to `~/Documents/KiCad/10.0/plugins/com.jesshunter.layers/`.
|
|||
|
||||
### Windows (10 / 11, ARM64 or x86_64)
|
||||
|
||||
This path uses **MSYS2 + MinGW** instead of full Visual Studio Build Tools — smaller
|
||||
footprint, works the same on ARM64 and x86_64.
|
||||
The Windows shell is a **WinUI 3** (.NET 8) app that hosts the Rust renderer through
|
||||
a `SwapChainPanel`. The Rust side builds as a cdylib via **MSYS2 + MinGW** — no full
|
||||
Visual Studio Build Tools required. Both ARM64 and x86_64 hosts work the same way.
|
||||
|
||||
#### 1. Install MSYS2
|
||||
|
||||
|
|
@ -100,7 +101,18 @@ PATH. Run this **once** in PowerShell (elevated shell not required):
|
|||
|
||||
Close and reopen every PowerShell / cmd window after this so they pick up the change.
|
||||
|
||||
#### 5. Build + install
|
||||
#### 5. Install .NET 8 SDK + Windows App SDK workload
|
||||
|
||||
In PowerShell:
|
||||
|
||||
```powershell
|
||||
winget install Microsoft.DotNet.SDK.8
|
||||
```
|
||||
|
||||
The Windows App SDK is pulled in automatically via NuGet when the shell builds — no extra
|
||||
workload install needed.
|
||||
|
||||
#### 6. Build + install
|
||||
|
||||
```bat
|
||||
git clone https://git.else-if.org/jess/Layers
|
||||
|
|
@ -111,6 +123,10 @@ install.bat
|
|||
Installs to `%USERPROFILE%\Documents\KiCad\10.0\plugins\com.jesshunter.layers\`
|
||||
(or the OneDrive-redirected equivalent).
|
||||
|
||||
Runtime requirement on the target machine: **.NET 8 Desktop Runtime**. `winget install
|
||||
Microsoft.DotNet.DesktopRuntime.8` — the installer's ~100 MB and common on modern
|
||||
Windows boxes.
|
||||
|
||||
Optional: `winget install GNOME.librsvg` gives you `rsvg-convert` for regenerating icons
|
||||
from `resources/Layers.svg`. Not required to build.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@ set -euo pipefail
|
|||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
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"
|
||||
APPDIR="$STAGE/bin"
|
||||
|
||||
|
|
@ -23,8 +34,8 @@ cargo build --release --bin layers
|
|||
|
||||
cp "$ROOT/target/release/layers" "$APPDIR/Layers"
|
||||
chmod +x "$APPDIR/Layers"
|
||||
[ -f "$ROOT/plugin.json" ] && cp "$ROOT/plugin.json" "$STAGE/plugin.json"
|
||||
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$STAGE/LICENCE"
|
||||
render_plugin_json "bin/Layers" "$STAGE/plugin.json"
|
||||
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$STAGE/LICENCE"
|
||||
cp -r "$ROOT/resources/." "$STAGE/resources/"
|
||||
|
||||
echo "staged: $STAGE"
|
||||
|
|
|
|||
63
build.bat
63
build.bat
|
|
@ -1,10 +1,15 @@
|
|||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
rem Build the Windows shell. Picks the GNU/MinGW target based on host arch:
|
||||
rem ARM64 -> aarch64-pc-windows-gnullvm (clangarm64 llvm-mingw toolchain)
|
||||
rem x86_64 -> x86_64-pc-windows-gnu (ucrt64 gcc-mingw toolchain)
|
||||
rem A user override via LAYERS_RUST_TARGET wins.
|
||||
rem Build the Windows shell — Rust cdylib + C# WinUI 3 host.
|
||||
rem
|
||||
rem Rust target (MSYS2 llvm-mingw / gcc-mingw):
|
||||
rem ARM64 -> aarch64-pc-windows-gnullvm
|
||||
rem x86_64 -> x86_64-pc-windows-gnu
|
||||
rem .NET publish target:
|
||||
rem ARM64 -> win-arm64
|
||||
rem x86_64 -> win-x64
|
||||
rem Override via LAYERS_RUST_TARGET / LAYERS_NET_RID if needed.
|
||||
|
||||
pushd %~dp0
|
||||
set "ROOT=%CD%"
|
||||
|
|
@ -16,16 +21,23 @@ if defined LAYERS_RUST_TARGET (
|
|||
) else (
|
||||
set "RUST_TARGET=x86_64-pc-windows-gnu"
|
||||
)
|
||||
echo target: %RUST_TARGET%
|
||||
if defined LAYERS_NET_RID (
|
||||
set "NET_RID=%LAYERS_NET_RID%"
|
||||
) else if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
||||
set "NET_RID=win-arm64"
|
||||
) else (
|
||||
set "NET_RID=win-x64"
|
||||
)
|
||||
echo rust target: %RUST_TARGET%
|
||||
echo .NET RID: %NET_RID%
|
||||
|
||||
set "STAGE=%ROOT%\build\bin\com.jesshunter.layers"
|
||||
set "APPDIR=%STAGE%\bin"
|
||||
|
||||
if exist "%STAGE%" rmdir /s /q "%STAGE%"
|
||||
mkdir "%APPDIR%" >nul 2>&1
|
||||
mkdir "%STAGE%\resources" >nul 2>&1
|
||||
|
||||
rem Optional: render icons from the SVG if rsvg-convert is on PATH (winget install GNOME.librsvg).
|
||||
rem Icons from the SVG via rsvg-convert (optional).
|
||||
where rsvg-convert >nul 2>&1
|
||||
if %ERRORLEVEL% equ 0 (
|
||||
for %%s in (24 48 128 256) do (
|
||||
|
|
@ -35,9 +47,7 @@ if %ERRORLEVEL% equ 0 (
|
|||
)
|
||||
)
|
||||
|
||||
rem nng 1.x (bundled by nng-sys 1.4.0-rc.0) has two type mismatches that become hard
|
||||
rem errors under clang 16+ (which ships in MSYS2 clangarm64). Downgrade them so the
|
||||
rem vendored C builds cleanly. Harmless on older clang / gcc too.
|
||||
rem nng 1.x triggers clang 16+ hard errors on type-incompatibilities; downgrade to warnings.
|
||||
set "NNG_RELAX=-Wno-error=incompatible-pointer-types -Wno-error=incompatible-function-pointer-types -Wno-incompatible-pointer-types -Wno-incompatible-function-pointer-types"
|
||||
if defined CFLAGS (
|
||||
set "CFLAGS=%CFLAGS% %NNG_RELAX%"
|
||||
|
|
@ -45,18 +55,43 @@ if defined CFLAGS (
|
|||
set "CFLAGS=%NNG_RELAX%"
|
||||
)
|
||||
|
||||
cargo build --release --bin layers --target %RUST_TARGET%
|
||||
rem --- 1. Rust cdylib (layers.dll) ---
|
||||
cargo build --release --lib --target %RUST_TARGET%
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo cargo build failed
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
copy /y "%ROOT%\target\%RUST_TARGET%\release\layers.exe" "%APPDIR%\Layers.exe" >nul
|
||||
if exist "%ROOT%\plugin.json" copy /y "%ROOT%\plugin.json" "%STAGE%\plugin.json" >nul
|
||||
if exist "%ROOT%\LICENCE" copy /y "%ROOT%\LICENCE" "%STAGE%\LICENCE" >nul
|
||||
rem --- 2. C# WinUI 3 shell ---
|
||||
pushd "%ROOT%\shell\windows\LayersShell"
|
||||
dotnet publish -c Release -r %NET_RID% --self-contained false -p:WindowsPackageType=None -p:PublishReadyToRun=true
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo dotnet publish failed
|
||||
popd
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
popd
|
||||
|
||||
rem --- 3. Stage ---
|
||||
set "CSHARP_OUT=%ROOT%\shell\windows\LayersShell\bin\Release\net8.0-windows10.0.19041.0\%NET_RID%\publish"
|
||||
xcopy /e /i /y /q "%CSHARP_OUT%" "%APPDIR%" >nul
|
||||
copy /y "%ROOT%\target\%RUST_TARGET%\release\layers.dll" "%APPDIR%\layers.dll" >nul
|
||||
|
||||
if exist "%ROOT%\LICENCE" copy /y "%ROOT%\LICENCE" "%STAGE%\LICENCE" >nul
|
||||
xcopy /e /i /y /q "%ROOT%\resources" "%STAGE%\resources" >nul
|
||||
|
||||
rem Render plugin.json.in -> stage\plugin.json with the shell exe as entrypoint.
|
||||
if not exist "%ROOT%\plugin.json.in" (
|
||||
echo ERROR: %ROOT%\plugin.json.in not found
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
powershell -NoProfile -Command ^
|
||||
"(Get-Content -Raw -LiteralPath '%ROOT%\plugin.json.in') -replace '@ENTRYPOINT@', 'bin/Layers.exe' ^| Set-Content -LiteralPath '%STAGE%\plugin.json' -NoNewline"
|
||||
|
||||
echo.
|
||||
echo staged: %STAGE%
|
||||
echo exe: %APPDIR%\Layers.exe
|
||||
popd
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ int32_t layers_startup(void);
|
|||
void layers_set_plugin_root(const char* path_utf8);
|
||||
|
||||
ViewportHandle* layers_create(void* nsview, float width, float height, float scale);
|
||||
/// Windows / WinUI 3 — host a Rust-rendered viewport in a SwapChainPanel.
|
||||
/// `swap_chain_panel` is the SwapChainPanel's native COM IInspectable pointer.
|
||||
ViewportHandle* layers_create_from_swap_chain_panel(void* swap_chain_panel, float width, float height, float scale);
|
||||
void layers_destroy(ViewportHandle* handle);
|
||||
void layers_render(ViewportHandle* handle);
|
||||
void layers_resize(ViewportHandle* handle, float width, float height, float scale);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"name": "Open Layers Panel",
|
||||
"description": "Opens the Layers plugin window. If already open, focuses the existing window.",
|
||||
"scopes": ["pcb"],
|
||||
"entrypoint": "bin/Layers.app/Contents/MacOS/Layers",
|
||||
"entrypoint": "@ENTRYPOINT@",
|
||||
"args": [],
|
||||
"show-button": true,
|
||||
"icons-light": ["resources/icon-24.png", "resources/icon-48.png"],
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application
|
||||
x:Class="LayersShell.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="AppBackground" Color="#0A0A0D"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace LayersShell;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private MainWindow? _window;
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LayersShell;
|
||||
|
||||
internal static class LayersNative
|
||||
{
|
||||
private const string Dll = "layers";
|
||||
|
||||
private static bool _startedUp;
|
||||
public static void StartupIfNeeded()
|
||||
{
|
||||
if (_startedUp) return;
|
||||
_startedUp = true;
|
||||
_ = layers_startup();
|
||||
}
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern int layers_startup();
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern void layers_set_plugin_root([MarshalAs(UnmanagedType.LPStr)] string path);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern IntPtr layers_create_from_swap_chain_panel(IntPtr swapChainPanel, float width, float height, float scale);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void layers_destroy(IntPtr handle);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void layers_render(IntPtr handle);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void layers_resize(IntPtr handle, float width, float height, float scale);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void layers_mouse_move(IntPtr handle, float x, float y);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void layers_mouse_left(IntPtr handle);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void layers_mouse_button(IntPtr handle, float x, float y, uint button, [MarshalAs(UnmanagedType.I1)] bool pressed);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void layers_mouse_scroll(IntPtr handle, float x, float y, float dx, float dy);
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern void layers_key_event(IntPtr handle, uint named, [MarshalAs(UnmanagedType.LPStr)] string? utf8, uint mods, ushort nativeKeycode, [MarshalAs(UnmanagedType.I1)] bool pressed);
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.ComImport]
|
||||
[System.Runtime.InteropServices.Guid("63aad0b8-7c24-40ff-85a8-640d944cc325")]
|
||||
[System.Runtime.InteropServices.InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface ISwapChainPanelNative
|
||||
{
|
||||
[PreserveSig]
|
||||
int SetSwapChain(IntPtr swapChain);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>LayersShell</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>false</EnableMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AssemblyName>Layers</AssemblyName>
|
||||
<ApplicationIcon>Assets/app.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- If NuGet complains "package not found", bump this to the newest 1.5.* or 1.6.* listed
|
||||
on https://www.nuget.org/packages/Microsoft.WindowsAppSDK. The API surface we use
|
||||
(MicaController, SwapChainPanel hosting) has been stable since 1.4. -->
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240607001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window
|
||||
x:Class="LayersShell.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Grid>
|
||||
<SwapChainPanel
|
||||
x:Name="RenderSurface"
|
||||
Background="Transparent"
|
||||
SizeChanged="RenderSurface_SizeChanged"
|
||||
PointerMoved="RenderSurface_PointerMoved"
|
||||
PointerPressed="RenderSurface_PointerPressed"
|
||||
PointerReleased="RenderSurface_PointerReleased"
|
||||
PointerWheelChanged="RenderSurface_PointerWheel"
|
||||
PointerEntered="RenderSurface_PointerEntered"
|
||||
PointerExited="RenderSurface_PointerExited"
|
||||
KeyDown="RenderSurface_KeyDown"
|
||||
KeyUp="RenderSurface_KeyUp"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.Graphics;
|
||||
using Windows.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace LayersShell;
|
||||
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
private IntPtr _handle = IntPtr.Zero;
|
||||
private MicaController? _mica;
|
||||
private SystemBackdropConfiguration? _backdropConfig;
|
||||
private IntPtr _hwnd = IntPtr.Zero;
|
||||
|
||||
private const double DefaultLogicalWidth = 480;
|
||||
private const double DefaultLogicalHeight = 640;
|
||||
|
||||
private bool _isFocused = true;
|
||||
private bool _isHovered = false;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Title = "Layers";
|
||||
_hwnd = WindowNative.GetWindowHandle(this);
|
||||
|
||||
var appWindow = AppWindow.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(_hwnd));
|
||||
if (appWindow is not null)
|
||||
{
|
||||
// Borderless chrome + topmost + default size.
|
||||
appWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
|
||||
if (appWindow.Presenter is OverlappedPresenter op)
|
||||
{
|
||||
op.IsAlwaysOnTop = true;
|
||||
op.IsResizable = true;
|
||||
op.IsMaximizable = false;
|
||||
op.IsMinimizable = true;
|
||||
op.SetBorderAndTitleBar(hasBorder: false, hasTitleBar: false);
|
||||
}
|
||||
appWindow.ResizeClient(new SizeInt32((int)DefaultLogicalWidth, (int)DefaultLogicalHeight));
|
||||
}
|
||||
|
||||
SetupMica();
|
||||
|
||||
Activated += OnActivated;
|
||||
Closed += OnClosed;
|
||||
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
PointToRust();
|
||||
LayersNative.StartupIfNeeded();
|
||||
CreateNativeHandle();
|
||||
StartRenderLoop();
|
||||
});
|
||||
}
|
||||
|
||||
private void SetupMica()
|
||||
{
|
||||
if (!MicaController.IsSupported())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_backdropConfig = new SystemBackdropConfiguration
|
||||
{
|
||||
IsInputActive = true,
|
||||
Theme = SystemBackdropTheme.Dark,
|
||||
};
|
||||
_mica = new MicaController
|
||||
{
|
||||
Kind = MicaKind.Base,
|
||||
};
|
||||
_mica.AddSystemBackdropTarget(this.As<Microsoft.UI.Composition.ICompositionSupportsSystemBackdrop>());
|
||||
_mica.SetSystemBackdropConfiguration(_backdropConfig);
|
||||
}
|
||||
|
||||
private void OnActivated(object sender, WindowActivatedEventArgs e)
|
||||
{
|
||||
_isFocused = e.WindowActivationState != WindowActivationState.Deactivated;
|
||||
if (_backdropConfig is not null)
|
||||
{
|
||||
_backdropConfig.IsInputActive = _isFocused;
|
||||
}
|
||||
ApplyFade();
|
||||
}
|
||||
|
||||
private void OnClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
_mica?.Dispose();
|
||||
_mica = null;
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
LayersNative.layers_destroy(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void PointToRust()
|
||||
{
|
||||
// Resolve <plugin_root> from the shell exe's parent/parent path: ...\bin\Layers.exe
|
||||
var exe = System.Reflection.Assembly.GetEntryAssembly()?.Location;
|
||||
if (string.IsNullOrEmpty(exe)) return;
|
||||
var binDir = Path.GetDirectoryName(exe) ?? string.Empty;
|
||||
var pluginRoot = Path.GetDirectoryName(binDir) ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(pluginRoot)) return;
|
||||
LayersNative.layers_set_plugin_root(pluginRoot);
|
||||
}
|
||||
|
||||
private void CreateNativeHandle()
|
||||
{
|
||||
if (_handle != IntPtr.Zero) return;
|
||||
var scale = (float)RenderSurface.CompositionScaleX;
|
||||
var width = (float)RenderSurface.ActualWidth;
|
||||
var height = (float)RenderSurface.ActualHeight;
|
||||
if (width <= 0 || height <= 0 || scale <= 0) return;
|
||||
|
||||
var native = RenderSurface.As<ISwapChainPanelNative>();
|
||||
var panelPtr = Marshal.GetIUnknownForObject(RenderSurface);
|
||||
try
|
||||
{
|
||||
_handle = LayersNative.layers_create_from_swap_chain_panel(panelPtr, width, height, scale);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.Release(panelPtr);
|
||||
}
|
||||
}
|
||||
|
||||
private Microsoft.UI.Xaml.DispatcherTimer? _renderTimer;
|
||||
private void StartRenderLoop()
|
||||
{
|
||||
_renderTimer = new Microsoft.UI.Xaml.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(1000.0 / 60.0),
|
||||
};
|
||||
_renderTimer.Tick += (_, __) =>
|
||||
{
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
LayersNative.layers_render(_handle);
|
||||
}
|
||||
};
|
||||
_renderTimer.Start();
|
||||
}
|
||||
|
||||
private void ApplyFade()
|
||||
{
|
||||
// Swift-shell parity: focused+hovered = 1.0, focused xor hovered = 0.5, neither = 0.1.
|
||||
double target = (_isFocused && _isHovered) ? 1.0
|
||||
: (_isFocused || _isHovered) ? 0.5
|
||||
: 0.1;
|
||||
var root = Content as FrameworkElement;
|
||||
if (root is not null)
|
||||
{
|
||||
root.Opacity = target;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Input routing ---
|
||||
|
||||
private void RenderSurface_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (_handle == IntPtr.Zero)
|
||||
{
|
||||
CreateNativeHandle();
|
||||
return;
|
||||
}
|
||||
var scale = (float)RenderSurface.CompositionScaleX;
|
||||
LayersNative.layers_resize(_handle, (float)e.NewSize.Width, (float)e.NewSize.Height, scale);
|
||||
}
|
||||
|
||||
private void RenderSurface_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (_handle == IntPtr.Zero) return;
|
||||
var p = e.GetCurrentPoint(RenderSurface).Position;
|
||||
LayersNative.layers_mouse_move(_handle, (float)p.X, (float)p.Y);
|
||||
}
|
||||
|
||||
private void RenderSurface_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
RenderSurface.Focus(FocusState.Programmatic);
|
||||
PushMouseButton(e, pressed: true);
|
||||
}
|
||||
|
||||
private void RenderSurface_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
PushMouseButton(e, pressed: false);
|
||||
}
|
||||
|
||||
private void PushMouseButton(PointerRoutedEventArgs e, bool pressed)
|
||||
{
|
||||
if (_handle == IntPtr.Zero) return;
|
||||
var p = e.GetCurrentPoint(RenderSurface);
|
||||
uint btn;
|
||||
var props = p.Properties;
|
||||
if (props.IsLeftButtonPressed || (!pressed && !props.IsRightButtonPressed && !props.IsMiddleButtonPressed)) btn = 0;
|
||||
else if (props.IsRightButtonPressed) btn = 1;
|
||||
else if (props.IsMiddleButtonPressed) btn = 2;
|
||||
else btn = 0;
|
||||
LayersNative.layers_mouse_button(_handle, (float)p.Position.X, (float)p.Position.Y, btn, pressed);
|
||||
}
|
||||
|
||||
private void RenderSurface_PointerWheel(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (_handle == IntPtr.Zero) return;
|
||||
var p = e.GetCurrentPoint(RenderSurface);
|
||||
// Wheel delta in Windows is multiples of 120 per notch; convert to pixel-ish deltas.
|
||||
var dy = (float)p.Properties.MouseWheelDelta / 3.0f;
|
||||
LayersNative.layers_mouse_scroll(_handle, (float)p.Position.X, (float)p.Position.Y, 0.0f, dy);
|
||||
}
|
||||
|
||||
private void RenderSurface_PointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
_isHovered = true;
|
||||
ApplyFade();
|
||||
}
|
||||
|
||||
private void RenderSurface_PointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
_isHovered = false;
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
LayersNative.layers_mouse_left(_handle);
|
||||
}
|
||||
ApplyFade();
|
||||
}
|
||||
|
||||
private void RenderSurface_KeyDown(object sender, KeyRoutedEventArgs e) => DispatchKey(e, pressed: true);
|
||||
private void RenderSurface_KeyUp(object sender, KeyRoutedEventArgs e) => DispatchKey(e, pressed: false);
|
||||
|
||||
private void DispatchKey(KeyRoutedEventArgs e, bool pressed)
|
||||
{
|
||||
if (_handle == IntPtr.Zero) return;
|
||||
uint named = WinKeyMap.MapVirtualKey(e.Key);
|
||||
string? text = named == 0 ? WinKeyMap.TextForKey(e.Key) : null;
|
||||
uint mods = WinKeyMap.CurrentModifiers();
|
||||
LayersNative.layers_key_event(_handle, named, text, mods, (ushort)e.Key, pressed);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
using System;
|
||||
using Microsoft.UI.Input;
|
||||
using Windows.System;
|
||||
|
||||
namespace LayersShell;
|
||||
|
||||
/// Maps WinUI virtual keys to the `layers_key_event` named-code + text protocol.
|
||||
/// named: 0=Character, 1=Enter, 2=Escape, 3=Backspace, 4=Tab,
|
||||
/// 5=ArrowLeft, 6=ArrowRight, 7=ArrowUp, 8=ArrowDown,
|
||||
/// 9=Delete, 10=Home, 11=End.
|
||||
/// mods: 1=Shift, 2=Ctrl, 4=Alt, 8=Logo (Win).
|
||||
internal static class WinKeyMap
|
||||
{
|
||||
public static uint MapVirtualKey(VirtualKey key) => key switch
|
||||
{
|
||||
VirtualKey.Enter => 1,
|
||||
VirtualKey.Escape => 2,
|
||||
VirtualKey.Back => 3,
|
||||
VirtualKey.Tab => 4,
|
||||
VirtualKey.Left => 5,
|
||||
VirtualKey.Right => 6,
|
||||
VirtualKey.Up => 7,
|
||||
VirtualKey.Down => 8,
|
||||
VirtualKey.Delete => 9,
|
||||
VirtualKey.Home => 10,
|
||||
VirtualKey.End => 11,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
public static string? TextForKey(VirtualKey key)
|
||||
{
|
||||
// Letters + digits + punctuation produced by the shell directly. Shift/altgr composition
|
||||
// is left to the iced layer's text_input which re-evaluates the character path.
|
||||
if (key >= VirtualKey.A && key <= VirtualKey.Z)
|
||||
{
|
||||
char c = (char)('a' + (int)(key - VirtualKey.A));
|
||||
return c.ToString();
|
||||
}
|
||||
if (key >= VirtualKey.Number0 && key <= VirtualKey.Number9)
|
||||
{
|
||||
char c = (char)('0' + (int)(key - VirtualKey.Number0));
|
||||
return c.ToString();
|
||||
}
|
||||
return key switch
|
||||
{
|
||||
VirtualKey.Space => " ",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public static uint CurrentModifiers()
|
||||
{
|
||||
uint m = 0;
|
||||
if ((InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift) & Windows.UI.Core.CoreVirtualKeyStates.Down) == Windows.UI.Core.CoreVirtualKeyStates.Down) m |= 1;
|
||||
if ((InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control) & Windows.UI.Core.CoreVirtualKeyStates.Down) == Windows.UI.Core.CoreVirtualKeyStates.Down) m |= 2;
|
||||
if ((InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu) & Windows.UI.Core.CoreVirtualKeyStates.Down) == Windows.UI.Core.CoreVirtualKeyStates.Down) m |= 4;
|
||||
if ((InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows) & Windows.UI.Core.CoreVirtualKeyStates.Down) == Windows.UI.Core.CoreVirtualKeyStates.Down) m |= 8;
|
||||
if ((InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows) & Windows.UI.Core.CoreVirtualKeyStates.Down) == Windows.UI.Core.CoreVirtualKeyStates.Down) m |= 8;
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="0.1.0.0" name="Layers.app"/>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
// Suppress the console window on Windows release builds. The plugin launches from a GUI
|
||||
// context (KiCad pcbnew's toolbar button), not a terminal — the black cmd window that
|
||||
// `subsystem = "console"` would spawn is noise. Debug builds keep the console so stderr
|
||||
// streams when launched from a terminal for diagnostics.
|
||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
|
@ -122,16 +128,13 @@ impl ApplicationHandler for ShellApp {
|
|||
}
|
||||
WindowEvent::Focused(focused) => {
|
||||
self.is_focused = focused;
|
||||
self.apply_window_alpha(layers::ui::colors::get());
|
||||
}
|
||||
WindowEvent::CursorEntered { .. } => {
|
||||
self.is_hovered = true;
|
||||
self.apply_window_alpha(layers::ui::colors::get());
|
||||
}
|
||||
WindowEvent::CursorLeft { .. } => {
|
||||
self.is_hovered = false;
|
||||
handle.push_mouse_left();
|
||||
self.apply_window_alpha(layers::ui::colors::get());
|
||||
window.request_redraw();
|
||||
}
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
|
|
|
|||
68
src/ffi.rs
68
src/ffi.rs
|
|
@ -167,6 +167,23 @@ pub extern "C" fn layers_create(
|
|||
Box::into_raw(Box::new(handle))
|
||||
}
|
||||
|
||||
/// Windows — create a viewport whose surface renders into a WinUI 3 SwapChainPanel.
|
||||
/// `swap_chain_panel` is the native IInspectable / COM pointer of the XAML element,
|
||||
/// obtained on the C# side via `SwapChainPanelNative.GetSwapChain` friends.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn layers_create_from_swap_chain_panel(
|
||||
swap_chain_panel: *mut c_void,
|
||||
width: f32,
|
||||
height: f32,
|
||||
scale: f32,
|
||||
) -> *mut ViewportHandle {
|
||||
let Some(handle) = create_handle_from_swap_chain_panel(swap_chain_panel, width, height, scale) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
Box::into_raw(Box::new(handle))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn layers_destroy(handle: *mut ViewportHandle) {
|
||||
if handle.is_null() {
|
||||
|
|
@ -378,22 +395,59 @@ fn create_handle_from_raw(
|
|||
height: f32,
|
||||
scale: f32,
|
||||
) -> Option<ViewportHandle> {
|
||||
let (instance, surface) = create_instance_and_surface(|instance| {
|
||||
let target = wgpu::SurfaceTargetUnsafe::RawHandle {
|
||||
raw_display_handle: raw_display,
|
||||
raw_window_handle: raw_window,
|
||||
};
|
||||
unsafe { instance.create_surface_unsafe(target).ok() }
|
||||
})?;
|
||||
finalise_handle(instance, surface, width, height, scale)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn create_handle_from_swap_chain_panel(
|
||||
swap_chain_panel: *mut c_void,
|
||||
width: f32,
|
||||
height: f32,
|
||||
scale: f32,
|
||||
) -> Option<ViewportHandle> {
|
||||
let ptr = NonNull::new(swap_chain_panel)?;
|
||||
let (instance, surface) = create_instance_and_surface(|instance| {
|
||||
let target = wgpu::SurfaceTargetUnsafe::SwapChainPanel(ptr.as_ptr());
|
||||
unsafe { instance.create_surface_unsafe(target).ok() }
|
||||
})?;
|
||||
finalise_handle(instance, surface, width, height, scale)
|
||||
}
|
||||
|
||||
fn create_instance_and_surface<F>(
|
||||
build_surface: F,
|
||||
) -> Option<(wgpu::Instance, wgpu::Surface<'static>)>
|
||||
where
|
||||
F: FnOnce(&wgpu::Instance) -> Option<wgpu::Surface<'static>>,
|
||||
{
|
||||
#[cfg(target_os = "macos")]
|
||||
let backends = wgpu::Backends::METAL;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
let backends = wgpu::Backends::DX12;
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
let backends = wgpu::Backends::all();
|
||||
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
..Default::default()
|
||||
});
|
||||
let surface = build_surface(&instance)?;
|
||||
Some((instance, surface))
|
||||
}
|
||||
|
||||
let target = wgpu::SurfaceTargetUnsafe::RawHandle {
|
||||
raw_display_handle: raw_display,
|
||||
raw_window_handle: raw_window,
|
||||
};
|
||||
let surface = unsafe { instance.create_surface_unsafe(target).ok()? };
|
||||
|
||||
fn finalise_handle(
|
||||
instance: wgpu::Instance,
|
||||
surface: wgpu::Surface<'static>,
|
||||
width: f32,
|
||||
height: f32,
|
||||
scale: f32,
|
||||
) -> Option<ViewportHandle> {
|
||||
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
compatible_surface: Some(&surface),
|
||||
|
|
|
|||
Loading…
Reference in New Issue