#![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>, handle: Option, is_focused: bool, is_hovered: bool, modifiers: ModifiersState, last_cursor: PhysicalPosition, current_alpha: f32, target_alpha: f32, tween_from: f32, tween_started: Option, } 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::() as u32, ); let backdrop: DWM_SYSTEMBACKDROP_TYPE = DWMSBT_MAINWINDOW; let _ = DwmSetWindowAttribute( hwnd, DWMWA_SYSTEMBACKDROP_TYPE, &backdrop as *const _ as _, std::mem::size_of::() as u32, ); } } fn discover_plugin_root() -> Option { 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()) }