Trying to clena the mess opened by plugins

This commit is contained in:
2026-02-21 01:03:55 +01:00
parent 5ef988382b
commit e9bca2548c
67 changed files with 1246 additions and 69 deletions

View File

@@ -0,0 +1,25 @@
[package]
name = "nih_plug_egui"
version = "0.0.0"
edition = "2021"
authors = ["Robbert van der Helm <mail@robbertvanderhelm.nl>"]
license = "ISC"
description = "An adapter to use egui GUIs with NIH-plug"
[features]
default = ["opengl", "default_fonts"]
# `nih_plug_egui` always uses OpenGL since egui's wgpu backend is still unstable
# depending on the platform
opengl = ["egui-baseview/opengl"]
default_fonts = ["egui-baseview/default_fonts"]
[dependencies]
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", default-features = false }
raw-window-handle = "0.5"
baseview = { path = "../baseview" }
crossbeam = "0.8"
egui-baseview = { path = "../egui-baseview" }
parking_lot = "0.12"
# To make the state persistable
serde = { version = "1.0", features = ["derive"] }

View File

@@ -0,0 +1,194 @@
//! An [`Editor`] implementation for egui.
use crate::egui::Vec2;
use crate::egui::ViewportCommand;
use crate::EguiState;
use baseview::gl::GlConfig;
use baseview::PhySize;
use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy};
use crossbeam::atomic::AtomicCell;
use egui_baseview::egui::Context;
use egui_baseview::EguiWindow;
use nih_plug::prelude::{Editor, GuiContext, ParamSetter, ParentWindowHandle};
use parking_lot::RwLock;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use std::sync::atomic::Ordering;
use std::sync::Arc;
/// An [`Editor`] implementation that calls an egui draw loop.
pub(crate) struct EguiEditor<T> {
pub(crate) egui_state: Arc<EguiState>,
/// The plugin's state. This is kept in between editor openenings.
pub(crate) user_state: Arc<RwLock<T>>,
/// The user's build function. Applied once at the start of the application.
pub(crate) build: Arc<dyn Fn(&Context, &mut T) + 'static + Send + Sync>,
/// The user's update function.
pub(crate) update: Arc<dyn Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync>,
/// The scaling factor reported by the host, if any. On macOS this will never be set and we
/// should use the system scaling factor instead.
pub(crate) scaling_factor: AtomicCell<Option<f32>>,
}
/// This version of `baseview` uses a different version of `raw_window_handle than NIH-plug, so we
/// need to adapt it ourselves.
struct ParentWindowHandleAdapter(nih_plug::editor::ParentWindowHandle);
unsafe impl HasRawWindowHandle for ParentWindowHandleAdapter {
fn raw_window_handle(&self) -> RawWindowHandle {
match self.0 {
ParentWindowHandle::X11Window(window) => {
let mut handle = raw_window_handle::XcbWindowHandle::empty();
handle.window = window;
RawWindowHandle::Xcb(handle)
}
ParentWindowHandle::AppKitNsView(ns_view) => {
let mut handle = raw_window_handle::AppKitWindowHandle::empty();
handle.ns_view = ns_view;
RawWindowHandle::AppKit(handle)
}
ParentWindowHandle::Win32Hwnd(hwnd) => {
let mut handle = raw_window_handle::Win32WindowHandle::empty();
handle.hwnd = hwnd;
RawWindowHandle::Win32(handle)
}
}
}
}
impl<T> Editor for EguiEditor<T>
where
T: 'static + Send + Sync,
{
fn spawn(
&self,
parent: ParentWindowHandle,
context: Arc<dyn GuiContext>,
) -> Box<dyn std::any::Any + Send> {
let build = self.build.clone();
let update = self.update.clone();
let state = self.user_state.clone();
let egui_state = self.egui_state.clone();
let (unscaled_width, unscaled_height) = self.egui_state.size();
let scaling_factor = self.scaling_factor.load();
let window = EguiWindow::open_parented(
&ParentWindowHandleAdapter(parent),
WindowOpenOptions {
title: String::from("egui window"),
// Baseview should be doing the DPI scaling for us
size: Size::new(unscaled_width as f64, unscaled_height as f64),
// NOTE: For some reason passing 1.0 here causes the UI to be scaled on macOS but
// not the mouse events.
scale: scaling_factor
.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64))
.unwrap_or(WindowScalePolicy::SystemScaleFactor),
#[cfg(feature = "opengl")]
gl_config: Some(GlConfig {
version: (3, 2),
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: true,
..Default::default()
}),
},
Default::default(),
state,
move |egui_ctx, _queue, state| build(egui_ctx, &mut state.write()),
move |egui_ctx, queue, state| {
let setter = ParamSetter::new(context.as_ref());
// If the window was requested to resize
if let Some(new_size) = egui_state.requested_size.swap(None) {
// Ask the plugin host to resize to self.size()
if context.request_resize() {
// Resize the content of egui window
queue.resize(PhySize::new(new_size.0, new_size.1));
egui_ctx.send_viewport_cmd(ViewportCommand::InnerSize(Vec2::new(
new_size.0 as f32,
new_size.1 as f32,
)));
// Update the state
egui_state.size.store(new_size);
}
}
// For now, just always redraw. Most plugin GUIs have meters, and those almost always
// need a redraw. Later we can try to be a bit more sophisticated about this. Without
// this we would also have a blank GUI when it gets first opened because most DAWs open
// their GUI while the window is still unmapped.
egui_ctx.request_repaint();
(update)(egui_ctx, &setter, &mut state.write());
},
);
self.egui_state.open.store(true, Ordering::Release);
Box::new(EguiEditorHandle {
egui_state: self.egui_state.clone(),
window,
})
}
/// Size of the editor window
fn size(&self) -> (u32, u32) {
let new_size = self.egui_state.requested_size.load();
// This method will be used to ask the host for new size.
// If the editor is currently being resized and new size hasn't been consumed and set yet, return new requested size.
if let Some(new_size) = new_size {
new_size
} else {
self.egui_state.size()
}
}
fn set_scale_factor(&self, factor: f32) -> bool {
// If the editor is currently open then the host must not change the current HiDPI scale as
// we don't have a way to handle that. Ableton Live does this.
if self.egui_state.is_open() {
return false;
}
self.scaling_factor.store(Some(factor));
true
}
fn param_value_changed(&self, _id: &str, _normalized_value: f32) {
// As mentioned above, for now we'll always force a redraw to allow meter widgets to work
// correctly. In the future we can use an `Arc<AtomicBool>` and only force a redraw when
// that boolean is set.
}
fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {}
fn param_values_changed(&self) {
// Same
}
}
/// The window handle used for [`EguiEditor`].
struct EguiEditorHandle {
egui_state: Arc<EguiState>,
window: WindowHandle,
}
/// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around
/// having this requirement?
unsafe impl Send for EguiEditorHandle {}
impl Drop for EguiEditorHandle {
fn drop(&mut self) {
self.egui_state.open.store(false, Ordering::Release);
// XXX: This should automatically happen when the handle gets dropped, but apparently not
self.window.close();
}
}

