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/// macOS-only; always `false` elsewhere - Linux/Windows hosts manage
103/// visibility natively and don't exhibit the pile-up.
104#[cfg(target_os = "macos")]
105#[must_use]
106pub fn should_skip_frame(handle: raw_window_handle::RawWindowHandle) -> bool {
107    use objc::{msg_send, sel, sel_impl};
108
109    let view_ptr = match handle {
110        raw_window_handle::RawWindowHandle::AppKit(h) => h.ns_view,
111        _ => return false,
112    };
113    if view_ptr.is_null() {
114        return true;
115    }
116
117    unsafe {
118        let view = view_ptr.cast::<objc::runtime::Object>();
119        let window: *mut objc::runtime::Object = msg_send![view, window];
120        if window.is_null() {
121            // Detached from any window - nothing to present into.
122            return true;
123        }
124        // `NSWindowOcclusionStateVisible` == 1 << 1. Bit clear => the
125        // window is not visible (minimized or fully covered).
126        let state: u64 = msg_send![window, occlusionState];
127        state & (1 << 1) == 0
128    }
129}
130
131#[cfg(not(target_os = "macos"))]
132#[must_use]
133pub fn should_skip_frame(_handle: raw_window_handle::RawWindowHandle) -> bool {
134    false
135}
136
137/// Walk every direct subview of the host-provided parent `NSView`
138/// and pin its top edge to the parent's top in unflipped Cocoa
139/// coordinates. Used by GUI backends that don't expose their own
140/// child `Window` per-frame (vizia) - they hand us the parent
141/// handle they got at `Editor::open` time and we walk the subview
142/// tree the host installed our backend's view into.
143#[cfg(target_os = "macos")]
144pub fn reanchor_all_children_to_top(parent: *mut std::ffi::c_void) {
145    use objc::runtime::Object;
146    use objc::{msg_send, sel, sel_impl};
147
148    if parent.is_null() {
149        return;
150    }
151    unsafe {
152        let parent_obj = parent.cast::<Object>();
153        let parent_frame: NsRect = msg_send![parent_obj, frame];
154        let subviews: *mut Object = msg_send![parent_obj, subviews];
155        if subviews.is_null() {
156            return;
157        }
158        let count: usize = msg_send![subviews, count];
159        for i in 0..count {
160            let child: *mut Object = msg_send![subviews, objectAtIndex: i];
161            if child.is_null() {
162                continue;
163            }
164            let child_frame: NsRect = msg_send![child, frame];
165            let new_y = parent_frame.size.height - child_frame.size.height;
166            if (new_y - child_frame.origin.y).abs() < f64::EPSILON {
167                continue;
168            }
169            let new_origin = NsPoint {
170                x: child_frame.origin.x,
171                y: new_y,
172            };
173            let _: () = msg_send![child, setFrameOrigin: new_origin];
174        }
175    }
176}
177
178#[cfg(not(target_os = "macos"))]
179pub fn reanchor_all_children_to_top(_parent: *mut std::ffi::c_void) {}