linebender/druid · error

release_input_lock was called on a WinHandler that did not…

Error message

release_input_lock was called on a WinHandler that did not expect text input.

What it means

This panic is the default (no-op) implementation of `WinHandler::release_input_lock` in druid-shell's window.rs. The shell calls it when a platform text-input path finishes editing and tries to release the lock acquired via `acquire_input_lock`, but the handler's default implementation did not expect any text input. It mirrors the acquire-side panic and signals the same contract violation on the release path.

Solutions

  1. Override `release_input_lock` in your WinHandler to unlock the state corresponding to the given TextFieldToken
  2. Also override `acquire_input_lock` — if release fired, acquire likely did too; implement both symmetrically
  3. Track which tokens were acquired so release can be matched correctly (map from token to handler state)

Example fix

// before
impl WinHandler for MyAppHandler {} // panicking defaults
// after
impl WinHandler for MyAppHandler {
    fn release_input_lock(&mut self, token: TextFieldToken) {
        if let Some(field) = self.text_field_for(token) {
            field.unlock_input();
        }
    }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Only release locks for tokens the handler actually acquired:
fn has_acquired(handler: &MyHandler, token: TextFieldToken) -> bool {
    handler.acquired_tokens.contains(&token)
}

Type guard

fn handles_text_input(handler: &dyn WinHandler) -> bool {
    !handler.text_widgets().is_empty()
}

Try / catch

// Avoid relying on panic recovery; guard instead:
if handler.handles_text_input() {
    handler.release_input_lock(token);
} else {
    tracing::warn!("release_input_lock on non-text handler ignored");
}

Prevention

When it happens

Trigger: The backend completes a text-editing session and calls `release_input_lock(token)` on a WinHandler using the default trait implementation that never registered text fields or overrode the lock methods.

Common situations: Custom WinHandlers with default trait impls receiving IME events; a text field whose acquire was handled elsewhere but release lands on the default; version skew where druid-shell added the lock API after the handler was written.

Related errors


AI-assisted analysis of linebender/druid@0f8b1195e4 (2026-09-10). Data as JSON: /api/errors/13fccfa63f240ccf. Report an issue: GitHub.

Appendix: source

Thrown at druid-shell/src/window.rs:670

    ///
    /// For more information, see [the text input documentation](crate::text).
    ///
    /// [`release_input_lock`]: WinHandler::release_input_lock
    #[allow(unused_variables)]
    fn acquire_input_lock(
        &mut self,
        token: TextFieldToken,
        mutable: bool,
    ) -> Box<dyn InputHandler> {
        panic!("acquire_input_lock was called on a WinHandler that did not expect text input.")
    }

    /// Release a lock previously acquired by [`acquire_input_lock`].
    ///
    /// [`acquire_input_lock`]: WinHandler::acquire_input_lock
    #[allow(unused_variables)]
    fn release_input_lock(&mut self, token: TextFieldToken) {
        panic!("release_input_lock was called on a WinHandler that did not expect text input.")
    }

    /// Called on a mouse wheel event.
    ///
    /// The polarity is the amount to be added to the scroll position,
    /// in other words the opposite of the direction the content should
    /// move on scrolling. This polarity is consistent with the
    /// deltaX and deltaY values in a web [WheelEvent].
    ///
    /// [WheelEvent]: https://w3c.github.io/uievents/#event-type-wheel
    #[allow(unused_variables)]
    fn wheel(&mut self, event: &MouseEvent) {}

    /// Called when a platform-defined zoom gesture occurs (such as pinching
    /// on the trackpad).
    #[allow(unused_variables)]
    fn zoom(&mut self, delta: f64) {}

View on GitHub (pinned to 0f8b1195e4)