Skip to main content

nice_plug_iced/
editor.rs

1use crossbeam_utils::atomic::AtomicCell;
2use iced_baseview::baseview::{Size, WindowOpenOptions, WindowScalePolicy};
3use iced_baseview::{
4    IcedBaseviewSettings, PollSubNotifier, Program, message, shell::window::WindowHandle,
5};
6use nice_plug_core::context::gui::{GuiContext, ParamSetter};
7use nice_plug_core::{
8    editor::{Editor, ParentWindowHandle},
9    params::persist::PersistentField,
10};
11use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
12use serde::{Deserialize, Serialize};
13use std::sync::{
14    Arc, Mutex,
15    atomic::{AtomicBool, Ordering},
16};
17
18use crate::{EditorSettings, application::EditorState};
19
20pub(crate) struct IcedEditor<P: Program + 'static, EState: Send + 'static>
21where
22    <P as Program>::Message: message::MaybeDebug + message::MaybeClone,
23{
24    pub(crate) window_state: Arc<WindowState>,
25    pub(crate) editor_state: Arc<Mutex<Option<EState>>>,
26
27    /// The user's build function. Applied once at the start of the application.
28    pub(crate) build: Arc<dyn Fn(EditorState<EState>, NiceGuiContext) -> P + 'static + Send + Sync>,
29    pub(crate) notifier: PollSubNotifier,
30
31    pub(crate) settings: Arc<EditorSettings>,
32
33    /// The scaling factor reported by the host, if any. On macOS this will never be set and we
34    /// should use the system scaling factor instead.
35    pub(crate) scaling_factor: AtomicCell<Option<f32>>,
36}
37
38impl<P: Program + 'static, State: Send + 'static> Editor for IcedEditor<P, State> {
39    fn spawn(
40        &self,
41        parent: ParentWindowHandle,
42        context: Arc<dyn GuiContext>,
43    ) -> Box<dyn std::any::Any + Send> {
44        let nice_ctx = NiceGuiContext {
45            context: context.clone(),
46            window_state: self.window_state.clone(),
47        };
48
49        let build = self.build.clone();
50        let editor_state = EditorState::from_shared(&self.editor_state);
51
52        let (unscaled_width, unscaled_height) = self.window_state.logical_size();
53        let scaling_factor = self.scaling_factor.load();
54
55        #[allow(clippy::needless_update)]
56        let window = iced_baseview::open_parented(
57            &ParentWindowHandleAdapter(parent),
58            IcedBaseviewSettings {
59                window: WindowOpenOptions {
60                    title: String::from("iced window"),
61                    // Baseview should be doing the DPI scaling for us
62                    size: Size::new(unscaled_width as f64, unscaled_height as f64),
63                    // NOTE: For some reason passing 1.0 here causes the UI to be scaled on macOS but
64                    //       not the mouse events.
65                    scale: scaling_factor
66                        .map(|factor| WindowScalePolicy::ScaleFactor(factor as f64))
67                        .unwrap_or(WindowScalePolicy::SystemScaleFactor),
68                    ..Default::default()
69                },
70                ignore_non_modifier_keys: self.settings.ignore_non_modifier_keys,
71                always_redraw: self.settings.always_redraw,
72            },
73            self.notifier.clone(),
74            move || (build)(editor_state, nice_ctx),
75        );
76
77        self.window_state.open.store(true, Ordering::Release);
78
79        Box::new(IcedEditorHandle {
80            iced_state: self.window_state.clone(),
81            _window: window,
82        })
83    }
84
85    /// Size of the editor window
86    fn size(&self) -> (u32, u32) {
87        let new_size = self.window_state.requested_logical_size.load();
88        // This method will be used to ask the host for new size.
89        // If the editor is currently being resized and new size hasn't been consumed and set yet, return new requested size.
90        if let Some(new_size) = new_size {
91            new_size
92        } else {
93            self.window_state.logical_size()
94        }
95    }
96
97    fn set_scale_factor(&self, factor: f32) -> bool {
98        // If the editor is currently open then the host must not change the current HiDPI scale as
99        // we don't have a way to handle that. Ableton Live does this.
100        if self.window_state.is_open() {
101            return false;
102        }
103
104        self.scaling_factor.store(Some(factor));
105        true
106    }
107
108    fn param_value_changed(&self, _id: &str, _normalized_value: f32) {
109        self.notifier.notify();
110    }
111
112    fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {
113        self.notifier.notify();
114    }
115
116    fn param_values_changed(&self) {
117        self.notifier.notify();
118    }
119}
120
121/// The window handle used for [`IcedEditor`].
122struct IcedEditorHandle<Message: 'static + Send> {
123    iced_state: Arc<WindowState>,
124    _window: WindowHandle<Message>,
125}
126
127/// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around
128/// having this requirement?
129unsafe impl<Message: 'static + Send> Send for IcedEditorHandle<Message> {}
130
131impl<Message: 'static + Send> Drop for IcedEditorHandle<Message> {
132    fn drop(&mut self) {
133        self.iced_state.open.store(false, Ordering::Release);
134    }
135}
136
137/// State for an `nice-plug-iced` editor window.
138#[derive(Debug, Serialize, Deserialize)]
139pub struct WindowState {
140    /// The window's size in logical pixels before applying `scale_factor`.
141    #[serde(with = "nice_plug_core::params::persist::serialize_atomic_cell")]
142    pub(crate) logical_size: AtomicCell<(u32, u32)>,
143
144    /// The new size of the window, if it was requested to resize by the GUI.
145    #[serde(skip)]
146    pub(crate) requested_logical_size: AtomicCell<Option<(u32, u32)>>,
147
148    /// Whether the editor's window is currently open.
149    #[serde(skip)]
150    pub(crate) open: AtomicBool,
151}
152
153impl<'a> PersistentField<'a, WindowState> for Arc<WindowState> {
154    fn set(&self, new_value: WindowState) {
155        self.logical_size.store(new_value.logical_size.load());
156    }
157
158    fn map<F, R>(&self, f: F) -> R
159    where
160        F: Fn(&WindowState) -> R,
161    {
162        f(self)
163    }
164}
165
166impl WindowState {
167    /// Initialize the GUI's state. This value can be passed to
168    /// [`create_iced_editor()`](crate::create_iced_editor). The window size is in logical
169    /// pixels, so before it is multiplied by the DPI scaling factor.
170    pub fn from_logical_size(width: u32, height: u32) -> Arc<WindowState> {
171        Arc::new(WindowState {
172            logical_size: AtomicCell::new((width, height)),
173            requested_logical_size: Default::default(),
174            open: AtomicBool::new(false),
175        })
176    }
177
178    /// Returns a `(width, height)` pair for the current size of the GUI in logical pixels.
179    pub fn logical_size(&self) -> (u32, u32) {
180        self.logical_size.load()
181    }
182
183    /// Whether the GUI is currently visible.
184    // Called `is_open()` instead of `open()` to avoid the ambiguity.
185    pub fn is_open(&self) -> bool {
186        self.open.load(Ordering::Acquire)
187    }
188
189    /// Set the new size that will be used to resize the window if the host allows.
190    pub fn set_requested_logical_size(&self, new_size: (u32, u32)) {
191        self.requested_logical_size.store(Some(new_size));
192    }
193}
194
195#[derive(Clone)]
196pub struct NiceGuiContext {
197    pub context: Arc<dyn GuiContext>,
198    window_state: Arc<WindowState>,
199}
200
201impl NiceGuiContext {
202    /// Returns a `(width, height)` pair for the current size of the GUI in logical pixels.
203    pub fn logical_size(&self) -> (u32, u32) {
204        self.window_state.logical_size()
205    }
206
207    /// Whether the GUI is currently visible.
208    // Called `is_open()` instead of `open()` to avoid the ambiguity.
209    pub fn is_open(&self) -> bool {
210        self.window_state.is_open()
211    }
212
213    /// Set the new size that will be used to resize the window if the host allows.
214    pub fn set_requested_logical_size(&self, new_size: (u32, u32)) {
215        self.window_state.set_requested_logical_size(new_size);
216
217        // Ask the plugin host to resize to self.size()
218        if self.context.request_resize() {
219            self.window_state.logical_size.store(new_size);
220
221            // TODO: Resize Iced content?
222        }
223    }
224
225    pub fn param_setter<'a>(&'a self) -> ParamSetter<'a> {
226        ParamSetter {
227            raw_context: &*self.context,
228        }
229    }
230}
231
232/// This version of `baseview` uses a different version of `raw_window_handle than nice-plug, so we
233/// need to adapt it ourselves.
234struct ParentWindowHandleAdapter(ParentWindowHandle);
235
236unsafe impl HasRawWindowHandle for ParentWindowHandleAdapter {
237    fn raw_window_handle(&self) -> RawWindowHandle {
238        match self.0 {
239            ParentWindowHandle::X11Window(window) => {
240                let mut handle = raw_window_handle::XcbWindowHandle::empty();
241                handle.window = window;
242                RawWindowHandle::Xcb(handle)
243            }
244            ParentWindowHandle::AppKitNsView(ns_view) => {
245                let mut handle = raw_window_handle::AppKitWindowHandle::empty();
246                handle.ns_view = ns_view;
247                RawWindowHandle::AppKit(handle)
248            }
249            ParentWindowHandle::Win32Hwnd(hwnd) => {
250                let mut handle = raw_window_handle::Win32WindowHandle::empty();
251                handle.hwnd = hwnd;
252                RawWindowHandle::Win32(handle)
253            }
254        }
255    }
256}