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
54 changes: 54 additions & 0 deletions crates/perry-ui-windows/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,43 @@ thread_local! {
static NEXT_HOTKEY_ID: std::cell::Cell<i32> = std::cell::Cell::new(1);
}

/// Should `msg`'s target get dialog-style keyboard navigation
/// (IsDialogMessageW: Tab / Shift-Tab / arrow-key focus movement)?
///
/// True when the message targets one of OUR widget HWNDs — walking up to
/// 4 ancestors so the inner EDIT of a composite control (COMBOBOX, etc.)
/// still qualifies — and that widget is not a WebView: WebView2's
/// Chromium children own their keyboard handling, and routing their keys
/// through the dialog manager would steal Tab from web content. Foreign
/// windows (owned dialogs, IME windows, thread messages with hwnd=0)
/// never qualify.
#[cfg(target_os = "windows")]
fn wants_dialog_navigation(hwnd: HWND) -> bool {
let mut probe = hwnd;
for _ in 0..4 {
if probe.0.is_null() {
return false;
}
// Our top-level windows qualify directly — before any control has
// focus, keyboard messages target the window itself, and the first
// Tab must still bootstrap focus into the first WS_TABSTOP child.
let probe_val = probe.0 as isize;
let is_app_window =
APPS.with(|apps| apps.borrow().iter().any(|a| a.hwnd.0 as isize == probe_val));
if is_app_window || crate::window::is_perry_window_hwnd(probe_val) {
return true;
}
let handle = crate::widgets::find_handle_by_hwnd(probe);
if handle != 0 {
// WebViews register as WidgetKind::Image (host HWND reuse), so
// ask the webview module directly.
return !crate::widgets::webview::is_webview(handle);
}
probe = unsafe { GetParent(probe).unwrap_or_default() };
}
false
}

