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:
Paul Lipscomb
2026-07-15 17:51:05 -04:00
parent e58f06d9fa
commit 7ecc718f5d
2256 changed files with 11 additions and 5 deletions
@@ -0,0 +1,58 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
/// A simple mock which counts the number of invocations and remembers the last argument.
pub struct InvocationMock<O: Clone> {
count: Cell<u32>,
last_arg: RefCell<Option<O>>,
}
impl<O: Clone> InvocationMock<O> {
pub fn invoke(&self, arg: O) {
self.count.replace(self.count.get() + 1);
self.last_arg.replace(Some(arg));
}
/// Returns how many times `invoke()` has been called.
pub fn invocation_count(&self) -> u32 {
self.count.get()
}
/// Returns a copy of the last argument passed to `invoke()`.
///
/// # Panics
///
/// Panics if there was no invocation at all.
pub fn last_arg(&self) -> O {
self.last_arg
.borrow()
.clone()
.expect("There were no invocations")
}
}
/// Executes the given closure `op`, passing it a shared invocation mock.
///
/// The `op` closure can take ownership of the invocation mock and pass it to another 'static
/// closure, which is supposed to call the invocation mock. It also returns a pointer to the same
/// mock, which can be used later to check the number of invocations and the last passed argument.
pub fn observe_invocations<O: Clone, R>(
op: impl FnOnce(Rc<InvocationMock<O>>) -> R,
) -> (Rc<InvocationMock<O>>, R) {
let (mock_one, mock_two) = create_invocation_mock();
(mock_one, op(mock_two))
}
/// Creates two pointers referring to the same invocation mock.
///
/// This is useful for testing 'static closures, that is, closures which need captured references to
/// be 'static. Which makes tests like "this closure did run n times" non-trivial. So instead of
/// using references, we can use shared pointers.
pub fn create_invocation_mock<O: Clone>() -> (Rc<InvocationMock<O>>, Rc<InvocationMock<O>>) {
let mock = InvocationMock {
count: Cell::new(0),
last_arg: RefCell::new(None),
};
let shared_mock = Rc::new(mock);
(shared_mock.clone(), shared_mock)
}
+5
View File
@@ -0,0 +1,5 @@
mod property;
pub use property::*;
mod invocation_mock;
pub use invocation_mock::*;
@@ -0,0 +1,547 @@
use rxrust::prelude::*;
use std::fmt;
use std::marker::PhantomData;
/// The lifetime parameter determines the scope of the subscribe closure, which is relevant when
/// capturing references.
///
/// In many real-world cases you should choose 'static. But check the unit tests below. If you use
/// 'static in them, the closure would require all of its captured references to be 'static. In that
/// case we would be forced to use shared ownership (e.g. `Rc`) instead of &mut references to do
/// the test.
pub type LocalProp<'a, T, I, N, N2> =
Prop<T, I, LocalPropSubject<'a, Option<I>>, LocalPropSubject<'a, T>, N, N2>;
pub type LocalPropSubject<'a, T> = LocalSubject<'a, T, ()>;
pub type SharedProp<T, I, N, N2> =
Prop<T, I, SharedPropSubject<Option<I>>, SharedPropSubject<T>, N, N2>;
pub type SharedPropSubject<T> = SharedSubject<T, ()>;
/// A reactive property which has the following characteristics:
///
/// - It can be initialized with a transformer, which is an fn that transforms the value passed to
/// the setter before actually setting the value. It's good for restricting the value range of
/// that property. It's not good for maintaining object-wide invariants because transformers which
/// enclose over surrounding state are not advisable and therefore currently disabled by taking fn
/// only.
/// - It's cloneable if its value is cloneable (cloning it clones the value and the transformer, not
/// the change listeners)
/// - Equality operators are based just on the value, not on transformers and listeners
///
/// # Type parameters
///
/// - `T`: value type
/// - `I`: initiator type
/// - `S`: subject type
/// - `N`: notifier (not a function pointer because the notifier is usually everywhere the same in
/// one application, so we save memory by attaching it to the type instead of the instance)
pub struct Prop<T, I, S, S2, N, N2>
where
// Incomparable values don't make sense because change notification is
// what properties are all about!
T: PartialEq + Clone,
I: Copy,
S: Observer<Item = Option<I>, Err = ()> + Default,
S2: Observer<Item = T, Err = ()> + Default,
N: Notifier<T = Option<I>, Subject = S>,
N2: Notifier<T = T, Subject = S2>,
{
value: T,
subject: S,
value_subject: S2,
transformer: fn(T) -> T,
p: PhantomData<(I, N, N2)>,
}
pub trait Notifier {
type T;
type Subject: Observer<Item = Self::T, Err = ()>;
fn notify(subject: &mut Self::Subject, value: &Self::T);
}
pub struct LocalSyncNotifier<'a, T>(PhantomData<&'a T>);
impl<'a, T> Notifier for LocalSyncNotifier<'a, T>
where
T: Clone,
{
type T = T;
type Subject = LocalPropSubject<'a, T>;
fn notify(subject: &mut Self::Subject, value: &Self::T) {
subject.next(value.clone())
}
}
impl<T, I, S, S2, N, N2> Prop<T, I, S, S2, N, N2>
where
T: PartialEq + Clone,
I: Copy,
S: Observer<Item = Option<I>, Err = ()> + Default,
S2: Observer<Item = T, Err = ()> + Default,
N: Notifier<T = Option<I>, Subject = S>,
N2: Notifier<T = T, Subject = S2>,
{
/// Creates the property with an initial value and identity transformer.
pub fn new(initial_value: T) -> Self {
Self {
value: initial_value,
subject: Default::default(),
value_subject: Default::default(),
transformer: |v| v,
p: PhantomData,
}
}
/// Creates the property with an initial value and a custom transformer. The transformer is not
/// applied to the initial value.
pub fn new_with_transformer(initial_value: T, transformer: fn(T) -> T) -> Self {
Self {
value: initial_value,
subject: Default::default(),
value_subject: Default::default(),
transformer,
p: PhantomData,
}
}
/// Returns a copy of the current value of this property.
pub fn get(&self) -> T
where
T: Copy,
{
self.value
}
/// Returns the current value of this property.
pub const fn get_ref(&self) -> &T {
&self.value
}
/// Sets this property to the given value. If a transformer has been defined, the given value
/// might be changed into another one before. Observers are notified only if the given value
/// is different from the current value.
pub fn set(&mut self, value: T) {
self.internal_set(value, None);
}
/// Sets this property to the given value using the given initiator.
pub fn set_with_initiator(&mut self, value: T, initiator: Option<I>) {
self.internal_set(value, initiator);
}
fn internal_set(&mut self, value: T, initiator: Option<I>) {
let transformed_value = (self.transformer)(value);
if transformed_value == self.value {
return;
}
self.value = transformed_value;
self.notify(initiator);
}
fn notify(&mut self, initiator: Option<I>) {
N::notify(&mut self.subject, &initiator);
N2::notify(&mut self.value_subject, &self.value);
}
pub fn set_without_notification(&mut self, value: T) {
let transformed_value = (self.transformer)(value);
if transformed_value == self.value {
return;
}
self.value = transformed_value;
}
pub fn set_with_optional_notification(&mut self, value: T, with_notification: bool) {
if with_notification {
self.set(value);
} else {
self.set_without_notification(value);
}
}
pub fn set_with_optional_notification_and_initiator(
&mut self,
value: T,
with_notification: bool,
initiator: Option<I>,
) {
if with_notification {
self.set_with_initiator(value, initiator);
} else {
self.set_without_notification(value);
}
}
/// Like `set` but returns old value.
pub fn replace(&mut self, value: T) -> T {
let old_value = self.value.clone();
self.set(value);
old_value
}
/// Sets the value of this property to the value of the given one, invoking listeners.
///
/// Consumes the given property.
pub fn apply_from(&mut self, other: Self) {
self.set(other.value)
}
/// Like `set()`, but lets you use the previous value for calculating the new one.
pub fn set_with(&mut self, f: impl Fn(&T) -> T) {
let value = f(&self.value);
self.set(value);
}
/// Modifies the value in place and notifies listeners.
pub fn mut_in_place(&mut self, f: impl Fn(&mut T)) {
f(&mut self.value);
self.notify(None);
}
pub fn set_with_with_initiator(&mut self, f: impl Fn(&T) -> T, initiator: Option<I>) {
let value = f(&self.value);
self.internal_set(value, initiator);
}
}
impl<'a, T, I, N, N2> LocalProp<'a, T, I, N, N2>
where
T: PartialEq + Clone,
I: Copy + 'a,
N: Notifier<T = Option<I>, Subject = LocalPropSubject<'a, Option<I>>>,
N2: Notifier<T = T, Subject = LocalPropSubject<'a, T>>,
{
/// Fires whenever the value has changed.
///
/// Event always contains a unit value instead of the
/// new value. This is perfect for combining observables because observables can be combined
/// much easier if they have the same type. UI event handlers for example are often not
/// interested in the new value anyway because they will just call some reusable
/// invalidation code that queries the new value itself.
pub fn changed(&self) -> impl LocalObservable<'a, Item = (), Err = ()> {
self.subject.clone().map_to(())
}
/// Fires whenever the value has changed. Also delivers the initiator of the change, if any.
pub fn changed_with_initiator(&self) -> impl LocalObservable<'a, Item = Option<I>, Err = ()> {
self.subject.clone()
}
/// Fires whenever the value has changed to the given value.
pub fn changed_to(&self, value: T) -> impl LocalObservable<'a, Item = (), Err = ()>
where
T: Clone + 'static,
{
self.value_subject
.clone()
.filter(move |v| *v == value)
.map_to(())
}
pub fn values(&self) -> impl LocalObservable<'a, Item = T, Err = ()>
where
T: Clone + 'static,
{
self.value_subject.clone()
}
}
impl<T, I, N, N2> SharedProp<T, I, N, N2>
where
T: PartialEq + Clone,
I: Copy + 'static,
N: Notifier<T = Option<I>, Subject = SharedPropSubject<Option<I>>>,
N2: Notifier<T = T, Subject = SharedPropSubject<T>>,
{
pub fn changed(
&self,
) -> impl SharedObservable<Unsub = SharedSubscription, Item = (), Err = ()> + 'static + Send + Sync
{
self.subject.clone().map_to(())
}
}
impl<T, I, S, S2, N, N2> fmt::Debug for Prop<T, I, S, S2, N, N2>
where
T: PartialEq + Clone + fmt::Debug,
I: Copy,
S: Observer<Item = Option<I>, Err = ()> + Default,
S2: Observer<Item = T, Err = ()> + Default,
N: Notifier<T = Option<I>, Subject = S>,
N2: Notifier<T = T, Subject = S2>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Property")
.field("value", &self.value)
.finish()
}
}
impl<T, I, S, S2, N, N2> Clone for Prop<T, I, S, S2, N, N2>
where
T: PartialEq + Clone,
I: Copy,
S: Observer<Item = Option<I>, Err = ()> + Default,
S2: Observer<Item = T, Err = ()> + Default,
N: Notifier<T = Option<I>, Subject = S>,
N2: Notifier<T = T, Subject = S2>,
{
fn clone(&self) -> Self {
Self {
value: self.value.clone(),
subject: Default::default(),
value_subject: Default::default(),
transformer: self.transformer,
p: PhantomData,
}
}
}
impl<T, I, S, S2, N, N2> Default for Prop<T, I, S, S2, N, N2>
where
T: PartialEq + Clone + Default,
I: Copy,
S: Observer<Item = Option<I>, Err = ()> + Default,
S2: Observer<Item = T, Err = ()> + Default,
N: Notifier<T = Option<I>, Subject = S>,
N2: Notifier<T = T, Subject = S2>,
{
fn default() -> Self {
Self {
value: Default::default(),
subject: Default::default(),
value_subject: Default::default(),
transformer: |v| v,
p: PhantomData,
}
}
}
impl<T, I, S, S2, N, N2> From<T> for Prop<T, I, S, S2, N, N2>
where
T: PartialEq + Clone,
I: Copy,
S: Observer<Item = Option<I>, Err = ()> + Default,
S2: Observer<Item = T, Err = ()> + Default,
N: Notifier<T = Option<I>, Subject = S>,
N2: Notifier<T = T, Subject = S2>,
{
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T, I, S, S2, N, N2> PartialEq for Prop<T, I, S, S2, N, N2>
where
T: PartialEq + Clone,
I: Copy,
S: Observer<Item = Option<I>, Err = ()> + Default,
S2: Observer<Item = T, Err = ()> + Default,
N: Notifier<T = Option<I>, Subject = S>,
N2: Notifier<T = T, Subject = S2>,
{
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get() {
// Given
let p = prop(5);
// When
// Then
assert_eq!(p.get(), 5);
}
#[test]
fn set() {
// Given
let mut p = prop(5);
// When
p.set(6);
// Then
assert_eq!(p.get(), 6);
}
#[test]
fn clone() {
// Given
let p = prop(5);
// When
let p2 = p.clone();
// Then
assert_eq!(p.get(), 5);
assert_eq!(p2.get(), 5);
}
#[test]
fn clone_set_independent() {
// Given
let mut p = prop(5);
// When
let mut p2 = p.clone();
p.set(2);
p2.set(7);
// Then
assert_eq!(p.get(), 2);
assert_eq!(p2.get(), 7);
}
#[test]
fn transformer() {
// Given
let mut p = local_prop_with_transformer(5, |v| v.min(100));
// When
p.set(105);
// Then
assert_eq!(p.get(), 100);
}
#[test]
fn clone_transformer_works() {
// Given
let p = local_prop_with_transformer(5, |v| v.min(100));
// When
let mut p2 = p.clone();
p2.set(105);
// Then
assert_eq!(p2.get(), 100);
}
#[test]
fn observe() {
// Given
let mut invocation_count = 0;
// When
{
let mut p = prop(5);
p.changed().subscribe(|_v| invocation_count += 1);
p.set(6);
}
// Then
assert_eq!(invocation_count, 1);
}
#[test]
fn clone_observe_independent() {
// Given
let mut p_invocation_count = 0;
let mut p2_invocation_count = 0;
// When
{
let mut p = prop(5);
p.changed().subscribe(|_v| p_invocation_count += 1);
let mut p2 = p.clone();
p2.changed().subscribe(|_v| p2_invocation_count += 1);
p.set(6);
p2.set(6);
}
// Then
assert_eq!(p_invocation_count, 1);
assert_eq!(p2_invocation_count, 1);
}
/// In C++ ReaLearn, we used to automatically adjust other fields in a struct whenever the
/// value of a property in that struct has changed by subscribing to it in the constructor.
/// Either to ensure that min value is always <= max value, or to keep the
/// processor in sync with the model. Each of those cases boils down to having a
/// self-referential struct: The struct holds an rx subject which holds a subscriber which
/// points "back" to a field of the very same struct.
///
/// In Rust, such self references would turn invalid as soon as the type moves, because moving
/// in Rust means memcpy to a different place in memory - which would let pointers/references
/// dangle. This is shown in the test.
///
/// In C++ this was possible because C++ has a move constructor called when moving an object, in
/// which all self references can be reestablished. Rust intentionally doesn't have such move
/// constructors because always doing a simple memcpy has a multitude of advantages.
///
/// In Rust, we can achieve the same by making sure the self-referenced data will stay where it
/// is, even if moved. We do that by putting it on the heap (e.g. using Box or Rc).
///
/// Another way is to always calculate the memory address of the self-referenced data
/// on-the-fly:
///
/// > So, to recap: instead of storing a pointer to an object itself, store some
/// > information so that you can calculate the pointer later. This is also commonly called
/// using > “handles”. (https://blog.sentry.io/2018/04/05/you-cant-rust-that)
///
/// Or we use this opportunity to reconsider the design. Instead of enforcing that min value is
/// <= max value, we could just let it happen and instead provide an additional method which
/// returns the fixed value ... a more functional style. After all, the models are not the kind
/// of core domain objects for which it is important that they keep invariants. They are
/// made specifically for UI and (de)serialization needs. That's also why we have domain
/// counterparts without the suffix `Model`, which have no properties, are immutable and
/// therefore don't have this kind of issues by definition.
///
/// Regarding use case 2, the "cached" processor to keep in sync with the model: An `Rc` would
/// certainly do the job, in our case with totally neglectable overhead. Or we don't expose
/// the properties directly and use setter methods which take care of updating the cached
/// processor. However, at first we might just want to go without caching the processor at all!
#[test]
fn update_other_member_on_change_fail() {
struct Combination<'a> {
value: TestProp<'a, i32>,
_derived_value: i32,
}
impl<'a> Combination<'a> {
fn new(initial_value: i32) -> Combination<'a> {
let mut c = Combination {
value: prop(initial_value),
_derived_value: initial_value,
};
let c_ptr = to_ptr(&mut c);
c.value.changed().subscribe(move |_| {
// This won't work because at the time we move c out of this `new` function,
// it will move to a different address in memory. In the old C++ code, this
// only worked because we did the subscription in a move/copy constructor, which
// was called whenever this value was moved.
// Related to discussion here: https://internals.rust-lang.org/t/idea-limited-custom-move-semantics-through-explicitly-specified-relocations/6704/12
let c = unsafe { &mut *c_ptr };
c._derived_value = c.value.get() * 2;
});
c
}
}
fn to_ptr<T>(value: &mut T) -> *mut T {
value as *mut T
}
// Given
let mut c = Combination::new(5);
to_ptr(&mut c);
// The following would most likely crash!
// c.value.set(8);
}
// TODO-low Add some SharedProp tests
type TestProp<'a, T> =
LocalProp<'a, T, u32, LocalSyncNotifier<'a, Option<u32>>, LocalSyncNotifier<'a, T>>;
fn prop<'a, T>(initial_value: T) -> TestProp<'a, T>
where
T: PartialEq + Clone,
{
TestProp::new(initial_value)
}
fn local_prop_with_transformer<'a, T>(
initial_value: T,
transformer: fn(T) -> T,
) -> TestProp<'a, T>
where
T: PartialEq + Clone,
{
TestProp::new_with_transformer(initial_value, transformer)
}
}