Vendor helgoboss/helgobox (ReaLearn) as basis for custom UI fork

Stripped upstream git history; starting point for replacing the native
SWELL/Win32 mapping UI with something more suited to bulk editing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-07-15 17:38:29 -04:00
parent 583ed77a67
commit e58f06d9fa
2232 changed files with 685575 additions and 1 deletions
@@ -0,0 +1,36 @@
[package]
name = "swell-ui"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[dependencies]
base.workspace = true
reaper-medium.workspace = true
reaper-low.workspace = true
reaper-common-types.workspace = true
rxrust.workspace = true
scopeguard.workspace = true
raw-window-handle.workspace = true
tracing.workspace = true
fragile.workspace = true
palette.workspace = true
[target.'cfg(windows)'.dependencies]
winapi = { workspace = true, features = ["winuser", "uxtheme"] }
libloading.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
objc2.workspace = true
# Disabled until we get egui to work nicely (search for "wonky"). It's also important to use cross v0.3.0 then
# (once it's released) because cross v2.5 uses Ubuntu < v20, which has an old glib that's not compatible with the
# glib crate (gdk-sys dependency).
# [target.'cfg(target_os = "linux")'.dependencies]
# gdk-sys = "0.17.0"
# gdkx11-sys = "0.17.0"
[lints.clippy]
enum_glob_use = "deny"
@@ -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] }
}
}
);
+164
View File
@@ -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(),
}
}
}
+368
View File
@@ -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