Trying to clena the mess opened by plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
This commit is contained in:
Submodule .cargo/baseview deleted from 237d323c72
Submodule .cargo/egui-baseview deleted from c5ad2f4bd0
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "crates/plugins", "crates/baseview", "crates/egui-baseview", "xtask"]
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.9"
|
||||
@@ -92,11 +92,14 @@ codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[patch."https://github.com/robbert-vdh/nih-plug"]
|
||||
nih_plug_egui = { path = "plugins/nih-plug-egui" }
|
||||
|
||||
[patch."https://github.com/BillyDM/egui-baseview.git"]
|
||||
egui-baseview = { path = "crates/egui-baseview" }
|
||||
egui-baseview = { path = "plugins/egui-baseview" }
|
||||
|
||||
[patch."https://github.com/RustAudio/baseview.git"]
|
||||
baseview = { path = "crates/baseview" }
|
||||
baseview = { path = "plugins/baseview" }
|
||||
|
||||
[package.metadata.bundle.bin.cagire-desktop]
|
||||
name = "Cagire"
|
||||
|
||||
@@ -40,6 +40,8 @@ pub enum MouseEvent {
|
||||
CursorMoved {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The screen-absolute logical coordinates of the mouse position
|
||||
screen_position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
@@ -81,6 +83,8 @@ pub enum MouseEvent {
|
||||
DragEntered {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The screen-absolute logical coordinates of the mouse position
|
||||
screen_position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
/// Data being dragged
|
||||
@@ -90,6 +94,8 @@ pub enum MouseEvent {
|
||||
DragMoved {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The screen-absolute logical coordinates of the mouse position
|
||||
screen_position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
/// Data being dragged
|
||||
@@ -101,6 +107,8 @@ pub enum MouseEvent {
|
||||
DragDropped {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The screen-absolute logical coordinates of the mouse position
|
||||
screen_position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
/// Data being dragged
|
||||
@@ -139,6 +139,7 @@ impl GlContext {
|
||||
pub(crate) fn resize(&self, size: NSSize) {
|
||||
unsafe { NSView::setFrameSize(self.view, size) };
|
||||
unsafe {
|
||||
let _: () = msg_send![self.context, update];
|
||||
let _: () = msg_send![self.view, setNeedsDisplay: YES];
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,11 @@ unsafe fn create_view_class() -> &'static Class {
|
||||
view_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
class.add_method(
|
||||
sel!(setFrameSize:),
|
||||
set_frame_size as extern "C" fn(&Object, Sel, NSSize),
|
||||
);
|
||||
|
||||
class.add_method(
|
||||
sel!(draggingEntered:),
|
||||
dragging_entered as extern "C" fn(&Object, Sel, id) -> NSUInteger,
|
||||
@@ -390,6 +395,54 @@ extern "C" fn update_tracking_areas(this: &Object, _self: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn set_frame_size(this: &Object, _: Sel, new_size: NSSize) {
|
||||
unsafe {
|
||||
let superclass = msg_send![this, superclass];
|
||||
let () = msg_send![super(this, superclass), setFrameSize: new_size];
|
||||
}
|
||||
|
||||
let state_ptr: *const c_void = unsafe { *this.get_ivar(BASEVIEW_STATE_IVAR) };
|
||||
if state_ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
let scale_factor = unsafe {
|
||||
let ns_window: *mut Object = msg_send![this, window];
|
||||
if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) }
|
||||
};
|
||||
|
||||
let new_window_info = WindowInfo::from_logical_size(
|
||||
Size::new(new_size.width, new_size.height),
|
||||
scale_factor,
|
||||
);
|
||||
|
||||
let old_info = state.window_info.get();
|
||||
if new_window_info.physical_size() != old_info.physical_size() {
|
||||
state.window_info.set(new_window_info);
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
if let Some(gl_context) = &state.window_inner.gl_context {
|
||||
gl_context.resize(new_size);
|
||||
}
|
||||
|
||||
state.trigger_deferrable_event(Event::Window(WindowEvent::Resized(new_window_info)));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_screen_position() -> Point {
|
||||
unsafe {
|
||||
let screen_point: NSPoint = msg_send![class!(NSEvent), mouseLocation];
|
||||
let main_screen: id = msg_send![class!(NSScreen), mainScreen];
|
||||
let screen_frame: NSRect = msg_send![main_screen, frame];
|
||||
Point {
|
||||
x: screen_point.x,
|
||||
y: screen_frame.size.height - screen_point.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: id) {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
@@ -401,9 +454,11 @@ extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: id) {
|
||||
let modifiers = unsafe { NSEvent::modifierFlags(event) };
|
||||
|
||||
let position = Point { x: point.x, y: point.y };
|
||||
let screen_position = get_screen_position();
|
||||
|
||||
state.trigger_event(Event::Mouse(MouseEvent::CursorMoved {
|
||||
position,
|
||||
screen_position,
|
||||
modifiers: make_modifiers(modifiers),
|
||||
}));
|
||||
}
|
||||
@@ -430,9 +485,9 @@ extern "C" fn scroll_wheel(this: &Object, _: Sel, event: id) {
|
||||
}));
|
||||
}
|
||||
|
||||
fn get_drag_position(sender: id) -> Point {
|
||||
fn get_drag_position(sender: id) -> (Point, Point) {
|
||||
let point: NSPoint = unsafe { msg_send![sender, draggingLocation] };
|
||||
Point::new(point.x, point.y)
|
||||
(Point::new(point.x, point.y), get_screen_position())
|
||||
}
|
||||
|
||||
fn get_drop_data(sender: id) -> DropData {
|
||||
@@ -473,9 +528,11 @@ extern "C" fn dragging_entered(this: &Object, _sel: Sel, sender: id) -> NSUInteg
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
let modifiers = state.keyboard_state().last_mods();
|
||||
let drop_data = get_drop_data(sender);
|
||||
let (position, screen_position) = get_drag_position(sender);
|
||||
|
||||
let event = MouseEvent::DragEntered {
|
||||
position: get_drag_position(sender),
|
||||
position,
|
||||
screen_position,
|
||||
modifiers: make_modifiers(modifiers),
|
||||
data: drop_data,
|
||||
};
|
||||
@@ -487,9 +544,11 @@ extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteg
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
let modifiers = state.keyboard_state().last_mods();
|
||||
let drop_data = get_drop_data(sender);
|
||||
let (position, screen_position) = get_drag_position(sender);
|
||||
|
||||
let event = MouseEvent::DragMoved {
|
||||
position: get_drag_position(sender),
|
||||
position,
|
||||
screen_position,
|
||||
modifiers: make_modifiers(modifiers),
|
||||
data: drop_data,
|
||||
};
|
||||
@@ -508,9 +567,11 @@ extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: id) -> BO
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
let modifiers = state.keyboard_state().last_mods();
|
||||
let drop_data = get_drop_data(sender);
|
||||
let (position, screen_position) = get_drag_position(sender);
|
||||
|
||||
let event = MouseEvent::DragDropped {
|
||||
position: get_drag_position(sender),
|
||||
position,
|
||||
screen_position,
|
||||
modifiers: make_modifiers(modifiers),
|
||||
data: drop_data,
|
||||
};
|
||||
@@ -65,7 +65,7 @@ pub(super) struct WindowInner {
|
||||
ns_view: id,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context: Option<GlContext>,
|
||||
pub(super) gl_context: Option<GlContext>,
|
||||
}
|
||||
|
||||
impl WindowInner {
|
||||
@@ -311,29 +311,32 @@ impl<'a> Window<'a> {
|
||||
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
if self.inner.open.get() {
|
||||
// NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even
|
||||
// though the size is in fractional pixels.
|
||||
let size = NSSize::new(size.width.round(), size.height.round());
|
||||
|
||||
unsafe { NSView::setFrameSize(self.inner.ns_view, size) };
|
||||
unsafe {
|
||||
let _: () = msg_send![self.inner.ns_view, setNeedsDisplay: YES];
|
||||
}
|
||||
|
||||
// When using OpenGL the `NSOpenGLView` needs to be resized separately? Why? Because
|
||||
// macOS.
|
||||
#[cfg(feature = "opengl")]
|
||||
if let Some(gl_context) = &self.inner.gl_context {
|
||||
gl_context.resize(size);
|
||||
}
|
||||
|
||||
// If this is a standalone window then we'll also need to resize the window itself
|
||||
if let Some(ns_window) = self.inner.ns_window.get() {
|
||||
unsafe { NSWindow::setContentSize_(ns_window, size) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn physical_size(&self) -> crate::PhySize {
|
||||
unsafe {
|
||||
let frame: NSRect = NSView::frame(self.inner.ns_view);
|
||||
let ns_window: *mut Object = msg_send![self.inner.ns_view, window];
|
||||
let scale: f64 = if ns_window.is_null() {
|
||||
1.0
|
||||
} else {
|
||||
NSWindow::backingScaleFactor(ns_window)
|
||||
};
|
||||
crate::PhySize {
|
||||
width: (frame.size.width * scale).round() as u32,
|
||||
height: (frame.size.height * scale).round() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&mut self, _mouse_cursor: MouseCursor) {
|
||||
todo!()
|
||||
}
|
||||
@@ -73,6 +73,7 @@ pub(super) struct DropTarget {
|
||||
// These are cached since DragOver and DragLeave callbacks don't provide them,
|
||||
// and handling drag move events gets awkward on the client end otherwise
|
||||
drag_position: Point,
|
||||
drag_screen_position: Point,
|
||||
drop_data: DropData,
|
||||
}
|
||||
|
||||
@@ -84,6 +85,7 @@ impl DropTarget {
|
||||
window_state,
|
||||
|
||||
drag_position: Point::new(0.0, 0.0),
|
||||
drag_screen_position: Point::new(0.0, 0.0),
|
||||
drop_data: DropData::None,
|
||||
}
|
||||
}
|
||||
@@ -117,6 +119,7 @@ impl DropTarget {
|
||||
let Some(window_state) = self.window_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
self.drag_screen_position = Point::new(pt.x as f64, pt.y as f64);
|
||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
||||
unsafe { ScreenToClient(window_state.hwnd, &mut pt as *mut POINT) };
|
||||
let phy_point = PhyPoint::new(pt.x, pt.y);
|
||||
@@ -216,6 +219,7 @@ impl DropTarget {
|
||||
|
||||
let event = MouseEvent::DragEntered {
|
||||
position: drop_target.drag_position,
|
||||
screen_position: drop_target.drag_screen_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
@@ -240,6 +244,7 @@ impl DropTarget {
|
||||
|
||||
let event = MouseEvent::DragMoved {
|
||||
position: drop_target.drag_position,
|
||||
screen_position: drop_target.drag_screen_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
@@ -272,6 +277,7 @@ impl DropTarget {
|
||||
|
||||
let event = MouseEvent::DragDropped {
|
||||
position: drop_target.drag_position,
|
||||
screen_position: drop_target.drag_screen_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
@@ -1,14 +1,15 @@
|
||||
use winapi::shared::guiddef::GUID;
|
||||
use winapi::shared::minwindef::{ATOM, FALSE, LOWORD, LPARAM, LRESULT, UINT, WPARAM};
|
||||
use winapi::shared::windef::{HWND, RECT};
|
||||
use winapi::shared::windef::{HWND, POINT, RECT};
|
||||
use winapi::um::combaseapi::CoCreateGuid;
|
||||
use winapi::um::ole2::{OleInitialize, RegisterDragDrop, RevokeDragDrop};
|
||||
use winapi::um::oleidl::LPDROPTARGET;
|
||||
use winapi::um::winuser::{
|
||||
AdjustWindowRectEx, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW,
|
||||
GetDpiForWindow, GetFocus, GetMessageW, GetWindowLongPtrW, LoadCursorW, PostMessageW,
|
||||
RegisterClassW, ReleaseCapture, SetCapture, SetCursor, SetFocus, SetProcessDpiAwarenessContext,
|
||||
SetTimer, SetWindowLongPtrW, SetWindowPos, TrackMouseEvent, TranslateMessage, UnregisterClassW,
|
||||
GetCursorPos, GetDpiForWindow, GetFocus, GetMessageW, GetWindowLongPtrW, LoadCursorW,
|
||||
PostMessageW, RegisterClassW, ReleaseCapture, SetCapture, SetCursor, SetFocus,
|
||||
SetProcessDpiAwarenessContext, SetTimer, SetWindowLongPtrW, SetWindowPos, TrackMouseEvent,
|
||||
TranslateMessage, UnregisterClassW,
|
||||
CS_OWNDC, GET_XBUTTON_WPARAM, GWLP_USERDATA, HTCLIENT, IDC_ARROW, MSG, SWP_NOMOVE,
|
||||
SWP_NOZORDER, TRACKMOUSEEVENT, WHEEL_DELTA, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DPICHANGED,
|
||||
WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
|
||||
@@ -35,8 +36,8 @@ const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1;
|
||||
|
||||
use crate::win::hook::{self, KeyboardHookHandle};
|
||||
use crate::{
|
||||
Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent,
|
||||
WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy,
|
||||
Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, Point, ScrollDelta, Size,
|
||||
WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy,
|
||||
};
|
||||
|
||||
use super::cursor::cursor_to_lpcwstr;
|
||||
@@ -202,8 +203,14 @@ unsafe fn wnd_proc_inner(
|
||||
|
||||
let physical_pos = PhyPoint { x, y };
|
||||
let logical_pos = physical_pos.to_logical(&window_state.window_info.borrow());
|
||||
|
||||
let mut screen_pt = POINT { x: 0, y: 0 };
|
||||
GetCursorPos(&mut screen_pt);
|
||||
let screen_position = Point::new(screen_pt.x as f64, screen_pt.y as f64);
|
||||
|
||||
let move_event = Event::Mouse(MouseEvent::CursorMoved {
|
||||
position: logical_pos,
|
||||
screen_position,
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
@@ -822,6 +829,10 @@ impl Window<'_> {
|
||||
self.state.deferred_tasks.borrow_mut().push_back(task);
|
||||
}
|
||||
|
||||
pub fn physical_size(&self) -> PhySize {
|
||||
self.state.window_info().physical_size()
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&mut self, mouse_cursor: MouseCursor) {
|
||||
self.state.cursor_icon.set(mouse_cursor);
|
||||
unsafe {
|
||||
@@ -6,7 +6,7 @@ use raw_window_handle::{
|
||||
|
||||
use crate::event::{Event, EventStatus};
|
||||
use crate::window_open_options::WindowOpenOptions;
|
||||
use crate::{MouseCursor, Size};
|
||||
use crate::{MouseCursor, PhySize, Size};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::macos as platform;
|
||||
@@ -92,6 +92,11 @@ impl<'a> Window<'a> {
|
||||
self.window.close();
|
||||
}
|
||||
|
||||
/// Returns the current physical size of the window by querying the OS directly.
|
||||
pub fn physical_size(&self) -> PhySize {
|
||||
self.window.physical_size()
|
||||
}
|
||||
|
||||
/// Resize the window to the given size. The size is always in logical pixels. DPI scaling will
|
||||
/// automatically be accounted for.
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
@@ -175,11 +175,14 @@ impl EventLoop {
|
||||
XEvent::MotionNotify(event) => {
|
||||
let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32);
|
||||
let logical_pos = physical_pos.to_logical(&self.window.window_info);
|
||||
let screen_physical = PhyPoint::new(event.root_x as i32, event.root_y as i32);
|
||||
let screen_position = screen_physical.to_logical(&self.window.window_info);
|
||||
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::CursorMoved {
|
||||
position: logical_pos,
|
||||
screen_position,
|
||||
modifiers: key_mods(event.state),
|
||||
}),
|
||||
);
|
||||
@@ -194,10 +197,13 @@ impl EventLoop {
|
||||
// we generate a CursorMoved as well, so the mouse position from here isn't lost
|
||||
let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32);
|
||||
let logical_pos = physical_pos.to_logical(&self.window.window_info);
|
||||
let screen_physical = PhyPoint::new(event.root_x as i32, event.root_y as i32);
|
||||
let screen_position = screen_physical.to_logical(&self.window.window_info);
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::CursorMoved {
|
||||
position: logical_pos,
|
||||
screen_position,
|
||||
modifiers: key_mods(event.state),
|
||||
}),
|
||||
);
|
||||
@@ -296,6 +296,10 @@ impl<'a> Window<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn physical_size(&self) -> crate::PhySize {
|
||||
self.inner.window_info.physical_size()
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&self, mouse_cursor: MouseCursor) {
|
||||
if self.inner.mouse_cursor.get() == mouse_cursor {
|
||||
return;
|
||||
@@ -11,9 +11,9 @@ crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
cagire = { path = "../.." }
|
||||
cagire-forth = { path = "../forth" }
|
||||
cagire-project = { path = "../project" }
|
||||
cagire-ratatui = { path = "../ratatui" }
|
||||
cagire-forth = { path = "../../crates/forth" }
|
||||
cagire-project = { path = "../../crates/project" }
|
||||
cagire-ratatui = { path = "../../crates/ratatui" }
|
||||
doux = { git = "https://github.com/Bubobubobubobubo/doux", features = ["native"] }
|
||||
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||
@@ -41,6 +41,28 @@ enum FontChoice {
|
||||
}
|
||||
|
||||
impl FontChoice {
|
||||
fn from_setting(s: &str) -> Self {
|
||||
match s {
|
||||
"6x13" => Self::Size6x13,
|
||||
"7x13" => Self::Size7x13,
|
||||
"9x15" => Self::Size9x15,
|
||||
"9x18" => Self::Size9x18,
|
||||
"10x20" => Self::Size10x20,
|
||||
_ => Self::Size8x13,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20",
|
||||
}
|
||||
}
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13 (Compact)",
|
||||
@@ -117,6 +139,7 @@ pub fn create_editor(
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
) -> Option<Box<dyn Editor>> {
|
||||
let egui_state_clone = Arc::clone(&egui_state);
|
||||
create_egui_editor(
|
||||
egui_state,
|
||||
None::<EditorState>,
|
||||
@@ -125,6 +148,9 @@ pub fn create_editor(
|
||||
panel_fill: egui::Color32::BLACK,
|
||||
..egui::Visuals::dark()
|
||||
});
|
||||
ctx.style_mut(|style| {
|
||||
style.spacing.window_margin = egui::Margin::ZERO;
|
||||
});
|
||||
},
|
||||
move |ctx, _setter, state| {
|
||||
let editor = state.get_or_insert_with(|| {
|
||||
@@ -144,6 +170,11 @@ pub fn create_editor(
|
||||
app.project_state.project = params.project.lock().clone();
|
||||
app.mark_all_patterns_dirty();
|
||||
|
||||
// Init window size from egui state
|
||||
let (w, h) = egui_state_clone.size();
|
||||
app.ui.window_width = w;
|
||||
app.ui.window_height = h;
|
||||
|
||||
EditorState {
|
||||
app,
|
||||
terminal: create_terminal(FontChoice::Size8x13),
|
||||
@@ -221,6 +252,23 @@ pub fn create_editor(
|
||||
|
||||
cagire::state::effects::tick_effects(&mut editor.app.ui, editor.app.page);
|
||||
|
||||
// Sync font/zoom from App state (options page or context menu may have changed them)
|
||||
let desired_font = FontChoice::from_setting(&editor.app.ui.font);
|
||||
if desired_font != editor.current_font {
|
||||
editor.terminal = create_terminal(desired_font);
|
||||
editor.current_font = desired_font;
|
||||
}
|
||||
if (editor.app.ui.zoom_factor - editor.zoom_factor).abs() > 0.01 {
|
||||
editor.zoom_factor = editor.app.ui.zoom_factor;
|
||||
ctx.set_zoom_factor(editor.zoom_factor);
|
||||
}
|
||||
|
||||
// Sync window size: if app state differs from egui state, request resize
|
||||
let (cur_w, cur_h) = egui_state_clone.size();
|
||||
if editor.app.ui.window_width != cur_w || editor.app.ui.window_height != cur_h {
|
||||
egui_state_clone.set_requested_size((editor.app.ui.window_width, editor.app.ui.window_height));
|
||||
}
|
||||
|
||||
let elapsed = editor.last_frame.elapsed();
|
||||
editor.last_frame = Instant::now();
|
||||
|
||||
@@ -236,7 +284,7 @@ pub fn create_editor(
|
||||
let current_zoom = editor.zoom_factor;
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE.fill(egui::Color32::BLACK))
|
||||
.frame(egui::Frame::NONE)
|
||||
.show(ctx, |ui| {
|
||||
ui.add(editor.terminal.backend_mut());
|
||||
|
||||
@@ -249,6 +297,7 @@ pub fn create_editor(
|
||||
ui.menu_button("Font", |ui| {
|
||||
for choice in FontChoice::ALL {
|
||||
if ui.selectable_label(current_font == choice, choice.label()).clicked() {
|
||||
editor.app.ui.font = choice.to_setting().to_string();
|
||||
editor.terminal = create_terminal(choice);
|
||||
editor.current_font = choice;
|
||||
ui.close();
|
||||
@@ -260,6 +309,7 @@ pub fn create_editor(
|
||||
let selected = (current_zoom - level).abs() < 0.01;
|
||||
let label = format!("{:.0}%", level * 100.0);
|
||||
if ui.selectable_label(selected, label).clicked() {
|
||||
editor.app.ui.zoom_factor = level;
|
||||
editor.zoom_factor = level;
|
||||
ctx.set_zoom_factor(level);
|
||||
ui.close();
|
||||
@@ -372,6 +372,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let actual_size = window.physical_size();
|
||||
if actual_size != self.physical_size {
|
||||
self.physical_size = actual_size;
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let do_repaint_now = if let Some(t) = self.repaint_after {
|
||||
now >= t || viewport_output.repaint_delay.is_zero()
|
||||
@@ -436,6 +441,7 @@ where
|
||||
baseview::MouseEvent::CursorMoved {
|
||||
position,
|
||||
modifiers,
|
||||
..
|
||||
} => {
|
||||
self.update_modifiers(modifiers);
|
||||
|
||||
25
plugins/nih-plug-egui/Cargo.toml
Normal file
25
plugins/nih-plug-egui/Cargo.toml
Normal 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"] }
|
||||
194
plugins/nih-plug-egui/src/editor.rs
Normal file
194
plugins/nih-plug-egui/src/editor.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
119
plugins/nih-plug-egui/src/lib.rs
Normal file
119
plugins/nih-plug-egui/src/lib.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
81
plugins/nih-plug-egui/src/resizable_window.rs
Normal file
81
plugins/nih-plug-egui/src/resizable_window.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
12
plugins/nih-plug-egui/src/widgets.rs
Normal file
12
plugins/nih-plug-egui/src/widgets.rs
Normal 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;
|
||||
72
plugins/nih-plug-egui/src/widgets/generic_ui.rs
Normal file
72
plugins/nih-plug-egui/src/widgets/generic_ui.rs
Normal 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, ¶m_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));
|
||||
}
|
||||
}
|
||||
357
plugins/nih-plug-egui/src/widgets/param_slider.rs
Normal file
357
plugins/nih-plug-egui/src/widgets/param_slider.rs
Normal 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
|
||||
}
|
||||
}
|
||||
21
plugins/nih-plug-egui/src/widgets/util.rs
Normal file
21
plugins/nih-plug-egui/src/widgets/util.rs
Normal 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()
|
||||
}
|
||||
@@ -309,6 +309,12 @@ impl App {
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
}
|
||||
AppCommand::SetFont(f) => self.ui.font = f,
|
||||
AppCommand::SetZoomFactor(z) => self.ui.zoom_factor = z,
|
||||
AppCommand::SetWindowSize(w, h) => {
|
||||
self.ui.window_width = w;
|
||||
self.ui.window_height = h;
|
||||
}
|
||||
AppCommand::ToggleLiveKeysFill => self.live_keys.flip_fill(),
|
||||
|
||||
// Panel
|
||||
@@ -375,8 +381,8 @@ impl App {
|
||||
AppCommand::AudioRefreshDevices => self.audio.refresh_devices(),
|
||||
|
||||
// Options page
|
||||
AppCommand::OptionsNextFocus => self.options.next_focus(),
|
||||
AppCommand::OptionsPrevFocus => self.options.prev_focus(),
|
||||
AppCommand::OptionsNextFocus => self.options.next_focus(self.plugin_mode),
|
||||
AppCommand::OptionsPrevFocus => self.options.prev_focus(self.plugin_mode),
|
||||
AppCommand::OptionsSetFocus(focus) => self.options.focus = focus,
|
||||
AppCommand::ToggleRefreshRate => self.audio.toggle_refresh_rate(),
|
||||
AppCommand::ToggleScope => self.audio.config.show_scope = !self.audio.config.show_scope,
|
||||
|
||||
@@ -30,7 +30,8 @@ impl App {
|
||||
layout: self.audio.config.layout,
|
||||
hue_rotation: self.ui.hue_rotation,
|
||||
onboarding_dismissed: self.ui.onboarding_dismissed.clone(),
|
||||
..Default::default()
|
||||
font: self.ui.font.clone(),
|
||||
zoom_factor: self.ui.zoom_factor,
|
||||
},
|
||||
link: crate::settings::LinkSettings {
|
||||
enabled: link.is_enabled(),
|
||||
|
||||
@@ -199,6 +199,9 @@ pub enum AppCommand {
|
||||
SetHueRotation(f32),
|
||||
ToggleRuntimeHighlight,
|
||||
ToggleCompletion,
|
||||
SetFont(String),
|
||||
SetZoomFactor(f32),
|
||||
SetWindowSize(u32, u32),
|
||||
|
||||
// Live keys
|
||||
ToggleLiveKeysFill,
|
||||
|
||||
@@ -88,6 +88,8 @@ pub fn init(args: InitArgs) -> Init {
|
||||
app.ui.hue_rotation = settings.display.hue_rotation;
|
||||
app.audio.config.layout = settings.display.layout;
|
||||
app.ui.onboarding_dismissed = settings.display.onboarding_dismissed.clone();
|
||||
app.ui.font = settings.display.font.clone();
|
||||
app.ui.zoom_factor = settings.display.zoom_factor;
|
||||
|
||||
let palette = settings.display.color_scheme.to_palette();
|
||||
let rotated =
|
||||
|
||||
@@ -675,8 +675,9 @@ fn handle_options_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect)
|
||||
}
|
||||
|
||||
let focus = ctx.app.options.focus;
|
||||
let focus_line = focus.line_index();
|
||||
let total_lines = 35;
|
||||
let plugin_mode = ctx.app.plugin_mode;
|
||||
let focus_line = focus.line_index(plugin_mode);
|
||||
let total_lines = if plugin_mode { 43 } else { 40 };
|
||||
let max_visible = padded.height as usize;
|
||||
|
||||
let scroll_offset = if total_lines <= max_visible {
|
||||
@@ -690,7 +691,7 @@ fn handle_options_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect)
|
||||
let relative_y = (row - padded.y) as usize;
|
||||
let abs_line = scroll_offset + relative_y;
|
||||
|
||||
if let Some(new_focus) = OptionsFocus::at_line(abs_line) {
|
||||
if let Some(new_focus) = OptionsFocus::at_line(abs_line, plugin_mode) {
|
||||
ctx.dispatch(AppCommand::OptionsSetFocus(new_focus));
|
||||
|
||||
// Value area starts at prefix(2) + label(20) = offset 22 from padded.x
|
||||
|
||||
@@ -26,6 +26,56 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
||||
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
|
||||
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
|
||||
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
|
||||
OptionsFocus::Font => {
|
||||
const FONTS: &[&str] = &["6x13", "7x13", "8x13", "9x15", "9x18", "10x20"];
|
||||
let pos = FONTS.iter().position(|f| *f == ctx.app.ui.font).unwrap_or(2);
|
||||
let new_pos = if right {
|
||||
(pos + 1) % FONTS.len()
|
||||
} else {
|
||||
(pos + FONTS.len() - 1) % FONTS.len()
|
||||
};
|
||||
ctx.dispatch(AppCommand::SetFont(FONTS[new_pos].to_string()));
|
||||
}
|
||||
OptionsFocus::ZoomFactor => {
|
||||
const ZOOMS: &[f32] = &[0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
|
||||
let pos = ZOOMS
|
||||
.iter()
|
||||
.position(|z| (z - ctx.app.ui.zoom_factor).abs() < 0.01)
|
||||
.unwrap_or(4);
|
||||
let new_pos = if right {
|
||||
(pos + 1) % ZOOMS.len()
|
||||
} else {
|
||||
(pos + ZOOMS.len() - 1) % ZOOMS.len()
|
||||
};
|
||||
ctx.dispatch(AppCommand::SetZoomFactor(ZOOMS[new_pos]));
|
||||
}
|
||||
OptionsFocus::WindowSize => {
|
||||
const WINDOW_SIZES: &[(u32, u32)] = &[
|
||||
(900, 600), (1050, 700), (1200, 800), (1350, 900), (1500, 1000),
|
||||
];
|
||||
let pos = WINDOW_SIZES
|
||||
.iter()
|
||||
.position(|&(w, h)| w == ctx.app.ui.window_width && h == ctx.app.ui.window_height)
|
||||
.unwrap_or_else(|| {
|
||||
WINDOW_SIZES
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|&(_, &(w, h))| {
|
||||
let dw = w as i64 - ctx.app.ui.window_width as i64;
|
||||
let dh = h as i64 - ctx.app.ui.window_height as i64;
|
||||
dw * dw + dh * dh
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(2)
|
||||
});
|
||||
let new_pos = if right {
|
||||
(pos + 1) % WINDOW_SIZES.len()
|
||||
} else {
|
||||
(pos + WINDOW_SIZES.len() - 1) % WINDOW_SIZES.len()
|
||||
};
|
||||
let (w, h) = WINDOW_SIZES[new_pos];
|
||||
ctx.dispatch(AppCommand::SetWindowSize(w, h));
|
||||
}
|
||||
OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
OptionsFocus::StartStopSync => ctx
|
||||
.link
|
||||
|
||||
@@ -11,6 +11,9 @@ pub enum OptionsFocus {
|
||||
ShowSpectrum,
|
||||
ShowCompletion,
|
||||
ShowPreview,
|
||||
Font,
|
||||
ZoomFactor,
|
||||
WindowSize,
|
||||
LinkEnabled,
|
||||
StartStopSync,
|
||||
Quantum,
|
||||
@@ -35,6 +38,9 @@ impl CyclicEnum for OptionsFocus {
|
||||
Self::ShowSpectrum,
|
||||
Self::ShowCompletion,
|
||||
Self::ShowPreview,
|
||||
Self::Font,
|
||||
Self::ZoomFactor,
|
||||
Self::WindowSize,
|
||||
Self::LinkEnabled,
|
||||
Self::StartStopSync,
|
||||
Self::Quantum,
|
||||
@@ -50,6 +56,8 @@ impl CyclicEnum for OptionsFocus {
|
||||
];
|
||||
}
|
||||
|
||||
// Line indices when Font/ZoomFactor are shown (plugin mode).
|
||||
// In terminal mode, Font/ZoomFactor are absent; all lines after ShowPreview shift up by 2.
|
||||
const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
|
||||
(OptionsFocus::ColorScheme, 2),
|
||||
(OptionsFocus::HueRotation, 3),
|
||||
@@ -59,34 +67,51 @@ const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
|
||||
(OptionsFocus::ShowSpectrum, 7),
|
||||
(OptionsFocus::ShowCompletion, 8),
|
||||
(OptionsFocus::ShowPreview, 9),
|
||||
(OptionsFocus::LinkEnabled, 13),
|
||||
(OptionsFocus::StartStopSync, 14),
|
||||
(OptionsFocus::Quantum, 15),
|
||||
(OptionsFocus::MidiOutput0, 25),
|
||||
(OptionsFocus::MidiOutput1, 26),
|
||||
(OptionsFocus::MidiOutput2, 27),
|
||||
(OptionsFocus::MidiOutput3, 28),
|
||||
(OptionsFocus::MidiInput0, 32),
|
||||
(OptionsFocus::MidiInput1, 33),
|
||||
(OptionsFocus::MidiInput2, 34),
|
||||
(OptionsFocus::MidiInput3, 35),
|
||||
(OptionsFocus::ResetOnboarding, 39),
|
||||
(OptionsFocus::Font, 10),
|
||||
(OptionsFocus::ZoomFactor, 11),
|
||||
(OptionsFocus::WindowSize, 12),
|
||||
(OptionsFocus::LinkEnabled, 16),
|
||||
(OptionsFocus::StartStopSync, 17),
|
||||
(OptionsFocus::Quantum, 18),
|
||||
(OptionsFocus::MidiOutput0, 28),
|
||||
(OptionsFocus::MidiOutput1, 29),
|
||||
(OptionsFocus::MidiOutput2, 30),
|
||||
(OptionsFocus::MidiOutput3, 31),
|
||||
(OptionsFocus::MidiInput0, 35),
|
||||
(OptionsFocus::MidiInput1, 36),
|
||||
(OptionsFocus::MidiInput2, 37),
|
||||
(OptionsFocus::MidiInput3, 38),
|
||||
(OptionsFocus::ResetOnboarding, 42),
|
||||
];
|
||||
|
||||
const PLUGIN_ONLY: &[OptionsFocus] = &[OptionsFocus::Font, OptionsFocus::ZoomFactor, OptionsFocus::WindowSize];
|
||||
|
||||
impl OptionsFocus {
|
||||
pub fn line_index(self) -> usize {
|
||||
FOCUS_LINES
|
||||
fn is_plugin_only(self) -> bool {
|
||||
PLUGIN_ONLY.contains(&self)
|
||||
}
|
||||
|
||||
pub fn line_index(self, plugin_mode: bool) -> usize {
|
||||
let base = FOCUS_LINES
|
||||
.iter()
|
||||
.find(|(f, _)| *f == self)
|
||||
.map(|(_, l)| *l)
|
||||
.unwrap_or(0)
|
||||
.unwrap_or(0);
|
||||
if plugin_mode || base <= 9 {
|
||||
base
|
||||
} else {
|
||||
base - 3
|
||||
}
|
||||
}
|
||||
|
||||
pub fn at_line(line: usize) -> Option<OptionsFocus> {
|
||||
FOCUS_LINES
|
||||
.iter()
|
||||
.find(|(_, l)| *l == line)
|
||||
.map(|(f, _)| *f)
|
||||
pub fn at_line(line: usize, plugin_mode: bool) -> Option<OptionsFocus> {
|
||||
FOCUS_LINES.iter().find_map(|(f, l)| {
|
||||
if f.is_plugin_only() && !plugin_mode {
|
||||
return None;
|
||||
}
|
||||
let effective = if plugin_mode || *l <= 9 { *l } else { *l - 3 };
|
||||
if effective == line { Some(*f) } else { None }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +121,25 @@ pub struct OptionsState {
|
||||
}
|
||||
|
||||
impl OptionsState {
|
||||
pub fn next_focus(&mut self) {
|
||||
self.focus = self.focus.next();
|
||||
pub fn next_focus(&mut self, plugin_mode: bool) {
|
||||
let mut f = self.focus;
|
||||
loop {
|
||||
f = f.next();
|
||||
if !f.is_plugin_only() || plugin_mode {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.focus = f;
|
||||
}
|
||||
|
||||
pub fn prev_focus(&mut self) {
|
||||
self.focus = self.focus.prev();
|
||||
pub fn prev_focus(&mut self, plugin_mode: bool) {
|
||||
let mut f = self.focus;
|
||||
loop {
|
||||
f = f.prev();
|
||||
if !f.is_plugin_only() || plugin_mode {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.focus = f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ pub struct UiState {
|
||||
pub prev_page: Page,
|
||||
pub prev_show_title: bool,
|
||||
pub onboarding_dismissed: Vec<String>,
|
||||
pub font: String,
|
||||
pub zoom_factor: f32,
|
||||
pub window_width: u32,
|
||||
pub window_height: u32,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -117,6 +121,10 @@ impl Default for UiState {
|
||||
prev_page: Page::default(),
|
||||
prev_show_title: true,
|
||||
onboarding_dismissed: Vec::new(),
|
||||
font: "8x13".to_string(),
|
||||
zoom_factor: 1.5,
|
||||
window_width: 1200,
|
||||
window_height: 800,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let onboarding_str = format!("{}/6 dismissed", app.ui.onboarding_dismissed.len());
|
||||
let hue_str = format!("{}°", app.ui.hue_rotation as i32);
|
||||
|
||||
let lines: Vec<Line> = vec![
|
||||
let mut lines: Vec<Line> = vec![
|
||||
render_section_header("DISPLAY", &theme),
|
||||
render_divider(content_width, &theme),
|
||||
render_option_line(
|
||||
@@ -168,7 +168,31 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
focus == OptionsFocus::ShowPreview,
|
||||
&theme,
|
||||
),
|
||||
Line::from(""),
|
||||
];
|
||||
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
|
||||
let window_str = format!("{}x{}", app.ui.window_width, app.ui.window_height);
|
||||
if app.plugin_mode {
|
||||
lines.push(render_option_line(
|
||||
"Font",
|
||||
&app.ui.font,
|
||||
focus == OptionsFocus::Font,
|
||||
&theme,
|
||||
));
|
||||
lines.push(render_option_line(
|
||||
"Zoom",
|
||||
&zoom_str,
|
||||
focus == OptionsFocus::ZoomFactor,
|
||||
&theme,
|
||||
));
|
||||
lines.push(render_option_line(
|
||||
"Window",
|
||||
&window_str,
|
||||
focus == OptionsFocus::WindowSize,
|
||||
&theme,
|
||||
));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.extend([
|
||||
link_header,
|
||||
render_divider(content_width, &theme),
|
||||
render_option_line(
|
||||
@@ -217,12 +241,12 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
focus == OptionsFocus::ResetOnboarding,
|
||||
&theme,
|
||||
),
|
||||
];
|
||||
]);
|
||||
|
||||
let total_lines = lines.len();
|
||||
let max_visible = padded.height as usize;
|
||||
|
||||
let focus_line = focus.line_index();
|
||||
let focus_line = focus.line_index(app.plugin_mode);
|
||||
|
||||
let scroll_offset = if total_lines <= max_visible {
|
||||
0
|
||||
|
||||
Reference in New Issue
Block a user