Layers/src/bin/layers_shell.rs

429 lines
15 KiB
Rust

#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
use std::path::PathBuf;
use std::time::Instant;
use layers::ffi::ViewportHandle;
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{Key, ModifiersState, NamedKey};
use winit::window::{Window, WindowAttributes, WindowId, WindowLevel};
const DEFAULT_LOGICAL_SIZE: (u32, u32) = (480, 640);
const MIN_LOGICAL_SIZE: (u32, u32) = (380, 220);
const ALPHA_FADE_MS: u128 = 150;
fn main() {
let plugin_root = discover_plugin_root();
// Tracing goes to a file; stderr is a deadlock risk when launched by KiCad.
let _ = layers::ffi::init_native_shell(plugin_root.as_deref());
std::panic::set_hook(Box::new(|info| {
tracing::error!("panic: {info}");
}));
tracing::info!(
plugin_root = ?plugin_root,
"layers shell: starting native event loop",
);
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)) {
tracing::error!("layers shell panicked: {e:?}");
std::process::exit(1);
}
}
fn run() {
let event_loop = EventLoop::new().expect("winit: create event loop");
event_loop.set_control_flow(ControlFlow::Wait);
let mut app = ShellApp::default();
if let Err(e) = event_loop.run_app(&mut app) {
tracing::error!("winit event loop: {e}");
std::process::exit(1);
}
}
struct ShellApp {
window: Option<Box<Window>>,
handle: Option<ViewportHandle>,
is_focused: bool,
is_hovered: bool,
modifiers: ModifiersState,
last_cursor: PhysicalPosition<f64>,
current_alpha: f32,
target_alpha: f32,
tween_from: f32,
tween_started: Option<Instant>,
}
impl Default for ShellApp {
fn default() -> Self {
Self {
window: None,
handle: None,
is_focused: false,
is_hovered: false,
modifiers: ModifiersState::empty(),
last_cursor: PhysicalPosition::new(0.0, 0.0),
current_alpha: 1.0,
target_alpha: 1.0,
tween_from: 1.0,
tween_started: None,
}
}
}
impl ApplicationHandler for ShellApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
tracing::info!("resumed: creating window");
let attrs = WindowAttributes::default()
.with_title("Layers")
.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)
.with_window_level(WindowLevel::AlwaysOnTop);
let window = event_loop
.create_window(attrs)
.expect("winit: create window");
let inner = window.inner_size();
let scale = window.scale_factor() as f32;
let window = Box::new(window);
let raw_window = window
.window_handle()
.expect("winit: window handle")
.as_raw();
#[cfg(target_os = "windows")]
apply_win11_chrome(&raw_window);
set_window_alpha(&window, self.current_alpha);
let raw_display = window
.display_handle()
.expect("winit: display handle")
.as_raw();
tracing::info!(
"creating viewport handle: {}x{} @ {}",
inner.width,
inner.height,
scale
);
let handle = match ViewportHandle::new_from_raw(
raw_window,
raw_display,
(inner.width as f32 / scale).max(1.0),
(inner.height as f32 / scale).max(1.0),
scale,
) {
Some(h) => h,
None => {
tracing::error!("new_from_raw returned None — wgpu surface failed");
event_loop.exit();
return;
}
};
tracing::info!("viewport handle ready");
self.window = Some(window);
self.handle = Some(handle);
self.refresh_target_alpha();
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_id: WindowId,
event: WindowEvent,
) {
let Some(window) = self.window.as_ref() else { return; };
let Some(handle) = self.handle.as_mut() else { return; };
let scale = window.scale_factor() as f32;
match event {
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::Resized(PhysicalSize { width, height }) => {
let w = (width as f32 / scale).max(1.0);
let h = (height as f32 / scale).max(1.0);
handle.resize_px(w, h, scale);
window.request_redraw();
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let size = window.inner_size();
let s = scale_factor as f32;
let w = (size.width as f32 / s).max(1.0);
let h = (size.height as f32 / s).max(1.0);
handle.resize_px(w, h, s);
window.request_redraw();
}
WindowEvent::Focused(focused) => {
self.is_focused = focused;
self.refresh_target_alpha();
}
WindowEvent::CursorEntered { .. } => {
self.is_hovered = true;
self.refresh_target_alpha();
}
WindowEvent::CursorLeft { .. } => {
self.is_hovered = false;
handle.push_mouse_left();
window.request_redraw();
self.refresh_target_alpha();
}
WindowEvent::CursorMoved { position, .. } => {
self.last_cursor = position;
let logical_x = position.x as f32 / scale;
let logical_y = position.y as f32 / scale;
handle.push_mouse_move(logical_x, logical_y);
window.request_redraw();
}
WindowEvent::MouseInput { state, button, .. } => {
let code = match button {
MouseButton::Left => 0,
MouseButton::Right => 1,
MouseButton::Middle => 2,
_ => return,
};
let pressed = matches!(state, ElementState::Pressed);
let logical_x = self.last_cursor.x as f32 / scale;
let logical_y = self.last_cursor.y as f32 / scale;
handle.push_mouse_button(logical_x, logical_y, code, pressed);
window.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
let (dx, dy) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x * 20.0, y * 20.0),
MouseScrollDelta::PixelDelta(p) => (p.x as f32, p.y as f32),
};
let logical_x = self.last_cursor.x as f32 / scale;
let logical_y = self.last_cursor.y as f32 / scale;
handle.push_mouse_scroll(logical_x, logical_y, dx, dy);
window.request_redraw();
}
WindowEvent::ModifiersChanged(mods) => {
self.modifiers = mods.state();
}
WindowEvent::KeyboardInput { event, .. } => {
let KeyEvent {
logical_key, state, text, ..
} = event;
let pressed = matches!(state, ElementState::Pressed);
let named = map_named_key(&logical_key);
let utf8 = match (named, &logical_key, &text) {
(0, Key::Character(s), _) => Some(s.to_string()),
(0, _, Some(s)) => Some(s.to_string()),
_ => None,
};
let mods_bits = encode_modifiers(self.modifiers);
handle.push_key_event(named, utf8, mods_bits, pressed);
window.request_redraw();
}
WindowEvent::RedrawRequested => {
handle.render_frame();
}
_ => {}
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
self.step_alpha_tween();
if let Some(window) = self.window.as_ref() {
window.request_redraw();
}
}
}
impl ShellApp {
fn refresh_target_alpha(&mut self) {
let colors = layers::ui::colors::get();
let target = if self.is_focused && self.is_hovered {
colors.window.alpha_focused_hovered
} else if self.is_focused || self.is_hovered {
colors.window.alpha_partial
} else {
colors.window.alpha_idle
};
if (target - self.target_alpha).abs() < 0.001 {
return;
}
self.tween_from = self.current_alpha;
self.target_alpha = target;
self.tween_started = Some(Instant::now());
}
fn step_alpha_tween(&mut self) {
let Some(started) = self.tween_started else { return; };
let Some(window) = self.window.as_ref() else { return; };
let prev = self.current_alpha;
let elapsed = started.elapsed().as_millis();
if elapsed >= ALPHA_FADE_MS {
self.current_alpha = self.target_alpha;
self.tween_started = None;
} else {
let t = elapsed as f32 / ALPHA_FADE_MS as f32;
self.current_alpha = self.tween_from + (self.target_alpha - self.tween_from) * t;
}
if (self.current_alpha - prev).abs() > 0.001 || self.tween_started.is_none() {
set_window_alpha(window, self.current_alpha);
}
}
}
fn map_named_key(key: &Key) -> u32 {
match key {
Key::Named(NamedKey::Enter) => 1,
Key::Named(NamedKey::Escape) => 2,
Key::Named(NamedKey::Backspace) => 3,
Key::Named(NamedKey::Tab) => 4,
Key::Named(NamedKey::ArrowLeft) => 5,
Key::Named(NamedKey::ArrowRight) => 6,
Key::Named(NamedKey::ArrowUp) => 7,
Key::Named(NamedKey::ArrowDown) => 8,
Key::Named(NamedKey::Delete) => 9,
Key::Named(NamedKey::Home) => 10,
Key::Named(NamedKey::End) => 11,
_ => 0,
}
}
fn encode_modifiers(mods: ModifiersState) -> u32 {
let mut bits = 0u32;
if mods.shift_key() { bits |= 1; }
if mods.control_key() { bits |= 2; }
if mods.alt_key() { bits |= 4; }
if mods.super_key() { bits |= 8; }
bits
}
#[cfg(target_os = "windows")]
fn set_window_alpha(window: &Window, alpha: f32) {
use raw_window_handle::RawWindowHandle;
use windows::Win32::Foundation::{COLORREF, HWND};
use windows::Win32::UI::WindowsAndMessaging::{
GetWindowLongPtrW, SetLayeredWindowAttributes, SetWindowLongPtrW,
GWL_EXSTYLE, LWA_ALPHA, WS_EX_LAYERED,
};
let Ok(handle) = window.window_handle() else { return; };
let RawWindowHandle::Win32(win32) = handle.as_raw() else { return; };
let hwnd = HWND(win32.hwnd.get() as *mut _);
let a = (alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
unsafe {
let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
let layered = WS_EX_LAYERED.0 as isize;
if ex & layered == 0 {
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex | layered);
}
let _ = SetLayeredWindowAttributes(hwnd, COLORREF(0), a, LWA_ALPHA);
}
}
#[cfg(target_os = "linux")]
fn set_window_alpha(window: &Window, alpha: f32) {
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use std::ffi::CString;
use x11_dl::xlib;
let Ok(win_handle) = window.window_handle() else { return; };
let Ok(disp_handle) = window.display_handle() else { return; };
let (display_ptr, xid) = match (win_handle.as_raw(), disp_handle.as_raw()) {
(RawWindowHandle::Xlib(win), RawDisplayHandle::Xlib(disp)) => {
let Some(dp) = disp.display else { return; };
(dp.as_ptr() as *mut xlib::Display, win.window as xlib::Window)
}
_ => return,
};
let xlib = match xlib::Xlib::open() {
Ok(x) => x,
Err(_) => return,
};
let atom_name = match CString::new("_NET_WM_WINDOW_OPACITY") {
Ok(s) => s,
Err(_) => return,
};
let cardinal_name = match CString::new("CARDINAL") {
Ok(s) => s,
Err(_) => return,
};
let opacity = (alpha.clamp(0.0, 1.0) * u32::MAX as f32).round() as u32;
unsafe {
let atom = (xlib.XInternAtom)(display_ptr, atom_name.as_ptr(), xlib::False);
let cardinal = (xlib.XInternAtom)(display_ptr, cardinal_name.as_ptr(), xlib::False);
if atom == 0 || cardinal == 0 { return; }
(xlib.XChangeProperty)(
display_ptr,
xid,
atom,
cardinal,
32,
xlib::PropModeReplace,
&opacity as *const u32 as *const u8,
1,
);
(xlib.XFlush)(display_ptr);
}
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
fn set_window_alpha(_window: &Window, _alpha: f32) {}
#[cfg(target_os = "windows")]
fn apply_win11_chrome(raw_window: &raw_window_handle::RawWindowHandle) {
use raw_window_handle::RawWindowHandle;
use windows::Win32::Foundation::{BOOL, HWND};
use windows::Win32::Graphics::Dwm::{
DwmSetWindowAttribute, DWMSBT_MAINWINDOW, DWMWA_SYSTEMBACKDROP_TYPE,
DWMWA_USE_IMMERSIVE_DARK_MODE, DWM_SYSTEMBACKDROP_TYPE,
};
let RawWindowHandle::Win32(handle) = raw_window else { return };
let hwnd = HWND(handle.hwnd.get() as *mut _);
unsafe {
let dark: BOOL = BOOL(1);
let _ = DwmSetWindowAttribute(
hwnd,
DWMWA_USE_IMMERSIVE_DARK_MODE,
&dark as *const _ as _,
std::mem::size_of::<BOOL>() as u32,
);
let backdrop: DWM_SYSTEMBACKDROP_TYPE = DWMSBT_MAINWINDOW;
let _ = DwmSetWindowAttribute(
hwnd,
DWMWA_SYSTEMBACKDROP_TYPE,
&backdrop as *const _ as _,
std::mem::size_of::<DWM_SYSTEMBACKDROP_TYPE>() as u32,
);
}
}
fn discover_plugin_root() -> Option<PathBuf> {
if let Ok(p) = std::env::var("LAYERS_PLUGIN_ROOT") {
let pb = PathBuf::from(p);
if pb.is_dir() {
return Some(pb);
}
}
let exe = std::env::current_exe().ok()?;
let bin_dir = exe.parent()?;
if bin_dir.file_name().and_then(|s| s.to_str()) == Some("bin") {
return bin_dir.parent().map(|p| p.to_path_buf());
}
bin_dir.parent().map(|p| p.to_path_buf())
}