Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/noa-app/src/app/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,18 @@ impl ApplicationHandler<UserEvent> for App {
.map(|state| state.window.clone())
{
window.focus_window();
// AppKit's native-tab teardown drops first responder off
// winit's content view, so `focus_window` (already-key
// window) leaves `KeyboardInput` dead. Re-target first
// responder at the content view, then discard any stale
// marked text by toggling IME off/on (same clear the
// `Focused(false)` path uses).
#[cfg(target_os = "macos")]
{
crate::macos_window::focus_content_view(&window);
window.set_ime_allowed(false);
window.set_ime_allowed(true);
}
}
}
UserEvent::PtyExit(window_id, pane_id) => {
Expand Down Expand Up @@ -375,6 +387,16 @@ impl ApplicationHandler<UserEvent> for App {
self.secure_input
.on_focus_change(true, &mut crate::secure_input::CarbonSecureInput);
if let Some(state) = self.windows.get(&window_id) {
// Belt-and-suspenders for the native-tab-close focus loss:
// whether or not the deferred `RestoreFocus` lands, a real
// focus gain re-anchors first responder on the content view
// and re-arms IME so key input can't stay dead.
#[cfg(target_os = "macos")]
{
crate::macos_window::focus_content_view(&state.window);
state.window.set_ime_allowed(false);
state.window.set_ime_allowed(true);
}
state.window.request_redraw();
}
}
Expand Down
38 changes: 38 additions & 0 deletions crates/noa-app/src/macos_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ pub(crate) fn show_quick_terminal_window(window: &Window) {
show_quick_terminal_window_impl(window);
}

/// Restore first responder to winit's content `NSView`. AppKit's native-tab
/// teardown moves first responder off the text-input view, so `KeyboardInput`
/// stops arriving even though the window is already key — `focus_window`
/// (`makeKeyAndOrderFront:`) can't fix that on an already-key window. This
/// re-targets first responder at the content view directly.
pub(crate) fn focus_content_view(window: &Window) {
focus_content_view_impl(window);
}

/// Commit a just-shown window to the screen immediately: draw any dirty
/// views (`displayIfNeeded`) and flush the pending window-server transaction
/// (`CATransaction flush`). Startup W1: the first window is shown from inside
Expand Down Expand Up @@ -454,6 +463,35 @@ fn show_quick_terminal_window_impl(window: &Window) {
}
}

#[cfg(target_os = "macos")]
fn focus_content_view_impl(window: &Window) {
use objc2::msg_send;
use objc2::runtime::AnyObject;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};

let Ok(handle) = window.window_handle() else {
return;
};
let RawWindowHandle::AppKit(appkit) = handle.as_raw() else {
return;
};
let ns_view = appkit.ns_view.as_ptr().cast::<AnyObject>();

// SAFETY: `ns_view` is winit's live AppKit `NSView` for this window and we
// are on the main event-loop thread. `makeFirstResponder:` is an AppKit
// window method; both pointers are nil-checked before use.
unsafe {
let ns_window: *mut AnyObject = msg_send![ns_view, window];
if ns_window.is_null() {
return;
}
let _: bool = msg_send![ns_window, makeFirstResponder: ns_view];
}
}

#[cfg(not(target_os = "macos"))]
fn focus_content_view_impl(_window: &Window) {}

#[cfg(not(target_os = "macos"))]
fn show_quick_terminal_window_impl(window: &Window) {
window.focus_window();
Expand Down