Skip to content

Conversation

@aidan729
Copy link

@aidan729 aidan729 commented Nov 8, 2025

This commit introduces a queued resize system that allows plugins to defer window resize operations to avoid borrow checker conflicts during event handling, while maintaining thread safety.

Changes

1. Added resize queue (RefCell-based)

  • Added pending_resizes: RefCell<Vec<(u32, u32)>> to WindowHandler
  • Uses RefCell instead of Arc<Mutex<>> because WindowHandler is GUI-thread-only and never crosses thread boundaries
  • More ergonomic and performant than mutex for single-threaded access

2. New public API methods

queue_resize(width: u32, height: u32)

  • Queues a resize request without immediately executing it
  • Allows plugins to request resizes during event handling without borrowing conflicts

process_pending_resizes(window: &mut Window) -> Option<(u32, u32)>

  • Processes the most recent queued resize request
  • Automatically clears older requests to prevent resize lag
  • Returns the applied size for plugins to sync UI state
  • Safe to call repeatedly (returns None if queue is empty)

Use Case

This pattern is useful for plugins that need to:

  1. Handle resize requests from the frontend during event callbacks
  2. Sync resize state back to frontend after completion
  3. Avoid borrow conflicts when calling resize during message handling

Why RefCell over Mutex?

WindowHandler is only accessed from the GUI thread (baseview guarantees this), so RefCell is appropriate and more ergonomic:

  • No need for .lock().unwrap() boilerplate
  • Better performance (no atomic operations)
  • Clearer intent (panic on misuse vs deadlock)
  • Follows Rust's 'use the weakest synchronization primitive' principle

Backwards Compatibility

These changes are fully backwards compatible:

  • New fields are private implementation details
  • New methods are additive (existing code continues to work)
  • No changes to existing public API surface

Example Usage

// In message handler (can't borrow window mutably here)
"resizeRequest" => {
    ctx.queue_resize(width, height);
}

// After event loop completes
if let Some((w, h)) = ctx.process_pending_resizes(window) {
    // Sync new size back to frontend
    ctx.send_json(json!({
        "type": "resizeUpdate",
        "width": w,
        "height": h
    }));
}

This commit introduces a queued resize system that allows plugins to defer
window resize operations to avoid borrow checker conflicts during event
handling, while maintaining thread safety.

## Changes

### 1. Added resize queue (RefCell-based)
- Added pending_resizes: RefCell<Vec<(u32, u32)>> to WindowHandler
- Uses RefCell instead of Arc<Mutex<>> because WindowHandler is
  GUI-thread-only and never crosses thread boundaries
- More ergonomic and performant than mutex for single-threaded access

### 2. New public API methods

**queue_resize(width: u32, height: u32)**
- Queues a resize request without immediately executing it
- Allows plugins to request resizes during event handling without
  borrowing conflicts

**process_pending_resizes(window: &mut Window) -> Option<(u32, u32)>**
- Processes the most recent queued resize request
- Automatically clears older requests to prevent resize lag
- Returns the applied size for plugins to sync UI state
- Safe to call repeatedly (returns None if queue is empty)

## Use Case

This pattern is useful for plugins that need to:
1. Handle resize requests from the frontend during event callbacks
2. Sync resize state back to frontend after completion
3. Avoid borrow conflicts when calling resize during message handling

## Why RefCell over Mutex?

WindowHandler is only accessed from the GUI thread (baseview guarantees
this), so RefCell is appropriate and more ergonomic:
- No need for .lock().unwrap() boilerplate
- Better performance (no atomic operations)
- Clearer intent (panic on misuse vs deadlock)
- Follows Rust's 'use the weakest synchronization primitive' principle

## Backwards Compatibility

These changes are fully backwards compatible:
- New fields are private implementation details
- New methods are additive (existing code continues to work)
- No changes to existing public API surface
@httnn
Copy link
Owner

httnn commented Nov 8, 2025

thanks for the PR! wondering whether there should instead be a more generic system for deferring any type of task from the message handler to the event loop? or is there some reason why resizing is special in this regard? haven't used this library myself in a long while so don't remember all of the details.

another thing: why have a queue when only the latest resize event is acknowledged? seems like a simple Option<(u32, u32)> would suffice.

@aidan729
Copy link
Author

thanks for the PR! wondering whether there should instead be a more generic system for deferring any type of task from the message handler to the event loop? or is there some reason why resizing is special in this regard? haven't used this library myself in a long while so don't remember all of the details.

another thing: why have a queue when only the latest resize event is acknowledged? seems like a simple Option<(u32, u32)> would suffice.

Thanks for taking the time to review this PR! I really appreciate the feedback. To give some context on why I added this feature: I ran into a critical thread safety issue where resize requests were being triggered from the audio processing thread, but resize operations must execute on the GUI thread. The fundamental problem is that baseview's window resize operations aren't thread safe and can only be called from the main GUI event loop. When resize requests come from the audio thread for example through parameter automation or plugin state changes - we need a way to safely defer them until we're back in the GUI thread's event loop where it's safe to actually perform the resize. This is why I went with a resize specific queue rather than a generic task queue, resize is the only operation that has this specific cross thread requirement where it must be deferred from the audio thread to the GUI thread. The queue acts as a thread safe handoff mechanism using RefCell (since the actual queue access only happens on the GUI thread) to collect resize requests and process them during on_frame() where we have the proper thread context. That said, you're absolutely right about the Vec vs Option question. Looking at the implementation, process_pending_resizes() only uses the last resize and discards everything else with queue.clear(), so an Option<(u32, u32)> would be both simpler and more semantically correct. There's no benefit to tracking multiple pending resizes since we only care about the final desired size. I'm happy to refactor this to use Option instead, if you think it would make the code cleaner and the intent more obvious. Would you prefer I update this PR with that change, or would you rather handle that refactoring yourself if you decide to merge this approach? either way I hope you might consider this as a change.

@httnn
Copy link
Owner

httnn commented Nov 14, 2025

I'm happy to refactor this to use Option instead, if you think it would make the code cleaner and the intent more obvious.

yeah this would be good, it doesn't only make the code cleaner, it also communicates the behaviour much better (and avoid some allocations, although that's negligible).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants