Skip to main content

truce_gui_utils/
lib.rs

1//! Shared host-side platform helpers for truce GUI backends that
2//! embed a wgpu-backed (or CALayer-backed) child view into a
3//! DAW-provided parent window.
4//!
5//! Currently macOS-only: the helpers pin an embedded `NSView`'s top
6//! edge to its superview's top edge across host-driven resizes.
7//! Linux/Windows hosts manage child-window positioning natively, so
8//! these helpers are no-ops there.
9
10#![allow(clippy::module_name_repetitions)]
11
12#[cfg(target_os = "macos")]
13#[repr(C)]
14#[derive(Clone, Copy)]
15struct NsPoint {
16    x: f64,
17    y: f64,
18}
19
20#[cfg(target_os = "macos")]
21#[repr(C)]
22#[derive(Clone, Copy)]
23struct NsSize {
24    width: f64,
25    height: f64,
26}
27
28#[cfg(target_os = "macos")]
29#[repr(C)]
30#[derive(Clone, Copy)]
31struct NsRect {
32    origin: NsPoint,
33    size: NsSize,
34}
35
36/// Re-anchor the editor's `NSView` to the **top** of its superview
37/// in unflipped Cocoa coordinates.
38///
39/// The CLAP / LV2 / AU shims set the child's autoresize mask to
40/// `NSViewMinYMargin | NSViewMaxXMargin` so the parent-resize
41/// cascade keeps the child pinned. `AppKit`'s autoresize math only
42/// runs when the *parent* resizes, though - resizing the *child*
43/// (via `baseview::Window::resize` / `setFrameSize:`) leaves the
44/// origin alone, which silently drifts the child off-anchor: a
45/// taller child grows *down* from its existing origin instead of
46/// staying anchored to the parent's top. Visually that looks like
47/// the editor's header / first row disappearing above the visible
48/// plug-in area.
49///
50/// Call this on macOS each frame (e.g. from `WindowHandler::on_frame`)
51/// so the child's origin tracks its size. No-op on non-macOS.
52#[cfg(target_os = "macos")]
53pub fn reanchor_to_superview_top(handle: raw_window_handle::RawWindowHandle) {
54    use objc::{msg_send, sel, sel_impl};
55
56    let view_ptr = match handle {
57        raw_window_handle::RawWindowHandle::AppKit(h) => h.ns_view,
58        _ => return,
59    };
60    if view_ptr.is_null() {
61        return;
62    }
63
64    unsafe {
65        let view = view_ptr.cast::<objc::runtime::Object>();
66        let superview: *mut objc::runtime::Object = msg_send![view, superview];
67        if superview.is_null() {
68            return;
69        }
70        let parent_frame: NsRect = msg_send![superview, frame];
71        let child_frame: NsRect = msg_send![view, frame];
72        let new_y = parent_frame.size.height - child_frame.size.height;
73        if (new_y - child_frame.origin.y).abs() < f64::EPSILON {
74            return;
75        }
76        let new_origin = NsPoint {
77            x: child_frame.origin.x,
78            y: new_y,
79        };
80        let _: () = msg_send![view, setFrameOrigin: new_origin];
81    }
82}
83
84#[cfg(not(target_os = "macos"))]
85pub fn reanchor_to_superview_top(_handle: raw_window_handle::RawWindowHandle) {}
86
87/// Whether a GUI backend's per-frame `on_frame` should skip all work
88/// this tick.
89///
90/// Returns `true` when the editor's `NSView` is detached from any
91/// window - the editor was torn down but baseview's frame timer is
92/// still firing (notably AU, which may not call `gui_close`) - or
93/// when the host window is not visible (minimized or fully occluded).
94///
95/// Skipping occluded frames is the load-bearing part: a non-visible
96/// window can't present, so any frame a backend renders queues a GPU
97/// drawable that can't be drained, and they pile up unbounded (tens of
98/// GB of wired memory) until the window returns to front. The
99/// `NSWindowOcclusionStateVisible` bit is the authoritative early
100/// signal, so this must be called first thing in `on_frame`.
101///
102/// On macOS the `NSWindowOcclusionStateVisible` bit is the
103/// authoritative signal; on Windows we skip when the host's child
104/// window is hidden or minimized (`IsWindowVisible` / `IsIconic`).
105/// Always `false` on Linux, which manages visibility natively and
106/// doesn't exhibit the pile-up.
107///
108/// The Windows case matters for a different reason than macOS: an
109/// embedded editor is a `WS_CHILD` of the host window, so its
110/// `on_frame` runs on the host's GUI thread. Rendering + a blocking
111/// `present` to a window the user can't see burns that thread for
112/// nothing and can back up the swapchain; skipping keeps the host
113/// (REAPER, etc.) responsive while its FX window is closed.
114#[cfg(target_os = "macos")]
115#[must_use]
116pub fn should_skip_frame(handle: raw_window_handle::RawWindowHandle) -> bool {
117    use objc::{msg_send, sel, sel_impl};
118
119    let view_ptr = match handle {
120        raw_window_handle::RawWindowHandle::AppKit(h) => h.ns_view,
121        _ => return false,
122    };
123    if view_ptr.is_null() {
124        return true;
125    }
126
127    unsafe {
128        let view = view_ptr.cast::<objc::runtime::Object>();
129        let window: *mut objc::runtime::Object = msg_send![view, window];
130        if window.is_null() {
131            // Detached from any window - nothing to present into.
132            return true;
133        }
134        // `NSWindowOcclusionStateVisible` == 1 << 1. Bit clear => the
135        // window is not visible (minimized or fully covered).
136        let state: u64 = msg_send![window, occlusionState];
137        state & (1 << 1) == 0
138    }
139}
140
141#[cfg(target_os = "windows")]
142#[must_use]
143pub fn should_skip_frame(handle: raw_window_handle::RawWindowHandle) -> bool {
144    unsafe extern "system" {
145        fn IsWindowVisible(hwnd: *mut std::ffi::c_void) -> i32;
146        fn IsIconic(hwnd: *mut std::ffi::c_void) -> i32;
147    }
148
149    let hwnd = match handle {
150        raw_window_handle::RawWindowHandle::Win32(h) => h.hwnd,
151        _ => return false,
152    };
153    if hwnd.is_null() {
154        return true;
155    }
156    // SAFETY: both are pure state queries on a window handle baseview
157    // owns for the editor's lifetime; no aliasing or threading concerns,
158    // and they're called from the GUI thread that owns the HWND.
159    unsafe { IsWindowVisible(hwnd) == 0 || IsIconic(hwnd) != 0 }
160}
161
162#[cfg(not(any(target_os = "macos", target_os = "windows")))]
163#[must_use]
164pub fn should_skip_frame(_handle: raw_window_handle::RawWindowHandle) -> bool {
165    false
166}
167
168/// Walk every direct subview of the host-provided parent `NSView`
169/// and pin its top edge to the parent's top in unflipped Cocoa
170/// coordinates. Used by GUI backends that don't expose their own
171/// child `Window` per-frame (vizia) - they hand us the parent
172/// handle they got at `Editor::open` time and we walk the subview
173/// tree the host installed our backend's view into.
174#[cfg(target_os = "macos")]
175pub fn reanchor_all_children_to_top(parent: *mut std::ffi::c_void) {
176    use objc::runtime::Object;
177    use objc::{msg_send, sel, sel_impl};
178
179    if parent.is_null() {
180        return;
181    }
182    unsafe {
183        let parent_obj = parent.cast::<Object>();
184        // Skip a parent that's been detached from its window: a sign
185        // the host is tearing the editor down, after which walking its
186        // subviews risks messaging a freed view. Mirrors the liveness
187        // guard in `should_skip_frame`.
188        let window: *mut Object = msg_send![parent_obj, window];
189        if window.is_null() {
190            return;
191        }
192        let parent_frame: NsRect = msg_send![parent_obj, frame];
193        let subviews: *mut Object = msg_send![parent_obj, subviews];
194        if subviews.is_null() {
195            return;
196        }
197        let count: usize = msg_send![subviews, count];
198        for i in 0..count {
199            let child: *mut Object = msg_send![subviews, objectAtIndex: i];
200            if child.is_null() {
201                continue;
202            }
203            let child_frame: NsRect = msg_send![child, frame];
204            let new_y = parent_frame.size.height - child_frame.size.height;
205            if (new_y - child_frame.origin.y).abs() < f64::EPSILON {
206                continue;
207            }
208            let new_origin = NsPoint {
209                x: child_frame.origin.x,
210                y: new_y,
211            };
212            let _: () = msg_send![child, setFrameOrigin: new_origin];
213        }
214    }
215}
216
217#[cfg(not(target_os = "macos"))]
218pub fn reanchor_all_children_to_top(_parent: *mut std::ffi::c_void) {}
219
220/// Whether hardware-accelerated OpenGL (the WGL extension entry
221/// points a modern GL context needs) is available in this process.
222///
223/// When the GPU driver's OpenGL ICD is absent or broken (mismatched
224/// driver packages, disabled adapter, RDP session), `wglCreateContext`
225/// silently hands back Microsoft's software "GDI Generic" GL 1.1
226/// context, which exposes **no** WGL extensions. GL-based editors
227/// (vizia via Skia) then die inside window creation - under the Win32
228/// window proc, where a panic cannot unwind and aborts the entire
229/// host. Callers probe this *before* opening such an editor and skip
230/// the open instead.
231///
232/// The probe mirrors baseview's own bootstrap: a hidden throwaway
233/// window, a basic pixel format, a legacy `wglCreateContext`, then a
234/// `wglGetProcAddress` lookup of the two extensions context creation
235/// requires. Everything is torn down before returning.
236#[cfg(target_os = "windows")]
237#[must_use]
238// Win32 ABI struct-filling: the casts and `as`-conversions mirror the
239// C headers' field types.
240#[allow(clippy::cast_possible_truncation, clippy::unnecessary_cast)]
241pub fn wgl_extensions_available() -> bool {
242    use windows_sys::Win32::Graphics::Gdi::{GetDC, ReleaseDC};
243    use windows_sys::Win32::Graphics::OpenGL::{
244        ChoosePixelFormat, PFD_DRAW_TO_WINDOW, PFD_MAIN_PLANE, PFD_SUPPORT_OPENGL, PFD_TYPE_RGBA,
245        PIXELFORMATDESCRIPTOR, SetPixelFormat, wglCreateContext, wglDeleteContext,
246        wglGetProcAddress, wglMakeCurrent,
247    };
248    use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
249    use windows_sys::Win32::UI::WindowsAndMessaging::{
250        CS_OWNDC, CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassW, UnregisterClassW,
251        WNDCLASSW,
252    };
253    use windows_sys::core::s;
254
255    // UTF-16, NUL-terminated: "truce-wgl-probe".
256    const CLASS_NAME: &[u16] = &[
257        0x74, 0x72, 0x75, 0x63, 0x65, 0x2d, 0x77, 0x67, 0x6c, 0x2d, 0x70, 0x72, 0x6f, 0x62, 0x65, 0,
258    ];
259
260    unsafe {
261        let hinstance = GetModuleHandleW(std::ptr::null());
262        let class = WNDCLASSW {
263            style: CS_OWNDC,
264            lpfnWndProc: Some(DefWindowProcW),
265            cbClsExtra: 0,
266            cbWndExtra: 0,
267            hInstance: hinstance,
268            hIcon: std::ptr::null_mut(),
269            hCursor: std::ptr::null_mut(),
270            hbrBackground: std::ptr::null_mut(),
271            lpszMenuName: std::ptr::null(),
272            lpszClassName: CLASS_NAME.as_ptr(),
273        };
274        let atom = RegisterClassW(&raw const class);
275        if atom == 0 {
276            return false;
277        }
278        let hwnd = CreateWindowExW(
279            0,
280            CLASS_NAME.as_ptr(),
281            CLASS_NAME.as_ptr(),
282            0,
283            0,
284            0,
285            1,
286            1,
287            std::ptr::null_mut(),
288            std::ptr::null_mut(),
289            hinstance,
290            std::ptr::null(),
291        );
292        if hwnd.is_null() {
293            UnregisterClassW(CLASS_NAME.as_ptr(), hinstance);
294            return false;
295        }
296
297        let mut available = false;
298        let hdc = GetDC(hwnd);
299        if !hdc.is_null() {
300            let pfd = PIXELFORMATDESCRIPTOR {
301                nSize: size_of::<PIXELFORMATDESCRIPTOR>() as u16,
302                nVersion: 1,
303                dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL,
304                iPixelType: PFD_TYPE_RGBA as u8,
305                cColorBits: 32,
306                cDepthBits: 24,
307                cStencilBits: 8,
308                iLayerType: PFD_MAIN_PLANE as u8,
309                ..std::mem::zeroed()
310            };
311            let format = ChoosePixelFormat(hdc, &raw const pfd);
312            if format != 0 && SetPixelFormat(hdc, format, &raw const pfd) != 0 {
313                let hglrc = wglCreateContext(hdc);
314                if !hglrc.is_null() {
315                    wglMakeCurrent(hdc, hglrc);
316                    available = wglGetProcAddress(s!("wglChoosePixelFormatARB")).is_some()
317                        && wglGetProcAddress(s!("wglCreateContextAttribsARB")).is_some();
318                    wglMakeCurrent(hdc, std::ptr::null_mut());
319                    wglDeleteContext(hglrc);
320                }
321            }
322            ReleaseDC(hwnd, hdc);
323        }
324        DestroyWindow(hwnd);
325        UnregisterClassW(CLASS_NAME.as_ptr(), hinstance);
326        available
327    }
328}