/// Get the HWND of the first (main) app window.
#[cfg(target_os = "windows")]
pub fn get_main_hwnd() -> Option<HWND> {
Expand Down Expand Up @@ -498,6 +535,23 @@ pub fn app_run(app_handle: i64) {
false,
);
}
// Dialog-style keyboard navigation (Tab / Shift-Tab / arrow
// keys between WS_TABSTOP controls). A raw GetMessage →
// DispatchMessage pump never runs the dialog manager, so
// every control's WS_TABSTOP bit was dead and Tab did
// nothing in Perry windows. IsDialogMessageW performs its
// own translate+dispatch when it handles a message, so skip
// the normal dispatch then. Deliberately AFTER the shortcut
// and onKeyDown/onKeyUp dispatch above: app shortcuts still
// win, and JS key events still observe Tab presses.
if (WM_KEYFIRST..=WM_KEYLAST).contains(&msg.message)
&& wants_dialog_navigation(msg.hwnd)
{
let root = GetAncestor(msg.hwnd, GA_ROOT);
if !root.0.is_null() && IsDialogMessageW(root, &msg).as_bool() {
continue;
}
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
// Process setTimeout/setInterval callbacks outside wndproc to avoid re-entrancy
Expand Down
8 changes: 6 additions & 2 deletions crates/perry-ui-windows/src/widgets/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ pub fn create() -> i64 {
unsafe {
let hinstance = GetModuleHandleW(None).unwrap();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
// WS_EX_CONTROLPARENT: dialog manager recurses into
// containers for Tab navigation (see vstack.rs).
WINDOW_EX_STYLE(WS_EX_CONTROLPARENT.0),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(window_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
Expand Down Expand Up @@ -110,7 +112,9 @@ pub fn section_create(title_ptr: *const u8) -> i64 {

// Create the BS_GROUPBOX frame
let groupbox_hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
// WS_EX_CONTROLPARENT: dialog manager recurses into
// containers for Tab navigation (see vstack.rs).
WINDOW_EX_STYLE(WS_EX_CONTROLPARENT.0),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(wide_title.as_ptr()),
WINDOW_STYLE(BS_GROUPBOX as u32 | WS_CHILD.0 | WS_VISIBLE.0 | WS_CLIPCHILDREN.0),
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-ui-windows/src/widgets/hstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ pub fn create_with_insets(spacing: f64, top: f64, left: f64, bottom: f64, right:
unsafe {
let hinstance = GetModuleHandleW(None).unwrap();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
// WS_EX_CONTROLPARENT: dialog manager recurses into
// containers for Tab navigation (see vstack.rs).
WINDOW_EX_STYLE(WS_EX_CONTROLPARENT.0),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(window_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-ui-windows/src/widgets/navstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ pub fn create(title_ptr: *const u8, body_handle: i64) -> i64 {
unsafe {
let hinstance = GetModuleHandleW(None).unwrap();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
// WS_EX_CONTROLPARENT: dialog manager recurses into
// containers for Tab navigation (see vstack.rs).
WINDOW_EX_STYLE(WS_EX_CONTROLPARENT.0),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(window_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-ui-windows/src/widgets/scrollview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ pub fn create() -> i64 {
unsafe {
let hinstance = GetModuleHandleW(None).unwrap();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
// WS_EX_CONTROLPARENT: dialog manager recurses into
// containers for Tab navigation (see vstack.rs).
WINDOW_EX_STYLE(WS_EX_CONTROLPARENT.0),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(window_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_CLIPCHILDREN,
Expand Down
6 changes: 5 additions & 1 deletion crates/perry-ui-windows/src/widgets/vstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@ pub fn create_with_insets(spacing: f64, top: f64, left: f64, bottom: f64, right:
unsafe {
let hinstance = GetModuleHandleW(None).unwrap();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
// WS_EX_CONTROLPARENT: the dialog manager (IsDialogMessageW
// in the message pump) must recurse INTO containers to find
// WS_TABSTOP controls — without it, Tab stops dead at the
// container level.
WINDOW_EX_STYLE(WS_EX_CONTROLPARENT.0),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(window_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-ui-windows/src/widgets/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ thread_local! {
#[cfg(target_os = "windows")]
const WEBVIEW_SUBCLASS_ID: usize = 0x77_76_69_77; // 'w','v','i','w'

/// True when `handle` belongs to a WebView. WebViews register their host
/// HWND as `WidgetKind::Image`, so kind checks can't identify them — the
/// message pump uses this to keep dialog-style Tab navigation away from
/// WebView2's Chromium children (they own their keyboard handling).
#[cfg(target_os = "windows")]
pub fn is_webview(handle: i64) -> bool {
WEBVIEW_STATES.with(|m| m.borrow().contains_key(&handle))
}

#[cfg(not(target_os = "windows"))]
pub fn is_webview(_handle: i64) -> bool {
false
}

static NEXT_DATA_DIR_TAG: AtomicI64 = AtomicI64::new(1);

// =============================================================================
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-ui-windows/src/widgets/zstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ pub fn create() -> i64 {
unsafe {
let hinstance = GetModuleHandleW(None).unwrap();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
// WS_EX_CONTROLPARENT: dialog manager recurses into
// containers for Tab navigation (see vstack.rs).
WINDOW_EX_STYLE(WS_EX_CONTROLPARENT.0),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(window_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-ui-windows/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ thread_local! {
static HWND_TO_WINDOW: RefCell<HashMap<isize, i64>> = RefCell::new(HashMap::new());
}

/// True when `hwnd` is a secondary Perry `Window(...)` top-level. Used by
/// the message pump's dialog-navigation gate (Tab focus movement must also
/// engage while focus sits on the window itself, before any control has
/// been focused).
#[cfg(target_os = "windows")]
pub fn is_perry_window_hwnd(hwnd_val: isize) -> bool {
HWND_TO_WINDOW.with(|m| m.borrow().contains_key(&hwnd_val))
}

fn str_from_header(ptr: *const u8) -> &'static str {
if ptr.is_null() {
return "";
Expand Down
Loading