View File

@@ -0,0 +1,119 @@
//! [egui](https://github.com/emilk/egui) editor support for NIH plug.
//!
//! TODO: Proper usage example, for now check out the gain_gui example
// See the comment in the main `nih_plug` crate
#![allow(clippy::type_complexity)]
use crossbeam::atomic::AtomicCell;
use egui::Context;
use nih_plug::params::persist::PersistentField;
use nih_plug::prelude::{Editor, ParamSetter};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[cfg(not(feature = "opengl"))]
compile_error!("There's currently no software rendering support for egui");
/// Re-export for convenience.
pub use egui_baseview::egui;
mod editor;
pub mod resizable_window;
pub mod widgets;
/// Create an [`Editor`] instance using an [`egui`][::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
/// `gui_gain` 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,
build: B,
update: U,
) -> Option<Box<dyn Editor>>
where
T: 'static + Send + Sync,
B: Fn(&Context, &mut T) + 'static + Send + Sync,
U: Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync,
{
Some(Box::new(editor::EguiEditor {
egui_state,
user_state: Arc::new(RwLock::new(user_state)),
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 `nih_plug_egui` editor.
#[derive(Debug, Serialize, Deserialize)]
pub struct EguiState {
/// The window's size in logical pixels before applying `scale_factor`.
#[serde(with = "nih_plug::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 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));
}
}

View File

@@ -0,0 +1,81 @@
//! Resizable window wrapper for Egui editor.
use egui_baseview::egui::emath::GuiRounding;
use egui_baseview::egui::{InnerResponse, UiBuilder};
use crate::egui::{pos2, CentralPanel, Context, Id, Rect, Response, Sense, Ui, Vec2};
use crate::EguiState;
/// Adds a corner to the plugin window that can be dragged in order to resize it.
/// Resizing happens through plugin API, hence a custom implementation is needed.
pub struct ResizableWindow {
id: Id,
min_size: Vec2,
}
impl ResizableWindow {
pub fn new(id_source: impl std::hash::Hash) -> Self {
Self {
id: Id::new(id_source),
min_size: Vec2::splat(16.0),
}
}
/// Won't shrink to smaller than this
#[inline]
pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
self.min_size = min_size.into();
self
}
pub fn show<R>(
self,
context: &Context,
egui_state: &EguiState,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
CentralPanel::default().show(context, move |ui| {
let ui_rect = ui.clip_rect();
let mut content_ui =
ui.new_child(UiBuilder::new().max_rect(ui_rect).layout(*ui.layout()));
let ret = add_contents(&mut content_ui);
let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
let corner_rect = Rect::from_min_size(ui_rect.max - corner_size, corner_size);
let corner_response = ui.interact(corner_rect, self.id.with("corner"), Sense::drag());
if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
let desired_size = (pointer_pos - ui_rect.min + 0.5 * corner_response.rect.size())
.max(self.min_size);
if corner_response.dragged() {
egui_state.set_requested_size((
desired_size.x.round() as u32,
desired_size.y.round() as u32,
));
}
}
paint_resize_corner(&content_ui, &corner_response);
ret
})
}
}
pub fn paint_resize_corner(ui: &Ui, response: &Response) {
let stroke = ui.style().interact(response).fg_stroke;
let painter = ui.painter();
let rect = response.rect.translate(-Vec2::splat(2.0)); // move away from the corner
let cp = rect.max.round_to_pixels(painter.pixels_per_point());
let mut w = 2.0;
while w <= rect.width() && w <= rect.height() {
painter.line_segment([pos2(cp.x - w, cp.y), pos2(cp.x, cp.y - w)], stroke);
w += 4.0;
}
}

View File

@@ -0,0 +1,12 @@
//! Custom egui widgets for displaying parameter values.
//!
//! # Note
//!
//! None of these widgets are finalized, and their sizes or looks can change at any point. Feel free
//! to copy the widgets and modify them to your personal taste.
pub mod generic_ui;
mod param_slider;
pub mod util;
pub use param_slider::ParamSlider;

View File

@@ -0,0 +1,72 @@
//! A simple generic UI widget that renders all parameters in a [`Params`] object as a scrollable
//! list of sliders and labels.
use std::sync::Arc;
use egui_baseview::egui::{self, TextStyle, Ui, Vec2};
use nih_plug::prelude::{Param, ParamFlags, ParamPtr, ParamSetter, Params};
use super::ParamSlider;
/// A widget that can be used to create a generic UI with. This is used in conjuction with empty
/// structs to emulate existential types.
pub trait ParamWidget {
fn add_widget<P: Param>(&self, ui: &mut Ui, param: &P, setter: &ParamSetter);
/// The same as [`add_widget()`][Self::add_widget()], but for a `ParamPtr`.
///
/// # Safety
///
/// Undefined behavior of the `ParamPtr` does not point to a valid parameter.
unsafe fn add_widget_raw(&self, ui: &mut Ui, param: &ParamPtr, setter: &ParamSetter) {
match param {
ParamPtr::FloatParam(p) => self.add_widget(ui, &**p, setter),
ParamPtr::IntParam(p) => self.add_widget(ui, &**p, setter),
ParamPtr::BoolParam(p) => self.add_widget(ui, &**p, setter),
ParamPtr::EnumParam(p) => self.add_widget(ui, &**p, setter),
}
}
}
/// Create a generic UI using [`ParamSlider`]s.
pub struct GenericSlider;
/// Create a scrollable generic UI using the specified widget. Takes up all the remaining vertical
/// space.
pub fn create(
ui: &mut Ui,
params: Arc<impl Params>,
setter: &ParamSetter,
widget: impl ParamWidget,
) {
let padding = Vec2::splat(ui.text_style_height(&TextStyle::Body) * 0.2);
egui::containers::ScrollArea::vertical()
// Take up all remaining space, use a wrapper container to adjust how much space that is
.auto_shrink([false, false])
.show(ui, |ui| {
let mut first_widget = true;
for (_, param_ptr, _) in params.param_map().into_iter() {
let flags = unsafe { param_ptr.flags() };
if flags.contains(ParamFlags::HIDE_IN_GENERIC_UI) {
continue;
}
// This list looks weird without a little padding
if !first_widget {
ui.allocate_space(padding);
}
ui.label(unsafe { param_ptr.name() });
unsafe { widget.add_widget_raw(ui, &param_ptr, setter) };
first_widget = false;
}
});
}
impl ParamWidget for GenericSlider {
fn add_widget<P: Param>(&self, ui: &mut Ui, param: &P, setter: &ParamSetter) {
// Make these sliders a bit wider, else they look a bit odd
ui.add(ParamSlider::for_param(param, setter).with_width(100.0));
}
}

View File

@@ -0,0 +1,357 @@
use std::sync::{Arc, LazyLock};
use egui_baseview::egui::emath::GuiRounding;
use egui_baseview::egui::{
self, emath, vec2, Key, Response, Sense, Stroke, TextEdit, TextStyle, Ui, Vec2, Widget,
WidgetText,
};
use nih_plug::prelude::{Param, ParamSetter};
use parking_lot::Mutex;
use super::util;
/// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the
/// noramlized parameter.
const GRANULAR_DRAG_MULTIPLIER: f32 = 0.0015;
static DRAG_NORMALIZED_START_VALUE_MEMORY_ID: LazyLock<egui::Id> =
LazyLock::new(|| egui::Id::new((file!(), 0)));
static DRAG_AMOUNT_MEMORY_ID: LazyLock<egui::Id> = LazyLock::new(|| egui::Id::new((file!(), 1)));
static VALUE_ENTRY_MEMORY_ID: LazyLock<egui::Id> = LazyLock::new(|| egui::Id::new((file!(), 2)));
/// A slider widget similar to [`egui::widgets::Slider`] that knows about NIH-plug parameters ranges
/// and can get values for it. The slider supports double click and control click to reset,
/// shift+drag for granular dragging, text value entry by clicking on the value text.
///
/// TODO: Vertical orientation
/// TODO: Check below for more input methods that should be added
/// TODO: Decouple the logic from the drawing so we can also do things like nobs without having to
/// repeat everything
/// TODO: Add WidgetInfo annotations for accessibility
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct ParamSlider<'a, P: Param> {
param: &'a P,
setter: &'a ParamSetter<'a>,
draw_value: bool,
slider_width: Option<f32>,
/// Will be set in the `ui()` function so we can request keyboard input focus on Alt+click.
keyboard_focus_id: Option<egui::Id>,
}
impl<'a, P: Param> ParamSlider<'a, P> {
/// Create a new slider for a parameter. Use the other methods to modify the slider before
/// passing it to [`Ui::add()`].
pub fn for_param(param: &'a P, setter: &'a ParamSetter<'a>) -> Self {
Self {
param,
setter,
draw_value: true,
slider_width: None,
keyboard_focus_id: None,
}
}
/// Don't draw the text slider's current value after the slider.
pub fn without_value(mut self) -> Self {
self.draw_value = false;
self
}
/// Set a custom width for the slider.
pub fn with_width(mut self, width: f32) -> Self {
self.slider_width = Some(width);
self
}
fn plain_value(&self) -> P::Plain {
self.param.modulated_plain_value()
}
fn normalized_value(&self) -> f32 {
self.param.modulated_normalized_value()
}
fn string_value(&self) -> String {
self.param.to_string()
}
/// Enable the keyboard entry part of the widget.
fn begin_keyboard_entry(&self, ui: &Ui) {
ui.memory_mut(|mem| mem.request_focus(self.keyboard_focus_id.unwrap()));
// Always initialize the field to the current value, that seems nicer than having to
// being typing from scratch
let value_entry_mutex = ui.memory_mut(|mem| {
mem.data
.get_temp_mut_or_default::<Arc<Mutex<String>>>(*VALUE_ENTRY_MEMORY_ID)
.clone()
});
*value_entry_mutex.lock() = self.string_value();
}
fn keyboard_entry_active(&self, ui: &Ui) -> bool {
ui.memory(|mem| mem.has_focus(self.keyboard_focus_id.unwrap()))
}
fn begin_drag(&self) {
self.setter.begin_set_parameter(self.param);
}
fn set_normalized_value(&self, normalized: f32) {
// This snaps to the nearest plain value if the parameter is stepped in some way.
// TODO: As an optimization, we could add a `const CONTINUOUS: bool` to the parameter to
// avoid this normalized->plain->normalized conversion for parameters that don't need
// it
let value = self.param.preview_plain(normalized);
if value != self.plain_value() {
self.setter.set_parameter(self.param, value);
}
}
/// Begin and end drag still need to be called when using this. Returns `false` if the string
/// could no tbe parsed.
fn set_from_string(&self, string: &str) -> bool {
match self.param.string_to_normalized_value(string) {
Some(normalized_value) => {
self.set_normalized_value(normalized_value);
true
}
None => false,
}
}
/// Begin and end drag still need to be called when using this..
fn reset_param(&self) {
self.setter
.set_parameter(self.param, self.param.default_plain_value());
}
fn granular_drag(&self, ui: &Ui, drag_delta: Vec2) {
// Remember the intial position when we started with the granular drag. This value gets
// reset whenever we have a normal itneraction with the slider.
let start_value = if Self::get_drag_amount_memory(ui) == 0.0 {
Self::set_drag_normalized_start_value_memory(ui, self.normalized_value());
self.normalized_value()
} else {
Self::get_drag_normalized_start_value_memory(ui)
};
let total_drag_distance = drag_delta.x + Self::get_drag_amount_memory(ui);
Self::set_drag_amount_memory(ui, total_drag_distance);
self.set_normalized_value(
(start_value + (total_drag_distance * GRANULAR_DRAG_MULTIPLIER)).clamp(0.0, 1.0),
);
}
fn end_drag(&self) {
self.setter.end_set_parameter(self.param);
}
fn get_drag_normalized_start_value_memory(ui: &Ui) -> f32 {
ui.memory(|mem| {
mem.data
.get_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID)
.unwrap_or(0.5)
})
}
fn set_drag_normalized_start_value_memory(ui: &Ui, amount: f32) {
ui.memory_mut(|mem| {
mem.data
.insert_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID, amount)
});
}
fn get_drag_amount_memory(ui: &Ui) -> f32 {
ui.memory(|mem| mem.data.get_temp(*DRAG_AMOUNT_MEMORY_ID).unwrap_or(0.0))
}
fn set_drag_amount_memory(ui: &Ui, amount: f32) {
ui.memory_mut(|mem| mem.data.insert_temp(*DRAG_AMOUNT_MEMORY_ID, amount));
}
fn slider_ui(&self, ui: &Ui, response: &mut Response) {
// Handle user input
// TODO: Optionally (since it can be annoying) add scrolling behind a builder option
if response.drag_started() {
// When beginning a drag or dragging normally, reset the memory used to keep track of
// our granular drag
self.begin_drag();
Self::set_drag_amount_memory(ui, 0.0);
}
if let Some(click_pos) = response.interact_pointer_pos() {
if ui.input(|i| i.modifiers.command) {
// Like double clicking, Ctrl+Click should reset the parameter
self.reset_param();
response.mark_changed();
// // FIXME: This releases the focus again when you release the mouse button without
// // moving the mouse a bit for some reason
// } else if ui.input().modifiers.alt && self.draw_value {
// // Allow typing in the value on an Alt+Click. Right now this is shown as part of the
// // value field, so it only makes sense when we're drawing that.
// self.begin_keyboard_entry(ui);
} else if ui.input(|i| i.modifiers.shift) {
// And shift dragging should switch to a more granulra input method
self.granular_drag(ui, response.drag_delta());
response.mark_changed();
} else {
let proportion =
emath::remap_clamp(click_pos.x, response.rect.x_range(), 0.0..=1.0) as f64;
self.set_normalized_value(proportion as f32);
response.mark_changed();
Self::set_drag_amount_memory(ui, 0.0);
}
}
if response.double_clicked() {
self.reset_param();
response.mark_changed();
}
if response.drag_stopped() {
self.end_drag();
}
// And finally draw the thing
if ui.is_rect_visible(response.rect) {
// We'll do a flat widget with background -> filled foreground -> slight border
ui.painter()
.rect_filled(response.rect, 0.0, ui.visuals().widgets.inactive.bg_fill);
let filled_proportion = self.normalized_value();
if filled_proportion > 0.0 {
let mut filled_rect = response.rect;
filled_rect.set_width(response.rect.width() * filled_proportion);
let filled_bg = if response.dragged() {
util::add_hsv(ui.visuals().selection.bg_fill, 0.0, -0.1, 0.1)
} else {
ui.visuals().selection.bg_fill
};
ui.painter().rect_filled(filled_rect, 0.0, filled_bg);
}
ui.painter().rect_stroke(
response.rect,
0.0,
Stroke::new(1.0, ui.visuals().widgets.active.bg_fill),
egui::StrokeKind::Middle,
);
}
}
fn value_ui(&self, ui: &mut Ui) {
let visuals = ui.visuals().widgets.inactive;
let should_draw_frame = ui.visuals().button_frame;
let padding = ui.spacing().button_padding;
// Either show the parameter's label, or show a text entry field if the parameter's label
// has been clicked on
let keyboard_focus_id = self.keyboard_focus_id.unwrap();
if self.keyboard_entry_active(ui) {
let value_entry_mutex = ui.memory_mut(|mem| {
mem.data
.get_temp_mut_or_default::<Arc<Mutex<String>>>(*VALUE_ENTRY_MEMORY_ID)
.clone()
});
let mut value_entry = value_entry_mutex.lock();
ui.add(
TextEdit::singleline(&mut *value_entry)
.id(keyboard_focus_id)
.font(TextStyle::Monospace),
);
if ui.input(|i| i.key_pressed(Key::Escape)) {
// Cancel when pressing escape
ui.memory_mut(|mem| mem.surrender_focus(keyboard_focus_id));
} else if ui.input(|i| i.key_pressed(Key::Enter)) {
// And try to set the value by string when pressing enter
self.begin_drag();
self.set_from_string(&value_entry);
self.end_drag();
ui.memory_mut(|mem| mem.surrender_focus(keyboard_focus_id));
}
} else {
let text = WidgetText::from(self.string_value()).into_galley(
ui,
None,
ui.available_width() - (padding.x * 2.0),
TextStyle::Button,
);
let response = ui.allocate_response(text.size() + (padding * 2.0), Sense::click());
if response.clicked() {
self.begin_keyboard_entry(ui);
}
if ui.is_rect_visible(response.rect) {
if should_draw_frame {
let fill = visuals.bg_fill;
let stroke = visuals.bg_stroke;
ui.painter().rect(
response.rect.expand(visuals.expansion),
visuals.corner_radius,
fill,
stroke,
egui::StrokeKind::Middle,
);
}
let text_pos = ui
.layout()
.align_size_within_rect(text.size(), response.rect.shrink2(padding))
.min;
ui.painter().add(egui::epaint::TextShape::new(
text_pos,
text,
visuals.fg_stroke.color,
));
}
}
}
}
impl<P: Param> Widget for ParamSlider<'_, P> {
fn ui(mut self, ui: &mut Ui) -> Response {
let slider_width = self
.slider_width
.unwrap_or_else(|| ui.spacing().slider_width);
ui.horizontal(|ui| {
// Allocate space, but add some padding on the top and bottom to make it look a bit slimmer.
let height = ui
.text_style_height(&TextStyle::Body)
.max(ui.spacing().interact_size.y * 0.8);
let slider_height = (height * 0.8).round_to_pixels(ui.painter().pixels_per_point());
let mut response = ui
.vertical(|ui| {
ui.allocate_space(vec2(slider_width, (height - slider_height) / 2.0));
let response = ui.allocate_response(
vec2(slider_width, slider_height),
Sense::click_and_drag(),
);
let (kb_edit_id, _) =
ui.allocate_space(vec2(slider_width, (height - slider_height) / 2.0));
// Allocate an automatic ID for keeping track of keyboard focus state
// FIXME: There doesn't seem to be a way to generate IDs in the public API, not sure how
// you're supposed to do this
self.keyboard_focus_id = Some(kb_edit_id);
response
})
.inner;
self.slider_ui(ui, &mut response);
if self.draw_value {
self.value_ui(ui);
}
response
})
.inner
}
}

View File

@@ -0,0 +1,21 @@
//! Utilities for creating these widgets.
use egui_baseview::egui::{self, Color32};
/// Additively modify the hue, saturation, and lightness [0, 1] values of a color.
pub fn add_hsv(color: Color32, h: f32, s: f32, v: f32) -> Color32 {
let mut hsv = egui::epaint::Hsva::from(color);
hsv.h += h;
hsv.s += s;
hsv.v += v;
hsv.into()
}
/// Multiplicatively modify the hue, saturation, and lightness [0, 1] values of a color.
pub fn scale_hsv(color: Color32, h: f32, s: f32, v: f32) -> Color32 {
let mut hsv = egui::epaint::Hsva::from(color);
hsv.h *= h;
hsv.s *= s;
hsv.v *= v;
hsv.into()
}