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
20 changes: 20 additions & 0 deletions crates/perry-codegen/src/lower_call/native/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,7 @@ pub(crate) fn lower_native_method_call(
let mut height_d: String = "768.0".to_string();
let mut body_handle: String = "0".to_string();
let mut icon_ptr: Option<String> = None;
let mut window_state_ptr: Option<String> = None;
for (key, val) in &props {
match key.as_str() {
"title" => {
Expand All @@ -1303,6 +1304,14 @@ pub(crate) fn lower_native_method_call(
let blk = ctx.block();
icon_ptr = Some(unbox_to_i64(blk, &v));
}
// Issue #1280 — `windowState: "normal" | "maximized" | "fullscreen"`.
// Forwarded to perry_ui_app_set_window_state; each platform
// backend applies the state at app_run time.
"windowState" => {
let v = lower_expr(ctx, val)?;
let blk = ctx.block();
window_state_ptr = Some(unbox_to_i64(blk, &v));
}
_ => {
let _ = lower_expr(ctx, val)?;
}
Expand All @@ -1318,6 +1327,11 @@ pub(crate) fn lower_native_method_call(
crate::types::VOID,
vec![I64],
));
ctx.pending_declares.push((
"perry_ui_app_set_window_state".to_string(),
crate::types::VOID,
vec![I64, I64],
));
ctx.pending_declares.push((
"perry_ui_app_set_body".to_string(),
crate::types::VOID,
Expand All @@ -1337,6 +1351,12 @@ pub(crate) fn lower_native_method_call(
if let Some(icon) = icon_ptr {
blk.call_void("perry_ui_app_set_icon", &[(I64, &icon)]);
}
if let Some(state_ptr) = window_state_ptr {
blk.call_void(
"perry_ui_app_set_window_state",
&[(I64, &app_handle), (I64, &state_ptr)],
);
}
blk.call_void(
"perry_ui_app_set_body",
&[(I64, &app_handle), (I64, &body_handle)],
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-ui-android/src/ffi/tabbar_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,7 @@ pub extern "C" fn perry_ui_app_set_vibrancy(_app_handle: i64, _value_ptr: i64) {

#[no_mangle]
pub extern "C" fn perry_ui_app_set_activation_policy(_app_handle: i64, _value_ptr: i64) {}

/// Issue #1280 — Android apps run in a single full-screen Activity. Stub.
#[no_mangle]
pub extern "C" fn perry_ui_app_set_window_state(_app_handle: i64, _value_ptr: i64) {}
34 changes: 34 additions & 0 deletions crates/perry-ui-gtk4/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ struct AppEntry {
transparent: bool,
vibrancy: Option<String>,
activation_policy: Option<String>,
/// Issue #1280 — "maximized" | "fullscreen" | None (= "normal").
window_state: Option<String>,
}

extern "C" {
Expand Down Expand Up @@ -116,6 +118,7 @@ pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 {
transparent: false,
vibrancy: None,
activation_policy: None,
window_state: None,
});
apps.len() as i64 // 1-based handle
})
Expand Down Expand Up @@ -276,6 +279,17 @@ pub fn app_run(_app_handle: i64) {
window.set_show_menubar(true);
}

// Issue #1280 — initial window state. GTK4 needs maximize() /
// fullscreen() called before `present()` so the window appears
// already in the requested state rather than flickering.
if let Some(ref state) = entry.window_state {
match state.as_str() {
"maximized" => window.maximize(),
"fullscreen" => window.fullscreen(),
_ => {}
}
}

window.present();
}
});
Expand Down Expand Up @@ -536,6 +550,26 @@ pub fn app_set_activation_policy(app_handle: i64, value_ptr: *const u8) {
});
}

/// Issue #1280 — initial window state. value_ptr points at a StringHeader
/// for one of "normal" | "maximized" | "fullscreen". Anything else is
/// silently ignored; the state is applied just before `window.present()`.
pub fn app_set_window_state(app_handle: i64, value_ptr: *const u8) {
let state_str = str_from_header(value_ptr);
if state_str.is_empty() {
return;
}
APPS.with(|a| {
let mut apps = a.borrow_mut();
let idx = (app_handle - 1) as usize;
if idx < apps.len() {
apps[idx].window_state = match state_str {
"maximized" | "fullscreen" => Some(state_str.to_string()),
_ => None,
};
}
});
}

/// Install keyboard shortcuts on a window using EventControllerKey.
fn install_shortcuts_on_window(window: &ApplicationWindow) {
let controller = EventControllerKey::new();
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-ui-gtk4/src/ffi/app_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ pub extern "C" fn perry_ui_app_set_activation_policy(app_handle: i64, value_ptr:
app::app_set_activation_policy(app_handle, value_ptr as *const u8);
}

/// Issue #1280 — initial window state. value_ptr = StringHeader pointer
/// ("normal" | "maximized" | "fullscreen"). Applied just before present().
#[no_mangle]
pub extern "C" fn perry_ui_app_set_window_state(app_handle: i64, value_ptr: i64) {
app::app_set_window_state(app_handle, value_ptr as *const u8);
}

/// Set minimum window size.
#[no_mangle]
pub extern "C" fn perry_ui_app_set_min_size(app_handle: i64, w: f64, h: f64) {
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-ui-ios/src/ffi/dialogs_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ pub extern "C" fn perry_ui_app_set_vibrancy(_app_handle: i64, _value_ptr: i64) {
#[no_mangle]
pub extern "C" fn perry_ui_app_set_activation_policy(_app_handle: i64, _value_ptr: i64) {}

/// Issue #1280 — windowState is a desktop concept; iOS apps always fill the
/// screen. Stub keeps the linker happy.
#[no_mangle]
pub extern "C" fn perry_ui_app_set_window_state(_app_handle: i64, _value_ptr: i64) {}

// =============================================================================
// Toolbar
// =============================================================================
Expand Down
67 changes: 67 additions & 0 deletions crates/perry-ui-macos/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ pub(crate) struct WindowEntry {
pub(crate) struct AppEntry {
pub(crate) window: Retained<NSWindow>,
pub(crate) _root_widget: Option<i64>,
/// Issue #1280 — initial window state applied on `app_run`. `zoom:` /
/// `toggleFullScreen:` need the window to be key+ordered front first.
pub(crate) window_state: Option<WindowState>,
}

/// Issue #1280 — initial window state for the main app window.
#[derive(Copy, Clone)]
pub(crate) enum WindowState {
Maximized,
Fullscreen,
}

/// Extract a &str from a *const StringHeader pointer.
Expand Down Expand Up @@ -114,6 +124,7 @@ pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 {
apps.push(AppEntry {
window,
_root_widget: None,
window_state: None,
});
apps.len() as i64 // 1-based handle
})
Expand Down Expand Up @@ -473,6 +484,41 @@ pub fn app_run(_app_handle: i64) {
}

entry.window.makeKeyAndOrderFront(None);

// Issue #1280 — apply initial window state. zoom: is the AppKit
// "green button" maximize (respects dock + menu bar). The
// toggleFullScreen: path enters native fullscreen on its own
// Space. Both need the window to be key+ordered front, which is
// why this runs here rather than in the setter.
if let Some(state) = entry.window_state {
unsafe {
match state {
WindowState::Maximized => {
let _: () =
msg_send![&*entry.window, zoom: std::ptr::null::<AnyObject>()];
}
WindowState::Fullscreen => {
let style_mask: usize = msg_send![&*entry.window, styleMask];
// Native fullscreen needs Resizable in the style
// mask; transient borderless / non-resizable
// windows can't enter it cleanly.
let resizable = NSWindowStyleMask::Resizable.0 as usize;
if style_mask & resizable != 0 {
let _: () = msg_send![
&*entry.window,
toggleFullScreen: std::ptr::null::<AnyObject>()
];
} else {
// Fallback: zoom() to maximize within the screen.
let _: () = msg_send![
&*entry.window,
zoom: std::ptr::null::<AnyObject>()
];
}
}
}
}
}
}
});

Expand Down Expand Up @@ -640,6 +686,27 @@ pub fn set_max_size(app_handle: i64, w: f64, h: f64) {
});
}

