Reorganize top-level directories with clearer naming convention
app -> app-desktop-macos, presets -> app-presets, server -> remote-server, daw-config-reaper -> osc-config-daw, plugin-reaper-realearn -> plugin-reaper-relearn. Updated run.py and presets.py path references accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
use base::hash_util::NonCryptoHashMap;
|
||||
use reaper_common_types::RgbColor;
|
||||
use reaper_low::Swell;
|
||||
use reaper_medium::Hbrush;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BrushCache {
|
||||
brushes: RefCell<NonCryptoHashMap<BrushDescriptor, Option<Brush>>>,
|
||||
}
|
||||
|
||||
impl BrushCache {
|
||||
/// Returns a handle to a cached brush according to the given descriptor.
|
||||
///
|
||||
/// The returned handle is guaranteed to remain valid because we require a static self.
|
||||
pub fn get_brush(&'static self, descriptor: BrushDescriptor) -> Option<Hbrush> {
|
||||
let mut brushes = self.brushes.borrow_mut();
|
||||
brushes
|
||||
.entry(descriptor)
|
||||
.or_insert_with(|| Brush::from_descriptor(descriptor))
|
||||
.as_ref()
|
||||
// It's okay to do this here because we require self to be 'static
|
||||
.map(|brush| brush.to_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Hash, Debug)]
|
||||
pub struct BrushDescriptor {
|
||||
color: RgbColor,
|
||||
}
|
||||
|
||||
impl BrushDescriptor {
|
||||
pub const fn solid(color: RgbColor) -> Self {
|
||||
Self { color }
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned brush.
|
||||
#[derive(Debug)]
|
||||
pub struct Brush(Hbrush);
|
||||
|
||||
impl Brush {
|
||||
pub fn from_descriptor(desc: BrushDescriptor) -> Option<Self> {
|
||||
let swell_rgb = Swell::RGB(desc.color.r, desc.color.g, desc.color.b);
|
||||
let brush = Swell::get().CreateSolidBrush(swell_rgb as _);
|
||||
Hbrush::new(brush).map(Self)
|
||||
}
|
||||
|
||||
pub fn to_inner(&self) -> Hbrush {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Brush {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
Swell::get().DeleteObject(self.0.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use reaper_common_types::RgbColor;
|
||||
use reaper_low::Swell;
|
||||
|
||||
pub trait SwellRgbColorExt {
|
||||
/// Converts this color to a single integer as expected by Win32/SWELL.
|
||||
fn to_raw(&self) -> u32;
|
||||
}
|
||||
|
||||
impl SwellRgbColorExt for RgbColor {
|
||||
fn to_raw(&self) -> u32 {
|
||||
Swell::RGB(self.r, self.g, self.b)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::SwellRgbColorExt;
|
||||
use reaper_common_types::RgbColor;
|
||||
use reaper_low::{raw, Swell};
|
||||
use reaper_medium::Hdc;
|
||||
|
||||
/// Represents a device context (HDC).
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct DeviceContext(Hdc);
|
||||
|
||||
impl DeviceContext {
|
||||
pub fn new(hdc: Hdc) -> DeviceContext {
|
||||
Self(hdc)
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> raw::HDC {
|
||||
self.0.as_ptr()
|
||||
}
|
||||
|
||||
pub fn set_bk_mode_to_transparent(&self) {
|
||||
unsafe {
|
||||
Swell::get().SetBkMode(self.as_ptr(), raw::TRANSPARENT as _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text_color(&self, color: RgbColor) {
|
||||
unsafe {
|
||||
Swell::get().SetTextColor(self.as_ptr(), color.to_raw() as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use base::hash_util::NonCryptoHashMap;
|
||||
use reaper_low::{raw, Swell};
|
||||
use reaper_medium::Hfont;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FontCache {
|
||||
fonts: RefCell<NonCryptoHashMap<FontDescriptor, Option<Font>>>,
|
||||
}
|
||||
|
||||
impl FontCache {
|
||||
/// Returns a handle to a cached font according to the given descriptor.
|
||||
///
|
||||
/// The returned handle is guaranteed to remain valid because we require a static self.
|
||||
pub fn get_font(&'static self, descriptor: FontDescriptor) -> Option<Hfont> {
|
||||
let mut fonts = self.fonts.borrow_mut();
|
||||
fonts
|
||||
.entry(descriptor)
|
||||
.or_insert_with(|| Font::from_descriptor(descriptor))
|
||||
.as_ref()
|
||||
// It's okay to do this here because we require self to be 'static
|
||||
.map(|font| font.to_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Hash, Debug)]
|
||||
pub struct FontDescriptor {
|
||||
pub name: &'static str,
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
impl FontDescriptor {
|
||||
pub const fn new(name: &'static str, size: u32) -> Self {
|
||||
Self { name, size }
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned font.
|
||||
#[derive(Debug)]
|
||||
pub struct Font(Hfont);
|
||||
|
||||
impl Font {
|
||||
pub fn from_descriptor(desc: FontDescriptor) -> Option<Self> {
|
||||
let mut font = raw::LOGFONT {
|
||||
lfHeight: desc.size as _,
|
||||
..Default::default()
|
||||
};
|
||||
for (i, byte) in desc.name.bytes().take(31).enumerate() {
|
||||
font.lfFaceName[i] = byte as _;
|
||||
}
|
||||
let font = unsafe { Swell::get().CreateFontIndirect(&mut font as *mut _) };
|
||||
Hfont::new(font).map(Self)
|
||||
}
|
||||
|
||||
pub fn to_inner(&self) -> Hfont {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Font {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
Swell::get().DeleteObject(self.0.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
mod view_manager;
|
||||
pub use view_manager::*;
|
||||
|
||||
mod window;
|
||||
pub use window::*;
|
||||
|
||||
mod device_context;
|
||||
pub use device_context::*;
|
||||
|
||||
mod menu;
|
||||
pub use menu::*;
|
||||
|
||||
mod view;
|
||||
pub use view::*;
|
||||
|
||||
mod units;
|
||||
pub use units::*;
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
mod string_types;
|
||||
pub use string_types::*;
|
||||
|
||||
pub mod menu_tree;
|
||||
|
||||
#[macro_use]
|
||||
mod color;
|
||||
pub use color::*;
|
||||
|
||||
mod brush;
|
||||
pub use brush::*;
|
||||
|
||||
mod font;
|
||||
pub use font::*;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
@@ -0,0 +1,118 @@
|
||||
use objc2::foundation::{MainThreadMarker, NSObject};
|
||||
use objc2::rc::{Id, Shared};
|
||||
use objc2::runtime::Class;
|
||||
use objc2::{extern_class, extern_methods, msg_send, msg_send_id, ClassType};
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct NSApplication;
|
||||
|
||||
unsafe impl ClassType for NSApplication {
|
||||
#[inherits(NSObject)]
|
||||
type Super = NSResponder;
|
||||
}
|
||||
);
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct NSResponder;
|
||||
|
||||
unsafe impl ClassType for NSResponder {
|
||||
type Super = NSObject;
|
||||
}
|
||||
);
|
||||
|
||||
pub(crate) fn ns_app() -> Id<NSApplication, Shared> {
|
||||
NSApplication::shared(unsafe { MainThreadMarker::new_unchecked() })
|
||||
}
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct NSEvent;
|
||||
|
||||
unsafe impl ClassType for NSEvent {
|
||||
type Super = NSObject;
|
||||
}
|
||||
);
|
||||
|
||||
extern_methods!(
|
||||
unsafe impl NSApplication {
|
||||
/// This can only be called on the main thread since it may initialize
|
||||
/// the application and since it's parameters may be changed by the main
|
||||
/// thread at any time (hence it is only safe to access on the main thread).
|
||||
pub fn shared(_mtm: MainThreadMarker) -> Id<Self, Shared> {
|
||||
let app: Option<_> = unsafe { msg_send_id![Self::class(), sharedApplication] };
|
||||
// SAFETY: `sharedApplication` always initializes the app if it isn't already
|
||||
unsafe { app.unwrap_unchecked() }
|
||||
}
|
||||
|
||||
pub fn current_event(&self) -> Option<Id<NSEvent, Shared>> {
|
||||
unsafe { msg_send_id![self, currentEvent] }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct NSWindow;
|
||||
|
||||
unsafe impl ClassType for NSWindow {
|
||||
#[inherits(NSObject)]
|
||||
type Super = NSResponder;
|
||||
}
|
||||
);
|
||||
|
||||
extern_methods!(
|
||||
unsafe impl NSWindow {
|
||||
#[sel(sendEvent:)]
|
||||
pub unsafe fn send_event(&self, event: &NSEvent);
|
||||
|
||||
pub fn content_view(&self) -> Option<Id<NSView, Shared>> {
|
||||
unsafe { msg_send_id![self, contentView] }
|
||||
}
|
||||
|
||||
pub fn child_windows(&self) -> Id<NSArrayOfWindows, Shared> {
|
||||
unsafe { msg_send_id![self, childWindows] }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct NSView;
|
||||
|
||||
unsafe impl ClassType for NSView {
|
||||
#[inherits(NSObject)]
|
||||
type Super = NSResponder;
|
||||
}
|
||||
);
|
||||
|
||||
extern_methods!(
|
||||
unsafe impl NSView {
|
||||
pub fn is_kind_of_class(&self, class: &Class) -> bool {
|
||||
unsafe { msg_send![self, isKindOfClass: class] }
|
||||
}
|
||||
|
||||
pub fn window(&self) -> Option<Id<NSWindow, Shared>> {
|
||||
unsafe { msg_send_id![self, window] }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct NSArrayOfWindows;
|
||||
|
||||
unsafe impl ClassType for NSArrayOfWindows {
|
||||
#[inherits(NSObject)]
|
||||
type Super = NSResponder;
|
||||
}
|
||||
);
|
||||
|
||||
extern_methods!(
|
||||
unsafe impl NSArrayOfWindows {
|
||||
pub fn first_object(&self) -> Option<Id<NSWindow, Shared>> {
|
||||
unsafe { msg_send_id![self, firstObject] }
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,164 @@
|
||||
use crate::SwellStringArg;
|
||||
use reaper_low::{raw, Swell};
|
||||
|
||||
/// Represents a top-level menu bar with resource management.
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct MenuBar {
|
||||
raw: raw::HMENU,
|
||||
}
|
||||
|
||||
impl MenuBar {
|
||||
pub fn new_popup_menu() -> MenuBar {
|
||||
Self {
|
||||
raw: Swell::get().CreatePopupMenu(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(resource_id: u32) -> Result<MenuBar, &'static str> {
|
||||
let swell = Swell::get();
|
||||
let raw = unsafe {
|
||||
swell.LoadMenu(
|
||||
swell.plugin_context().h_instance(),
|
||||
resource_id as u16 as raw::ULONG_PTR as raw::LPSTR,
|
||||
)
|
||||
};
|
||||
if raw.is_null() {
|
||||
return Err("couldn't load menu");
|
||||
}
|
||||
Ok(MenuBar { raw })
|
||||
}
|
||||
|
||||
pub fn menu(&self) -> Menu {
|
||||
Menu::new(self.raw)
|
||||
}
|
||||
|
||||
pub fn get_sub_menu(&self, index: u32) -> Option<Menu> {
|
||||
get_sub_menu_at(self.raw, index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MenuBar {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
Swell::get().DestroyMenu(self.raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a menu or submenu.
|
||||
///
|
||||
/// Doesn't need to implement Drop because Windows will destroy all sub menus automatically
|
||||
/// when the root menu is destroyed.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct Menu {
|
||||
raw: raw::HMENU,
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
pub fn new(raw: raw::HMENU) -> Self {
|
||||
Self { raw }
|
||||
}
|
||||
|
||||
pub fn raw(self) -> raw::HMENU {
|
||||
self.raw
|
||||
}
|
||||
|
||||
pub fn set_item_checked(self, item_id: u32, checked: bool) {
|
||||
unsafe {
|
||||
Swell::get().CheckMenuItem(
|
||||
self.raw,
|
||||
item_id as _,
|
||||
if checked {
|
||||
raw::MF_CHECKED
|
||||
} else {
|
||||
raw::MF_UNCHECKED
|
||||
} as _,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item<'b>(self, item_id: u32, text: impl Into<SwellStringArg<'b>>) {
|
||||
unsafe {
|
||||
let swell_string_arg = text.into();
|
||||
let mut mi = raw::MENUITEMINFO {
|
||||
fMask: raw::MIIM_TYPE | raw::MIIM_DATA | raw::MIIM_ID,
|
||||
wID: item_id,
|
||||
dwTypeData: swell_string_arg.as_ptr() as _,
|
||||
..Default::default()
|
||||
};
|
||||
Swell::get().InsertMenuItem(self.raw, -1, 1, &mut mi as _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entry_count(&self) -> Result<u32, &'static str> {
|
||||
let res = unsafe { Swell::get().GetMenuItemCount(self.raw) };
|
||||
if res == -1 {
|
||||
return Err("couldn't menu get entry count");
|
||||
}
|
||||
Ok(res as u32)
|
||||
}
|
||||
|
||||
pub fn add_menu<'b>(self, text: impl Into<SwellStringArg<'b>>) -> Menu {
|
||||
unsafe {
|
||||
let swell_string_arg = text.into();
|
||||
let sub_menu = Swell::get().CreatePopupMenu();
|
||||
let mut mi = raw::MENUITEMINFO {
|
||||
fMask: raw::MIIM_TYPE | raw::MIIM_DATA | raw::MIIM_SUBMENU,
|
||||
hSubMenu: sub_menu,
|
||||
dwTypeData: swell_string_arg.as_ptr() as _,
|
||||
..Default::default()
|
||||
};
|
||||
Swell::get().InsertMenuItem(self.raw, -1, 1, &mut mi as _);
|
||||
Menu::new(sub_menu)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_separator(self) {
|
||||
unsafe {
|
||||
let mut mi = raw::MENUITEMINFO {
|
||||
fMask: raw::MIIM_TYPE,
|
||||
fType: raw::MF_SEPARATOR,
|
||||
..Default::default()
|
||||
};
|
||||
Swell::get().InsertMenuItem(self.raw, -1, 1, &mut mi as _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_sub_menu_at(&self, index: u32) -> Option<Menu> {
|
||||
get_sub_menu_at(self.raw, index)
|
||||
}
|
||||
|
||||
pub fn set_item_text<'b>(self, item_id: u32, text: impl Into<SwellStringArg<'b>>) {
|
||||
unsafe {
|
||||
let swell_string_arg = text.into();
|
||||
let mut mi = raw::MENUITEMINFO {
|
||||
fMask: raw::MIIM_TYPE | raw::MIIM_DATA,
|
||||
dwTypeData: swell_string_arg.as_ptr() as _,
|
||||
..Default::default()
|
||||
};
|
||||
Swell::get().SetMenuItemInfo(self.raw, item_id as _, 0, &mut mi as _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_item_enabled(self, item_id: u32, enabled: bool) {
|
||||
unsafe {
|
||||
Swell::get().EnableMenuItem(
|
||||
self.raw,
|
||||
item_id as _,
|
||||
if enabled {
|
||||
raw::MF_ENABLED
|
||||
} else {
|
||||
raw::MF_GRAYED
|
||||
} as _,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sub_menu_at(raw: raw::HMENU, index: u32) -> Option<Menu> {
|
||||
let menu = unsafe { Swell::get().GetSubMenu(raw, index as _) };
|
||||
if menu.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(Menu::new(menu))
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Entry<R> {
|
||||
Menu(Menu<R>),
|
||||
Item(Item<R>),
|
||||
Separator(Separator),
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl<R> Entry<R> {
|
||||
fn index_recursive(&mut self, counter: &mut Counter) {
|
||||
match self {
|
||||
Entry::Menu(m) => {
|
||||
m.id = counter.next_value();
|
||||
for e in &mut m.entries {
|
||||
e.index_recursive(counter);
|
||||
}
|
||||
}
|
||||
Entry::Item(i) => {
|
||||
i.id = counter.next_value();
|
||||
}
|
||||
Entry::Separator(i) => {
|
||||
i.id = counter.next_value();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_item_by_id_recursive(self, id: u32) -> Option<Item<R>> {
|
||||
match self {
|
||||
Entry::Menu(m) => m
|
||||
.entries
|
||||
.into_iter()
|
||||
.find_map(|e| e.find_item_by_id_recursive(id)),
|
||||
Entry::Item(i) => {
|
||||
if i.id == id {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Menu<R> {
|
||||
pub id: u32,
|
||||
pub text: String,
|
||||
pub entries: Vec<Entry<R>>,
|
||||
}
|
||||
|
||||
impl<R> Menu<R> {
|
||||
/// Assigns all menu entries consecutive IDs starting from the given first ID.
|
||||
///
|
||||
/// Returns next non-used value.
|
||||
///
|
||||
/// This is useful for popup menus.
|
||||
pub fn index(&mut self, first_id: u32) -> u32 {
|
||||
let mut counter = Counter::starting_from(first_id);
|
||||
for e in &mut self.entries {
|
||||
e.index_recursive(&mut counter);
|
||||
}
|
||||
counter.next_value()
|
||||
}
|
||||
|
||||
/// Returns the item that has the given ID.
|
||||
///
|
||||
/// Also looks into sub menus.
|
||||
///
|
||||
/// This is useful for popup menus.
|
||||
pub fn find_item_by_id(self, id: u32) -> Option<Item<R>> {
|
||||
self.entries
|
||||
.into_iter()
|
||||
.find_map(|e| e.find_item_by_id_recursive(id))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Item<R> {
|
||||
pub id: u32,
|
||||
pub text: String,
|
||||
pub result: R,
|
||||
pub opts: ItemOpts,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Separator {
|
||||
pub id: u32,
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
/// Unlabeled menu.
|
||||
///
|
||||
/// This is useful for aggregating a set of entries that can then be added in one go.
|
||||
pub fn anonymous_menu<R>(entries: Vec<Entry<R>>) -> Menu<R> {
|
||||
Menu {
|
||||
id: 0,
|
||||
text: "".to_owned(),
|
||||
entries,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu<R>(text: impl Into<String>, entries: Vec<Entry<R>>) -> Entry<R> {
|
||||
Entry::Menu(Menu {
|
||||
id: 0,
|
||||
text: text.into(),
|
||||
entries,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn item<R>(text: impl Into<String>, result: R) -> Entry<R> {
|
||||
Entry::Item(Item {
|
||||
id: 0,
|
||||
text: text.into(),
|
||||
result,
|
||||
opts: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn separator<R>() -> Entry<R> {
|
||||
Entry::Separator(Separator { id: 0, text: None })
|
||||
}
|
||||
|
||||
pub fn labeled_separator<R>(name: impl Into<String>) -> Entry<R> {
|
||||
Entry::Separator(Separator {
|
||||
id: 0,
|
||||
text: Some(name.into()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn item_with_opts<R>(text: impl Into<String>, opts: ItemOpts, result: R) -> Entry<R> {
|
||||
Entry::Item(Item {
|
||||
id: 0,
|
||||
text: text.into(),
|
||||
result,
|
||||
opts,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_closure)]
|
||||
pub fn disabled_item<R: Default>(text: impl Into<String>) -> Entry<R> {
|
||||
item_with_opts(
|
||||
text,
|
||||
ItemOpts {
|
||||
enabled: false,
|
||||
checked: false,
|
||||
},
|
||||
R::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ItemOpts {
|
||||
pub enabled: bool,
|
||||
pub checked: bool,
|
||||
}
|
||||
|
||||
impl Default for ItemOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
checked: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Counter {
|
||||
value: u32,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn starting_from(value: u32) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
|
||||
pub fn next_value(&mut self) -> u32 {
|
||||
let val = self.value;
|
||||
self.value += 1;
|
||||
val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod input;
|
||||
pub use input::*;
|
||||
|
||||
mod output;
|
||||
pub use output::*;
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::menu_tree::{Entry, Menu};
|
||||
use crate::Menu as SwellMenu;
|
||||
|
||||
/// Adds all entries within the given pure menu to the given SWELL menu, ignoring the label of the pure menu.
|
||||
///
|
||||
/// Also adds a separator first if the SWELL menu already contains entries.
|
||||
pub fn add_all_entries_of_menu<R>(swell_menu: SwellMenu, root_menu: &Menu<R>) {
|
||||
// Add separator if there are entries already
|
||||
if swell_menu.entry_count().is_ok_and(|count| count > 0) {
|
||||
swell_menu.add_separator();
|
||||
}
|
||||
// Add entries
|
||||
for e in &root_menu.entries {
|
||||
fill_menu_recursively(swell_menu, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the given menu entry and potential sub entries to the given SWELL menu.
|
||||
fn fill_menu_recursively<R>(swell_menu: SwellMenu, entry: &Entry<R>) {
|
||||
match entry {
|
||||
Entry::Menu(m) => {
|
||||
let sub_menu = swell_menu.add_menu(m.text.as_str());
|
||||
for e in &m.entries {
|
||||
fill_menu_recursively(sub_menu, e);
|
||||
}
|
||||
}
|
||||
Entry::Item(i) => {
|
||||
swell_menu.add_item(i.id, i.text.as_str());
|
||||
if i.opts.checked {
|
||||
swell_menu.set_item_checked(i.id, true);
|
||||
}
|
||||
if !i.opts.enabled {
|
||||
swell_menu.set_item_enabled(i.id, false);
|
||||
}
|
||||
}
|
||||
Entry::Separator(s) => {
|
||||
swell_menu.add_separator();
|
||||
if let Some(text) = &s.text {
|
||||
swell_menu.add_item(s.id, text.as_str());
|
||||
swell_menu.set_item_enabled(s.id, false);
|
||||
swell_menu.add_separator();
|
||||
}
|
||||
}
|
||||
Entry::Nothing => {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use reaper_low::raw;
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
pub struct SwellStringArg<'a>(Cow<'a, CStr>);
|
||||
|
||||
impl SwellStringArg<'_> {
|
||||
pub fn as_ptr(&self) -> *const c_char {
|
||||
self.0.as_ptr()
|
||||
}
|
||||
|
||||
pub(super) fn as_lparam(&self) -> raw::LPARAM {
|
||||
self.0.as_ptr() as _
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a CStr> for SwellStringArg<'a> {
|
||||
fn from(s: &'a CStr) -> Self {
|
||||
SwellStringArg(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for SwellStringArg<'a> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
// Requires copying
|
||||
SwellStringArg(
|
||||
CString::new(s)
|
||||
.expect("Rust string too exotic for REAPER")
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SwellStringArg<'_> {
|
||||
fn from(s: String) -> Self {
|
||||
// Doesn't require copying because we own the string now
|
||||
SwellStringArg(
|
||||
CString::new(s)
|
||||
.expect("Rust string too exotic for REAPER")
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
pub type SharedView<V> = Rc<V>;
|
||||
pub type WeakView<V> = std::rc::Weak<V>;
|
||||
@@ -0,0 +1,297 @@
|
||||
use reaper_low::raw;
|
||||
use reaper_low::raw::RECT;
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
|
||||
/// An abstract unit used for dialog dimensions, independent of HiDPI and stuff.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub struct DialogUnits(pub u32);
|
||||
|
||||
impl DialogUnits {
|
||||
pub fn get(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn as_raw(self) -> i32 {
|
||||
self.0 as _
|
||||
}
|
||||
|
||||
pub fn scale(&self, scale: f64) -> Self {
|
||||
DialogUnits((scale * self.0 as f64).round() as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for DialogUnits {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<u32> for DialogUnits {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: u32) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixels on a screen.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct Pixels(pub u32);
|
||||
|
||||
impl Pixels {
|
||||
pub fn get(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn as_raw(self) -> i32 {
|
||||
self.0 as _
|
||||
}
|
||||
|
||||
pub fn scale(&self, scale: f64) -> Self {
|
||||
Pixels((scale * self.0 as f64).round() as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f64> for Pixels {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: f64) -> Self::Output {
|
||||
Self((self.0 as f64 * rhs).round() as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Pixels(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Pixels(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Point in a coordinate system.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub struct Point<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
impl<T> Point<T> {
|
||||
pub const fn new(x: T, y: T) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
/// These factors should correspond to those in `dialogs.cpp`.
|
||||
fn effective_scale_factors() -> ScaleFactors {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let scaling_256 = reaper_low::Swell::get().SWELL_GetScaling256();
|
||||
let hidpi_factor = scaling_256 as f64 / 256.0;
|
||||
ScaleFactors {
|
||||
main: 1.9 * hidpi_factor,
|
||||
y: 0.92,
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
ScaleFactors { main: 1.6, y: 0.95 }
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
ScaleFactors { main: 1.0, y: 1.0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct ScaleFactors {
|
||||
/// The main scale factor which affects both x and y coordinates.
|
||||
///
|
||||
/// Corresponds to `SWELL_DLG_SCALE_AUTOGEN` in `dialogs.cpp`.
|
||||
main: f64,
|
||||
/// An additional scale factor which is applied to y coordinates.
|
||||
///
|
||||
/// Set to 1.0 if you want to use the main factor only.
|
||||
///
|
||||
/// Corresponds to `SWELL_DLG_SCALE_AUTOGEN_YADJ` in `dialogs.cpp`.
|
||||
y: f64,
|
||||
}
|
||||
|
||||
impl ScaleFactors {
|
||||
pub fn x_factor(&self) -> f64 {
|
||||
self.main
|
||||
}
|
||||
|
||||
pub fn y_factor(&self) -> f64 {
|
||||
self.main * self.y
|
||||
}
|
||||
}
|
||||
|
||||
impl Point<DialogUnits> {
|
||||
/// Converts this dialog unit point to pixels.
|
||||
///
|
||||
/// The Window struct contains a method which can do this including Windows HiDPI information.
|
||||
pub fn in_pixels(&self) -> Point<Pixels> {
|
||||
// TODO-low On Windows this works differently. See original ReaLearn. But on the other hand
|
||||
// ... this is only for the first short render before the optimal size is calculated.
|
||||
// So as long as it works, this heuristic is okay.
|
||||
let scale_factors = effective_scale_factors();
|
||||
Point {
|
||||
x: Pixels((scale_factors.x_factor() * self.x.get() as f64) as _),
|
||||
y: Pixels((scale_factors.y_factor() * self.y.get() as f64) as _),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(self, scaling: &DialogScaling) -> Self {
|
||||
Self {
|
||||
x: self.x.scale(scaling.x_scale),
|
||||
y: self.y.scale(scaling.y_scale),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Point<T> {
|
||||
pub fn to_dimensions(self) -> Dimensions<T> {
|
||||
Dimensions::new(self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<Dimensions<T>> for Point<T> {
|
||||
fn from(d: Dimensions<T>) -> Self {
|
||||
d.to_point()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dimensions of a rectangle.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct Dimensions<T> {
|
||||
pub width: T,
|
||||
pub height: T,
|
||||
}
|
||||
|
||||
impl<T> Dimensions<T> {
|
||||
pub const fn new(width: T, height: T) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Dimensions<T> {
|
||||
pub fn to_point(self) -> Point<T> {
|
||||
Point::new(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl Dimensions<Pixels> {
|
||||
pub fn to_vst(self) -> (i32, i32) {
|
||||
(self.width.get() as _, self.height.get() as _)
|
||||
}
|
||||
|
||||
pub fn scale(self, scaling: DialogScaling) -> Self {
|
||||
Self {
|
||||
width: self.width.scale(scaling.width_scale),
|
||||
height: self.height.scale(scaling.height_scale),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dimensions<DialogUnits> {
|
||||
/// Converts the given dialog unit dimensions to pixels.
|
||||
///
|
||||
/// Doesn't take window-specific HIDPI info into account! Use `Window` for this.
|
||||
pub fn in_pixels(&self) -> Dimensions<Pixels> {
|
||||
self.to_point().in_pixels().to_dimensions()
|
||||
}
|
||||
|
||||
pub fn scale(self, scaling: &DialogScaling) -> Self {
|
||||
Self {
|
||||
width: self.width.scale(scaling.width_scale),
|
||||
height: self.height.scale(scaling.height_scale),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> From<Point<T>> for Dimensions<T> {
|
||||
fn from(p: Point<T>) -> Self {
|
||||
p.to_dimensions()
|
||||
}
|
||||
}
|
||||
|
||||
/// This is not the scaling applied by SWELL but the one applied before by us when generating
|
||||
/// the RC file. In future we might produce different RC files for different operating systems.
|
||||
/// Then this is maybe the only scaling info we need and we can ditch SWELL scaling.
|
||||
#[derive(Debug)]
|
||||
pub struct DialogScaling {
|
||||
pub x_scale: f64,
|
||||
pub y_scale: f64,
|
||||
pub width_scale: f64,
|
||||
pub height_scale: f64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Rect {
|
||||
pub left: i32,
|
||||
pub top: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn contains(&self, point: Point<i32>) -> bool {
|
||||
point.x >= self.left
|
||||
&& point.y >= self.top
|
||||
&& point.x < self.right()
|
||||
&& point.y < self.bottom()
|
||||
}
|
||||
|
||||
pub fn right(&self) -> i32 {
|
||||
self.left + self.width as i32
|
||||
}
|
||||
|
||||
pub fn bottom(&self) -> i32 {
|
||||
self.top + self.height as i32
|
||||
}
|
||||
|
||||
// pub fn normalize(&self, parent_height: u32) -> Self {
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// Self {
|
||||
// top: parent_height as i32 - self.top,
|
||||
// ..*self
|
||||
// }
|
||||
// }
|
||||
// #[cfg(not(target_os = "macos"))]
|
||||
// {
|
||||
// *self
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
impl From<raw::RECT> for Rect {
|
||||
fn from(value: RECT) -> Self {
|
||||
Self {
|
||||
left: value.left,
|
||||
top: value.top,
|
||||
width: (value.right - value.left).unsigned_abs(),
|
||||
height: (value.bottom - value.top).unsigned_abs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for raw::RECT {
|
||||
fn from(value: Rect) -> Self {
|
||||
Self {
|
||||
left: value.left,
|
||||
top: value.top,
|
||||
right: value.right(),
|
||||
bottom: value.bottom(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
use crate::{create_window, DeviceContext, Pixels, Point, SharedView, Window};
|
||||
use reaper_low::raw;
|
||||
use rxrust::prelude::*;
|
||||
|
||||
use reaper_medium::Hbrush;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Represents a displayable logical part of the UI, such as a panel.
|
||||
///
|
||||
/// Each view has a 1:0..1 relationship to a window. One can say that a view is
|
||||
/// implemented/displayed by a window. A window (= HWND) is the more low-level technical
|
||||
/// implementation concept which has lots of possible behavior (see `Window` struct) whereas a view
|
||||
/// is a higher-level logical concept which uses window methods to implement very particular and
|
||||
/// aptly-named logic which makes sense for that part of the UI.
|
||||
///
|
||||
/// All views have a few things in common, e.g. views can be opened (window gets created) and closed
|
||||
/// (window gets destroyed). These common things are modeled by this trait. In addition to a common
|
||||
/// interface, this trait provides default implementations for that common behavior.
|
||||
///
|
||||
/// An other important part of this trait are window events/callbacks, which implementors can
|
||||
/// handle.
|
||||
///
|
||||
/// # Design
|
||||
///
|
||||
/// ## Why do view callback methods take self not as mutable reference?
|
||||
/// win32 window procedures can be *reentered*, see the win32 docs! Now let's assume we would take
|
||||
/// self as mutable reference (`&mut self`). If we would have a borrow checker (`RefCell`), it would
|
||||
/// complain on reentry by panicking. Rightly so. Without `RefCell` things would get very unsafe and
|
||||
/// we wouldn't even get notified about it. I think the only correct way is to never let the window
|
||||
/// procedure call view methods in a mutable context. Make all view handler methods take an
|
||||
/// immutable reference. The same strategy which we are using with `IReaperControlSurface` in
|
||||
/// `reaper-rs`, because this is reentrant as well.
|
||||
///
|
||||
/// ## Why are there no exceptions?
|
||||
/// One could argue that e.g. `WM_INITDIALOG` is not reentered and we could therefore make an
|
||||
/// exception. But not only the win32 window procedure might call our view, also our own code. Just
|
||||
/// think of a `close()` method which takes `&mut self` and calls `DestroyWindow()`. Windows would
|
||||
/// send a `WM_INITDIALOG` message while we are still in the `close()` method, et voilà ... we would
|
||||
/// have 2 mutable accesses. It's just not safe and would cause a false feeling of security!
|
||||
///
|
||||
/// ## So how do we mutate things in the callback methods?
|
||||
/// Everything which needs to be mutable needs to be wrapped with a `RefCell`. We need to pursue the
|
||||
/// fine-granular `RefCell` approach because reentrancy is unavoidable. We just need to make sure
|
||||
/// not to write to the same data member non-exclusively. If we fail to achieve that, at least
|
||||
/// the panic lets us know about the issue.
|
||||
///
|
||||
/// ## Why do view callback methods take self as `SharedView<Self>`?
|
||||
/// Given the above mentioned safety measures and knowing that we must keep views as `Rc`s anyway
|
||||
/// (for lifetime reasons, see `ViewManager`), it is possible to take self as `SharedView<Self>`
|
||||
/// without sacrificing anything. The obvious advantage we have is that it gives us an easy way to
|
||||
/// access view methods in subscribe closures without running into lifetime problems (such as &self
|
||||
/// disappearing while still being used in the closure).
|
||||
pub trait View: Debug {
|
||||
// Data providers (implementation required, used internally)
|
||||
// =========================================================
|
||||
|
||||
/// ID of the dialog resource to look up when creating the window.
|
||||
///
|
||||
/// The dialog resource basically defines the window's initial look.
|
||||
fn dialog_resource_id(&self) -> u32;
|
||||
|
||||
/// Returns the current window, if any.
|
||||
///
|
||||
/// In order to implement behavior common to views, the `View` trait needs mutable access to
|
||||
/// this context.
|
||||
fn view_context(&self) -> &ViewContext;
|
||||
|
||||
// Event handlers (implementation optional)
|
||||
// =================================================
|
||||
|
||||
/// WM_INITDIALOG.
|
||||
///
|
||||
/// Should return `true` if you want the window to be actually shown when it's created.
|
||||
fn show_window_on_init(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// WM_INITDIALOG.
|
||||
///
|
||||
/// Should return `true` if keyboard focus is desired.
|
||||
fn opened(self: SharedView<Self>, _window: Window) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_CLOSE.
|
||||
///
|
||||
/// Should return `true` if the window must not be destroyed.
|
||||
fn close_requested(self: SharedView<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_DESTROY.
|
||||
fn on_destroy(self: SharedView<Self>, _window: Window) {}
|
||||
|
||||
/// WM_SHOWWINDOW.
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn shown_or_hidden(self: SharedView<Self>, shown: bool) -> bool {
|
||||
let _ = shown;
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_COMMAND, HIWORD(wparam) == 0.
|
||||
fn button_clicked(self: SharedView<Self>, resource_id: u32) {
|
||||
let _ = resource_id;
|
||||
}
|
||||
|
||||
/// WM_COMMAND, HIWORD(wparam) == CBN_SELCHANGE
|
||||
fn option_selected(self: SharedView<Self>, resource_id: u32) {
|
||||
let _ = resource_id;
|
||||
}
|
||||
|
||||
/// WM_VSCROLL, LOWORD(wparam).
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn scrolled_vertically(self: SharedView<Self>, _code: u32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_HSCROLL, lparam (!= 0).
|
||||
fn slider_moved(self: SharedView<Self>, _slider: Window) {}
|
||||
|
||||
/// Should return `true` if processed.
|
||||
fn resized(self: SharedView<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Should return `true` if processed.
|
||||
fn focused(self: SharedView<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_MOUSEWHEEL, HIWORD(wparam).
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn mouse_wheel_turned(self: SharedView<Self>, distance: i32) -> bool {
|
||||
let _ = distance;
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_MOUSEMOVE.
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn mouse_moved(self: SharedView<Self>, position: Point<i32>) -> bool {
|
||||
let _ = position;
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_NCHITTEST.
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn mouse_test(self: SharedView<Self>, position: Point<i32>) -> bool {
|
||||
let _ = position;
|
||||
false
|
||||
}
|
||||
|
||||
/// When F1 was pressed.
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn help_requested(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_KEYDOWN.
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn key_down(self: SharedView<Self>, key_code: u8) -> bool {
|
||||
let _ = key_code;
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_KEYUP.
|
||||
///
|
||||
/// On macOS, a multi-line text field fires this instead of edit_control_changed.
|
||||
/// But it's not fired on Windows!
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn key_up(self: SharedView<Self>, key_code: u8) -> bool {
|
||||
let _ = key_code;
|
||||
false
|
||||
}
|
||||
|
||||
/// EN_CHANGE, LOWORD(wparam).
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn edit_control_changed(self: SharedView<Self>, resource_id: u32) -> bool {
|
||||
let _ = resource_id;
|
||||
false
|
||||
}
|
||||
|
||||
/// EN_SETFOCUS, LOWORD(wparam).
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn edit_control_focus_set(self: SharedView<Self>, resource_id: u32) -> bool {
|
||||
let _ = resource_id;
|
||||
false
|
||||
}
|
||||
|
||||
/// EN_KILLFOCUS, LOWORD(wparam).
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
///
|
||||
/// Currently not fired on Linux!
|
||||
fn edit_control_focus_killed(self: SharedView<Self>, _resource_id: u32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_CONTEXTMENU
|
||||
///
|
||||
/// Should return `true` if processed in order to prevent the context menu request going up to higher layers.
|
||||
fn context_menu_wanted(self: SharedView<Self>, _location: Point<Pixels>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_PAINT
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn paint(self: SharedView<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_ERASEBKGND
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn erase_background(self: SharedView<Self>, device_context: DeviceContext) -> bool {
|
||||
let _ = device_context;
|
||||
false
|
||||
}
|
||||
|
||||
/// WM_CTLCOLORSTATIC
|
||||
///
|
||||
/// Can return a custom background brush for painting that control.
|
||||
fn control_color_static(
|
||||
self: SharedView<Self>,
|
||||
device_context: DeviceContext,
|
||||
window: Window,
|
||||
) -> Option<Hbrush> {
|
||||
let _ = device_context;
|
||||
let _ = window;
|
||||
None
|
||||
}
|
||||
|
||||
/// WM_CTLCOLORDLG
|
||||
///
|
||||
/// Can return a custom background brush for painting that dialog.
|
||||
fn control_color_dialog(
|
||||
self: SharedView<Self>,
|
||||
device_context: DeviceContext,
|
||||
window: Window,
|
||||
) -> Option<Hbrush> {
|
||||
let _ = device_context;
|
||||
let _ = window;
|
||||
None
|
||||
}
|
||||
|
||||
/// Timer with the given ID fires.
|
||||
///
|
||||
/// Should return `true` if processed.
|
||||
fn timer(&self, id: usize) -> bool {
|
||||
let _ = id;
|
||||
false
|
||||
}
|
||||
|
||||
/// Called whenever the DialogProc (not WindowProc!!!) is called, before any other callback
|
||||
/// method.
|
||||
///
|
||||
/// Return `None` to indicate that processing should continue, that is, the other callback
|
||||
/// methods should be called accordingly.
|
||||
fn process_raw(
|
||||
&self,
|
||||
window: Window,
|
||||
msg: raw::UINT,
|
||||
wparam: raw::WPARAM,
|
||||
lparam: raw::LPARAM,
|
||||
) -> Option<raw::INT_PTR> {
|
||||
let _ = window;
|
||||
let _ = msg;
|
||||
let _ = wparam;
|
||||
let _ = lparam;
|
||||
None
|
||||
}
|
||||
|
||||
fn get_keyboard_event_receiver(&self, focused_window: Window) -> Option<Window> {
|
||||
Some(focused_window)
|
||||
}
|
||||
|
||||
/// If `true`, `RealearnAccelerator` will forward raw keyboard events to this window.
|
||||
///
|
||||
/// - Absolutely necessary for egui containers (so that egui receives keyboard events)
|
||||
/// - For normal dialogs this can be bad (at least on Windows) because tabbing through text fields is not possible
|
||||
/// anymore (https://github.com/helgoboss/helgobox/issues/1213)
|
||||
fn wants_raw_keyboard_input(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// Public methods (intended to be used by consumers)
|
||||
// =================================================
|
||||
|
||||
/// Opens this view in the given parent window.
|
||||
fn open(self: SharedView<Self>, parent_window: Window) -> Option<Window>
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
let resource_id = self.dialog_resource_id();
|
||||
create_window(self, resource_id, Some(parent_window))
|
||||
}
|
||||
|
||||
/// Opens this view in a free window.
|
||||
fn open_without_parent(self: SharedView<Self>) -> Option<Window>
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
let resource_id = self.dialog_resource_id();
|
||||
create_window(self, resource_id, None)
|
||||
}
|
||||
|
||||
/// Closes this view.
|
||||
fn close(&self) {
|
||||
if let Some(window) = self.view_context().window.get() {
|
||||
window.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this view is currently open.
|
||||
fn is_open(&self) -> bool {
|
||||
self.view_context().window.get().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Context data of a view.
|
||||
///
|
||||
/// If Rust traits could provide data in the form of fields, this would be it.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ViewContext {
|
||||
pub(crate) window: Cell<Option<Window>>,
|
||||
pub(crate) closed_subject: RefCell<LocalSubject<'static, (), ()>>,
|
||||
}
|
||||
|
||||
impl ViewContext {
|
||||
/// Returns the current window associated with this view if this view is open.
|
||||
pub fn window(&self) -> Option<Window> {
|
||||
self.window.get()
|
||||
}
|
||||
|
||||
/// Returns the current window associated with this view.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the window doesn't exist (the view is not open).
|
||||
pub fn require_window(&self) -> Window {
|
||||
self.window().expect("window not found but required")
|
||||
}
|
||||
|
||||
/// Returns the control with the given resource ID.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the window or control doesn't exist.
|
||||
pub fn require_control(&self, resource_id: u32) -> Window {
|
||||
self.require_window().require_control(resource_id)
|
||||
}
|
||||
|
||||
/// Fires when the window is closed.
|
||||
pub fn closed(&self) -> impl LocalObservable<'static, Item = (), Err = ()> + 'static {
|
||||
self.closed_subject.borrow().clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
//! This file is supposed to encapsulate most of the (ugly) win32 API glue code
|
||||
use crate::{
|
||||
BrushCache, BrushDescriptor, DeviceContext, FontCache, FontDescriptor, Pixels, Point,
|
||||
SharedView, View, WeakView, Window,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use reaper_low::{raw, Swell};
|
||||
use rxrust::prelude::*;
|
||||
use std::os::raw::c_void;
|
||||
use std::panic::catch_unwind;
|
||||
use std::ptr::null_mut;
|
||||
|
||||
use base::hash_util::NonCryptoHashMap;
|
||||
use fragile::Fragile;
|
||||
use reaper_common_types::RgbColor;
|
||||
use reaper_medium::{Hbrush, Hdc, Hfont, Hwnd};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Creates a window according to the given dialog resource.
|
||||
///
|
||||
/// It's added as a child to the given parent window and attached to the specified view.
|
||||
///
|
||||
/// Internally, this creates a new win32 dialog using the given resource ID. Uses the methods in the
|
||||
/// given view for all callbacks.
|
||||
pub(crate) fn create_window(
|
||||
view: SharedView<dyn View>,
|
||||
resource_id: u32,
|
||||
parent_window: Option<Window>,
|
||||
) -> Option<Window> {
|
||||
let swell = Swell::get();
|
||||
let hwnd = unsafe {
|
||||
// This will call the dialog procedure `view_dialog_proc`. In order to still know which
|
||||
// of the many view objects we are dealing with, we make use of the lparam parameter of
|
||||
// `CreateDialogParamA` by passing it an address which points to the concrete view.
|
||||
// `view_dialog_proc` with message WM_INITDIALOG will be called immediately, not async.
|
||||
// That's important because we must be sure that the given view Rc reference is still
|
||||
// valid when it arrives in `view_dialog_proc`.
|
||||
swell.CreateDialogParam(
|
||||
swell.plugin_context().h_instance(),
|
||||
resource_id as u16 as raw::ULONG_PTR as raw::LPSTR,
|
||||
parent_window.map(|w| w.raw()).unwrap_or(null_mut()),
|
||||
Some(view_dialog_proc),
|
||||
convert_view_ref_to_address(&view),
|
||||
)
|
||||
};
|
||||
Window::new(hwnd)
|
||||
}
|
||||
|
||||
/// This struct manages the mapping from windows to views.
|
||||
///
|
||||
/// This is necessary to get from "global" win32 world into beloved "local" Rust struct world.
|
||||
#[derive(Default)]
|
||||
pub struct ViewManager {
|
||||
/// Holds a mapping from window handles (HWND) to views
|
||||
view_map: RefCell<NonCryptoHashMap<raw::HWND, WeakView<dyn View>>>,
|
||||
brush_cache: BrushCache,
|
||||
font_cache: FontCache,
|
||||
}
|
||||
|
||||
impl ViewManager {
|
||||
pub fn get_solid_brush(&'static self, color: RgbColor) -> Option<Hbrush> {
|
||||
self.brush_cache.get_brush(BrushDescriptor::solid(color))
|
||||
}
|
||||
|
||||
pub fn get_font(&'static self, descriptor: FontDescriptor) -> Option<Hfont> {
|
||||
self.font_cache.get_font(descriptor)
|
||||
}
|
||||
|
||||
/// If the given window is one of ours (one that drives our views) and the associated view
|
||||
/// still exists, it returns that associated view.
|
||||
pub fn get_associated_view(&self, window: Window) -> Option<SharedView<dyn View>> {
|
||||
let view_map = self.view_map.borrow();
|
||||
let view = view_map.get(&window.raw())?;
|
||||
view.upgrade()
|
||||
}
|
||||
|
||||
/// Returns the global window manager instance
|
||||
pub fn get() -> &'static ViewManager {
|
||||
static VIEW_MANAGER: OnceLock<Fragile<ViewManager>> = OnceLock::new();
|
||||
// We need to initialize the manager lazily because it's impossible to do that using a const
|
||||
// function (at least in Rust stable).
|
||||
VIEW_MANAGER
|
||||
.get_or_init(|| Fragile::new(ViewManager::default()))
|
||||
.get()
|
||||
}
|
||||
|
||||
/// Registers a new HWND-to-view mapping
|
||||
fn register_view(&self, hwnd: raw::HWND, view: &SharedView<dyn View>) {
|
||||
self.view_map
|
||||
.borrow_mut()
|
||||
.insert(hwnd, SharedView::downgrade(view));
|
||||
}
|
||||
|
||||
/// Looks up a view by its corresponding HWND
|
||||
fn lookup_view(&self, hwnd: raw::HWND) -> Option<SharedView<dyn View>> {
|
||||
let view_map = self.view_map.borrow();
|
||||
let weak_view = view_map.get(&hwnd)?;
|
||||
weak_view.upgrade().or_else(|| {
|
||||
// Not existing. The primary owner (most likely a parent view) dropped
|
||||
// it already.
|
||||
tracing::warn!("Requested ui is registered in ui map but has been dropped already");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Unregisters a HWND-to-View mapping
|
||||
fn unregister_view(&self, hwnd: raw::HWND) {
|
||||
self.view_map.borrow_mut().remove(&hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Converts the given view Rc reference to an address which can be transmitted as LPARAM.
|
||||
// `SharedView<dyn View>` is a so-called trait object, a *fat* pointer which is twice as large as a
|
||||
// normal pointer (on 64-bit architectures 2 x 64 bit = 128 bit = 16 bytes). This is too big to
|
||||
// encode within LPARAM. `&SharedView<dyn View>` is *not* a trait object but a reference to the
|
||||
// trait object. Therefore it is a thin pointer already.
|
||||
fn convert_view_ref_to_address(view_trait_object_ref: &SharedView<dyn View>) -> isize {
|
||||
let view_trait_object_ptr = view_trait_object_ref as *const _ as *const c_void;
|
||||
view_trait_object_ptr as isize
|
||||
}
|
||||
|
||||
// Converts the given address back to the original view Rc reference.
|
||||
fn interpret_address_as_view_ref<'a>(view_trait_object_address: isize) -> &'a SharedView<dyn View> {
|
||||
let view_trait_object_ptr = view_trait_object_address as *const c_void;
|
||||
unsafe { &*(view_trait_object_ptr as *const _) }
|
||||
}
|
||||
|
||||
/// This is our dialog procedure.
|
||||
///
|
||||
/// It's called by Windows (or the emulation layer). It basically finds the particular `View`
|
||||
/// instance which matches the HWND and then delegates to its methods. Please note that this is
|
||||
/// a DialogProc, not a WindowProc. The difference is mainly the return value. A WindowProc
|
||||
/// usually returns 0 if the message has been processed, or it delegates to `DefWindowProc()`.
|
||||
/// If we do the latter in a DialogProc, non-child windows start to become always modal (not
|
||||
/// returning focus) because it's wrong!
|
||||
///
|
||||
/// In DialogProc it's the opposite: It returns 1 if the message has been processed and 0 if not.
|
||||
/// If we have a message where the return value has a special meaning (beyond processed or
|
||||
/// unprocesssed), we need to "return" that via `SetWindowLong` instead, except for WM_INITDIALOG.
|
||||
/// See https://docs.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-dlgproc.
|
||||
unsafe extern "C" fn view_dialog_proc(
|
||||
hwnd: raw::HWND,
|
||||
msg: raw::UINT,
|
||||
wparam: raw::WPARAM,
|
||||
lparam: raw::LPARAM,
|
||||
) -> raw::INT_PTR {
|
||||
catch_unwind(|| {
|
||||
DIALOG_PROC_ALREADY_ENTERED.with(|entered| {
|
||||
// Detect reentrancy
|
||||
let already_entered = entered.replace(true);
|
||||
scopeguard::defer! {
|
||||
if !already_entered {
|
||||
entered.set(false);
|
||||
}
|
||||
}
|
||||
// Obtain view
|
||||
let view: SharedView<dyn View> = if msg == raw::WM_INITDIALOG {
|
||||
// A view window is initializing. At this point lparam contains the value which we
|
||||
// passed when calling CreateDialogParam. This contains the address of a
|
||||
// view reference. At subsequent calls, this address is not passed anymore
|
||||
// but only the HWND. So we need to save a HWND-to-view mapping now.
|
||||
let view_ref = interpret_address_as_view_ref(lparam as _);
|
||||
ViewManager::get().register_view(hwnd, view_ref);
|
||||
view_ref.clone()
|
||||
} else {
|
||||
// Try to find view corresponding to given HWND
|
||||
match ViewManager::get().lookup_view(hwnd) {
|
||||
None => {
|
||||
// View is not (yet) registered. Do default stuff.
|
||||
return 0;
|
||||
}
|
||||
Some(v) => v,
|
||||
}
|
||||
};
|
||||
// Found view.
|
||||
// Delegate to view struct methods.
|
||||
let window = Window::new(hwnd).expect("window was null");
|
||||
if let Some(result) = view.process_raw(window, msg, wparam, lparam) {
|
||||
return result;
|
||||
}
|
||||
const KEYBOARD_MSG_FOR_X_BRIDGE: u32 = raw::WM_USER + 100;
|
||||
match msg {
|
||||
raw::WM_INITDIALOG => {
|
||||
view.view_context().window.replace(Some(window));
|
||||
let show_window = view.show_window_on_init();
|
||||
let keyboard_focus_desired = view.opened(window);
|
||||
if show_window {
|
||||
window.show();
|
||||
// WM_INITDIALOG is special in a DialogProc in that we don't need to use
|
||||
// `SetWindowLong()` for return values with special meaning.
|
||||
keyboard_focus_desired.into()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
raw::WM_SHOWWINDOW => view.shown_or_hidden(wparam == 1).into(),
|
||||
raw::WM_DESTROY => {
|
||||
let view_context = view.view_context();
|
||||
view_context.closed_subject.borrow_mut().next(());
|
||||
view_context.window.replace(None);
|
||||
view.on_destroy(window);
|
||||
ViewManager::get().unregister_view(hwnd);
|
||||
1
|
||||
}
|
||||
// This is called on Linux when receiving a keyboard message via SendMessage.
|
||||
// The only time we do this is when we want to forward keyboard interaction from
|
||||
// the RealearnAccelerator to egui. egui runs in an XBridge window - a sort
|
||||
// of child window of the SWELL window. Returning 0 here makes sure that the
|
||||
// messages are passed through to the XBridge window. This is SWELL functionality
|
||||
// (check the SWELL code).
|
||||
KEYBOARD_MSG_FOR_X_BRIDGE => 0,
|
||||
raw::WM_MOUSEMOVE => view
|
||||
.mouse_moved(Point::new(
|
||||
loword_signed(lparam as _),
|
||||
hiword_signed(lparam as _),
|
||||
))
|
||||
.into(),
|
||||
raw::WM_NCHITTEST => view
|
||||
.mouse_test(Point::new(
|
||||
loword_signed(lparam as _),
|
||||
hiword_signed(lparam as _),
|
||||
))
|
||||
.into(),
|
||||
raw::WM_SIZE => view.resized().into(),
|
||||
raw::WM_SETFOCUS => view.focused().into(),
|
||||
raw::WM_COMMAND => {
|
||||
let resource_id = loword(wparam as _);
|
||||
match hiword(wparam as _) as u32 {
|
||||
0 => {
|
||||
view.button_clicked(resource_id as _);
|
||||
// We just say the click is handled. Don't know where this would not
|
||||
// be the case.
|
||||
1
|
||||
}
|
||||
raw::CBN_SELCHANGE => {
|
||||
view.option_selected(resource_id as _);
|
||||
// We just say the selection is handled. Don't know where this would not
|
||||
// be the case.
|
||||
1
|
||||
}
|
||||
raw::EN_SETFOCUS => view.edit_control_focus_set(resource_id as _).into(),
|
||||
raw::EN_KILLFOCUS => {
|
||||
view.edit_control_focus_killed(resource_id as _).into()
|
||||
}
|
||||
raw::EN_CHANGE => {
|
||||
// Edit control change event is fired even if we change an edit control
|
||||
// text programmatically. We don't want this. In general.
|
||||
if already_entered {
|
||||
return 0;
|
||||
}
|
||||
view.edit_control_changed(resource_id as _).into()
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
raw::WM_VSCROLL => {
|
||||
let code = loword(wparam as _);
|
||||
view.scrolled_vertically(code as _).into()
|
||||
}
|
||||
raw::WM_HSCROLL => {
|
||||
if lparam <= 0 {
|
||||
// This is not a slider. Not interested.
|
||||
return 0;
|
||||
}
|
||||
let raw_slider = Hwnd::new(lparam as raw::HWND).expect("slider hwnd is null");
|
||||
view.slider_moved(Window::from_hwnd(raw_slider));
|
||||
1
|
||||
}
|
||||
raw::WM_MOUSEWHEEL => {
|
||||
let distance = hiword_signed(wparam);
|
||||
view.mouse_wheel_turned(distance).into()
|
||||
}
|
||||
raw::WM_KEYDOWN => view.key_down(wparam as _).into(),
|
||||
raw::WM_KEYUP => view.key_up(wparam as _).into(),
|
||||
raw::WM_CLOSE => {
|
||||
let processed = view.close_requested();
|
||||
if !processed {
|
||||
window.destroy();
|
||||
}
|
||||
1
|
||||
}
|
||||
raw::WM_CONTEXTMENU => {
|
||||
let x = loword(lparam as _);
|
||||
let y = hiword(lparam as _);
|
||||
view.context_menu_wanted(Point::new(Pixels(x as _), Pixels(y as _)))
|
||||
.into()
|
||||
}
|
||||
raw::WM_PAINT => isize::from(view.paint()),
|
||||
raw::WM_ERASEBKGND => {
|
||||
if let Some(hdc) = Hdc::new(wparam as raw::HDC) {
|
||||
isize::from(view.erase_background(DeviceContext::new(hdc)))
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
raw::WM_CTLCOLORSTATIC => {
|
||||
let brush = view.control_color_static(
|
||||
DeviceContext::new(
|
||||
Hdc::new(wparam as raw::HDC).expect("HDC in WM_CTLCOLORSTATIC is null"),
|
||||
),
|
||||
Window::new(lparam as raw::HWND)
|
||||
.expect("WM_CTLCOLORSTATIC control is null"),
|
||||
);
|
||||
brush.map(|b| b.as_ptr()).unwrap_or(null_mut()) as _
|
||||
}
|
||||
raw::WM_CTLCOLORDLG => {
|
||||
let brush = view.control_color_dialog(
|
||||
DeviceContext::new(
|
||||
Hdc::new(wparam as raw::HDC).expect("HDC in WM_CTLCOLORDLG is null"),
|
||||
),
|
||||
Window::new(lparam as raw::HWND).expect("WM_CTLCOLORDLG control is null"),
|
||||
);
|
||||
brush.map(|b| b.as_ptr()).unwrap_or(null_mut()) as _
|
||||
}
|
||||
raw::WM_TIMER => {
|
||||
if view.timer(wparam) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn loword(wparam: usize) -> u16 {
|
||||
(wparam & 0xffff) as _
|
||||
}
|
||||
|
||||
fn loword_signed(wparam: usize) -> i32 {
|
||||
loword(wparam) as i16 as i32
|
||||
}
|
||||
|
||||
fn hiword(wparam: usize) -> u16 {
|
||||
((wparam >> 16) & 0xffff) as _
|
||||
}
|
||||
|
||||
fn hiword_signed(wparam: usize) -> i32 {
|
||||
hiword(wparam) as i16 as i32
|
||||
}
|
||||
|
||||
// Used for global dialog proc reentrancy check.
|
||||
thread_local!(static DIALOG_PROC_ALREADY_ENTERED: Cell<bool> = const { Cell::new(false) });
|
||||
@@ -0,0 +1,29 @@
|
||||
use libloading::{Library, Symbol};
|
||||
use winapi::shared::minwindef::UINT;
|
||||
use winapi::shared::windef::HWND;
|
||||
|
||||
/// Provides access to some Win32 API functions that are not available in older Windows versions.
|
||||
///
|
||||
/// This is better than eagerly linking to these functions because then the resulting binary
|
||||
/// wouldn't work *at all* in the older Windows versions, whereas with this approach, we can
|
||||
/// fall back to alternative logic or alternative values on a case-by-case basis.
|
||||
pub struct DynamicWinApi {
|
||||
user32_library: Library,
|
||||
}
|
||||
|
||||
impl DynamicWinApi {
|
||||
pub fn load() -> Self {
|
||||
unsafe {
|
||||
Self {
|
||||
user32_library: Library::new("user32.dll").unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Should be available from Windows 10 onwards.
|
||||
pub fn get_dpi_for_window(&self) -> Option<Symbol<GetDpiForWindow>> {
|
||||
unsafe { self.user32_library.get(b"GetDpiForWindow\0").ok() }
|
||||
}
|
||||
}
|
||||
|
||||
type GetDpiForWindow = extern "system" fn(hwnd: HWND) -> UINT;
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user