nice-plug-egui 0.1.3

An adapter to use egui GUIs with nice-plug
Documentation
//! [egui](https://github.com/emilk/egui) editor support for nice-plug.
//!
//! TODO: Proper usage example, for now check out the gain_gui example

// See the comment in the main `nice-plug` crate
#![allow(clippy::type_complexity)]

use crossbeam::atomic::AtomicCell;
use egui::{Context, Ui};
use nice_plug_core::context::gui::ParamSetter;
use nice_plug_core::editor::Editor;
use nice_plug_core::params::persist::PersistentField;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

#[cfg(not(any(feature = "opengl", feature = "wgpu")))]
compile_error!("There's currently no software rendering support for egui");

/// Re-export for convenience.
pub use egui_baseview::*;

#[cfg(all(feature = "opengl", not(feature = "wgpu")))]
pub use baseview::gl::{GlConfig, Profile};

mod editor;
pub mod resizable_window;
pub mod widgets;

#[derive(Default, Debug, Clone)]
pub struct EguiSettings {
    pub graphics_config: GraphicsConfig,

    #[cfg(all(feature = "opengl", not(feature = "wgpu")))]
    /// By default this is set to `false`.
    pub enable_vsync_on_x11: bool,

    #[cfg(all(feature = "opengl", not(feature = "wgpu")))]
    /// The configuration of the OpenGL context.
    ///
    /// By default this is set to:
    /// ```ignore
    /// GlConfig {
    ///     version: (3, 2),
    ///     profile: Profile::Core,
    ///     red_bits: 8,
    ///     blue_bits: 8,
    ///     green_bits: 8,
    ///     alpha_bits: 8,
    ///     depth_bits: 24,
    ///     stencil_bits: 8,
    ///     samples: None,
    ///     srgb: true,
    ///     double_buffer: true,
    ///     vsync: false,
    /// }
    /// ```
    pub gl_config: GlConfig,
}

/// Create an [`Editor`] instance using an [`egui`] GUI. Using the user state parameter is
/// optional, but it can be useful for keeping track of some temporary GUI-only settings. See the
/// `nice-plug_gain_egui` example for more information on how to use this. The [`EguiState`] passed
/// to this function contains the GUI's intitial size, and this is kept in sync whenever the GUI gets
/// resized. You can also use this to know if the GUI is open, so you can avoid performing
/// potentially expensive calculations while the GUI is not open. If you want this size to be
/// persisted when restoring a plugin instance, then you can store it in a `#[persist = "key"]`
/// field on your parameters struct.
///
/// See [`EguiState::from_size()`].
pub fn create_egui_editor<T, B, U>(
    egui_state: Arc<EguiState>,
    user_state: T,
    settings: EguiSettings,
    build: B,
    update: U,
) -> Option<Box<dyn Editor>>
where
    T: 'static + Send,
    B: Fn(&Context, &mut Queue, &mut T) + 'static + Send + Sync,
    U: Fn(&mut Ui, &ParamSetter, &mut Queue, &mut T) + 'static + Send + Sync,
{
    Some(Box::new(editor::EguiEditor {
        egui_state,
        user_state: Arc::new(Mutex::new(user_state)),
        settings: Arc::new(settings),
        build: Arc::new(build),
        update: Arc::new(update),

        // TODO: We can't get the size of the window when baseview does its own scaling, so if the
        //       host does not set a scale factor on Windows or Linux we should just use a factor of
        //       1. That may make the GUI tiny but it also prevents it from getting cut off.
        #[cfg(target_os = "macos")]
        scaling_factor: AtomicCell::new(None),
        #[cfg(not(target_os = "macos"))]
        scaling_factor: AtomicCell::new(Some(1.0)),
    }))
}

/// State for an `nice-plug-egui` editor.
#[derive(Debug, Serialize, Deserialize)]
pub struct EguiState {
    /// The window's size in logical pixels before applying `scale_factor`.
    #[serde(with = "nice_plug_core::params::persist::serialize_atomic_cell")]
    size: AtomicCell<(u32, u32)>,

    /// The new size of the window, if it was requested to resize by the GUI.
    #[serde(skip)]
    requested_size: AtomicCell<Option<(u32, u32)>>,

    /// Whether the editor's window is currently open.
    #[serde(skip)]
    open: AtomicBool,
}

impl<'a> PersistentField<'a, EguiState> for Arc<EguiState> {
    fn set(&self, new_value: EguiState) {
        self.size.store(new_value.size.load());
    }

    fn map<F, R>(&self, f: F) -> R
    where
        F: Fn(&EguiState) -> R,
    {
        f(self)
    }
}

impl EguiState {
    /// Initialize the GUI's state. This value can be passed to [`create_egui_editor()`]. The window
    /// size is in logical pixels, so before it is multiplied by the DPI scaling factor.
    pub fn from_size(width: u32, height: u32) -> Arc<EguiState> {
        Arc::new(EguiState {
            size: AtomicCell::new((width, height)),
            requested_size: Default::default(),
            open: AtomicBool::new(false),
        })
    }

    /// Returns a `(width, height)` pair for the current size of the GUI in logical pixels.
    pub fn size(&self) -> (u32, u32) {
        self.size.load()
    }

    /// Whether the GUI is currently visible.
    // Called `is_open()` instead of `open()` to avoid the ambiguity.
    pub fn is_open(&self) -> bool {
        self.open.load(Ordering::Acquire)
    }

    /// Set the new size (in logical pixels) that will be used to resize the window if the host allows.
    pub fn set_requested_size(&self, new_size: (u32, u32)) {
        self.requested_size.store(Some(new_size));
    }
}