/// Issue #1280 — record the requested initial window state. Applied in
/// `app_run` after the window is key+ordered front (zoom: / toggleFullScreen:
/// don't take effect on a window that hasn't been shown yet).
/// `value_ptr` is a StringHeader pointer to one of
/// "normal" | "maximized" | "fullscreen". Anything else is silently ignored.
pub fn set_window_state(app_handle: i64, value_ptr: *const u8) {
let state_str = str_from_header(value_ptr);
let state = match state_str {
"maximized" => Some(WindowState::Maximized),
"fullscreen" => Some(WindowState::Fullscreen),
_ => None,
};
APPS.with(|a| {
let mut apps = a.borrow_mut();
let idx = (app_handle - 1) as usize;
if idx < apps.len() {
apps[idx].window_state = state;
}
});
}

/// Resize the main app window dynamically.
pub fn app_set_size(app_handle: i64, width: f64, height: f64) {
APPS.with(|a| {
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-ui-macos/src/lib_ffi/core_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ pub extern "C" fn perry_ui_app_set_activation_policy(app_handle: i64, value_ptr:
app::app_set_activation_policy(app_handle, value_ptr as *const u8);
}

/// Issue #1280 — initial window state. value_ptr = StringHeader pointer to
/// one of "normal" | "maximized" | "fullscreen". Applied on app_run().
#[no_mangle]
pub extern "C" fn perry_ui_app_set_window_state(app_handle: i64, value_ptr: i64) {
app::set_window_state(app_handle, value_ptr as *const u8);
}

/// Poll for pending file-open requests (from macOS Open With or argv).
/// Returns a StringHeader pointer (empty string if none pending).
#[no_mangle]
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-ui-tvos/src/ffi/app_keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub extern "C" fn perry_ui_app_set_vibrancy(_app_handle: i64, _value_ptr: i64) {
#[no_mangle]
pub extern "C" fn perry_ui_app_set_activation_policy(_app_handle: i64, _value_ptr: i64) {}

/// Issue #1280 — windowState is a desktop concept; tvOS apps always fill the
/// screen. Stub keeps the linker happy.
#[no_mangle]
pub extern "C" fn perry_ui_app_set_window_state(_app_handle: i64, _value_ptr: i64) {}

// =============================================================================
// Toolbar
// =============================================================================
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-ui-visionos/src/ffi_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,11 @@ pub extern "C" fn perry_ui_app_set_vibrancy(_app_handle: i64, _value_ptr: i64) {
#[no_mangle]
pub extern "C" fn perry_ui_app_set_activation_policy(_app_handle: i64, _value_ptr: i64) {}

/// Issue #1280 — windowState is a desktop concept; visionOS volumes/windows
/// have a different model. Stub keeps the linker happy.
#[no_mangle]
pub extern "C" fn perry_ui_app_set_window_state(_app_handle: i64, _value_ptr: i64) {}

// =============================================================================
// Toolbar
// =============================================================================
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-ui-watchos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,9 @@ pub extern "C" fn perry_ui_app_set_transparent(_app: i64, _val: f64) {}
pub extern "C" fn perry_ui_app_set_vibrancy(_app: i64, _ptr: i64) {}
#[no_mangle]
pub extern "C" fn perry_ui_app_set_activation_policy(_app: i64, _ptr: i64) {}
/// Issue #1280 — watchOS apps don't have user-resizable windows. Stub.
#[no_mangle]
pub extern "C" fn perry_ui_app_set_window_state(_app: i64, _ptr: i64) {}
#[no_mangle]
pub extern "C" fn perry_ui_toolbar_create() -> i64 {
0
Expand Down
75 changes: 73 additions & 2 deletions crates/perry-ui-windows/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ pub(crate) struct AppEntry {
root_widget: Option<i64>,
min_size: Option<(f64, f64)>,
max_size: Option<(f64, f64)>,
/// Issue #1280 — initial window state requested by App({ windowState }).
/// Applied at app_run time; None / "normal" => SW_SHOW.
window_state: Option<WindowState>,
}

/// Issue #1280 — initial window state for the main app window.
#[derive(Copy, Clone)]
pub(crate) enum WindowState {
Maximized,
Fullscreen,
}

struct PendingShortcut {
Expand Down Expand Up @@ -217,6 +227,7 @@ pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 {
root_widget: None,
min_size: None,
max_size: None,
window_state: None,
});
apps.len() as i64
})
Expand All @@ -233,6 +244,7 @@ pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 {
root_widget: None,
min_size: None,
max_size: None,
window_state: None,
});
apps.len() as i64
})
Expand Down Expand Up @@ -290,9 +302,46 @@ pub fn app_run(app_handle: i64) {
let apps = apps.borrow();
let idx = (app_handle - 1) as usize;
if idx < apps.len() {
let hwnd = apps[idx].hwnd;
let state = apps[idx].window_state;
unsafe {
let _ = ShowWindow(apps[idx].hwnd, SW_SHOW);
let _ = UpdateWindow(apps[idx].hwnd);
// Issue #1280 — apply requested initial window state.
// Fullscreen on Win32 = drop WS_OVERLAPPEDWINDOW frame and
// resize to the monitor's full rect (not just the work
// area, which excludes the taskbar). Maximized = standard
// SW_SHOWMAXIMIZED which respects the taskbar.
match state {
Some(WindowState::Fullscreen) => {
let style = GetWindowLongW(hwnd, GWL_STYLE) as u32;
let new_style = style & !WS_OVERLAPPEDWINDOW.0;
SetWindowLongW(hwnd, GWL_STYLE, new_style as i32);
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
let mut mi = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
if GetMonitorInfoW(monitor, &mut mi).as_bool() {
let r = mi.rcMonitor;
let _ = SetWindowPos(
hwnd,
HWND_TOP,
r.left,
r.top,
r.right - r.left,
r.bottom - r.top,
SWP_NOOWNERZORDER | SWP_FRAMECHANGED,
);
}
let _ = ShowWindow(hwnd, SW_SHOW);
}
Some(WindowState::Maximized) => {
let _ = ShowWindow(hwnd, SW_SHOWMAXIMIZED);
}
None => {
let _ = ShowWindow(hwnd, SW_SHOW);
}
}
let _ = UpdateWindow(hwnd);
}
}
});
Expand Down Expand Up @@ -560,6 +609,28 @@ pub fn set_max_size(app_handle: i64, w: f64, h: f64) {
});
}

/// Issue #1280 — record the requested initial window state. The state is
/// applied in `app_run` (Win32 needs the window to exist + be ready before
/// `ShowWindow(SW_SHOWMAXIMIZED)` or the fullscreen frame swap takes effect).
/// `value_ptr` is a perry-runtime StringHeader pointer to one of
/// "normal" | "maximized" | "fullscreen". Anything else is silently ignored.
pub fn set_window_state(app_handle: i64, value_ptr: *const u8) {
let state_str = str_from_header(value_ptr);
let state = match state_str {
"maximized" => Some(WindowState::Maximized),
"fullscreen" => Some(WindowState::Fullscreen),
// "normal" or anything else => default behavior (SW_SHOW).
_ => None,
};
APPS.with(|apps| {
let mut apps = apps.borrow_mut();
let idx = (app_handle - 1) as usize;
if idx < apps.len() {
apps[idx].window_state = state;
}
});
}

/// Resize the main app window dynamically.
pub fn app_set_size(app_handle: i64, width: f64, height: f64) {
#[cfg(target_os = "windows")]
Expand Down
Loading