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
3 changes: 3 additions & 0 deletions changelog.d/5694-state-value-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed native `State.value` reads returning `undefined`, which made TextField and
SecureField callbacks appear to receive empty values even though their native
change handlers delivered the correct string.
1 change: 1 addition & 0 deletions changelog.d/5731-embed-project-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed `--embed` and configured embed patterns resolving relative to an entry file's directory instead of the package/project root.
5 changes: 5 additions & 0 deletions changelog.d/5742-android-x86-64.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed `perry compile --target android-x86_64` to use the x86_64 Android
runtime, NDK linker/JNI target, UI library, native-addon architecture, metadata,
lockfile, output-path, and post-link paths consistently instead of falling back
to host or arm64 behavior. Android cross-compilation also now uses libc's
platform-native `ioctl` request type when building PTY support.
4 changes: 4 additions & 0 deletions changelog.d/5884-windows-bloomview-dpi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed Windows DPI awareness being initialized after the hidden widget parking
window was created. Top-level windows and BloomView surfaces now agree on
physical sizing above 100% display scaling, and BloomView dimensions consistently
use logical 96-DPI pixels.
12 changes: 12 additions & 0 deletions crates/perry-hir/src/lower/expr_member/native_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub(crate) fn is_native_dispatch_member(module: &str, class: &str, prop: &str) -
// Data getters resolved by FFI.
"blob" => is_blob_getter_name(prop),
"fetch" => is_fetch_response_getter_name(prop),
// `State` is an opaque perry/ui handle, so `.value` must invoke the
// platform getter instead of falling through to an own-property read
// on the NaN-boxed handle. Other perry/ui members are methods and reach
// dispatch through the call-expression path.
"perry/ui" => class == "State" && prop == "value",
// Web Streams: only the getter list reaches dispatch (methods are
// PropertyGet bound-method reads).
"readable_stream"
Expand Down Expand Up @@ -588,4 +593,11 @@ mod tests {
"custom"
));
}

#[test]
fn perry_ui_state_dispatches_value_getter_only() {
assert!(is_native_dispatch_member("perry/ui", "State", "value"));
assert!(!is_native_dispatch_member("perry/ui", "State", "custom"));
assert!(!is_native_dispatch_member("perry/ui", "Canvas", "value"));
}
}
47 changes: 47 additions & 0 deletions crates/perry-hir/src/lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,53 @@ fn test_plain_class_to_class_heritage_keeps_static_extends_name() {
);
}

/// #5694: `State` is a native `perry/ui` handle, so reading `.value` must
/// remain a zero-argument native getter call after local-native rewriting.
#[test]
fn test_perry_ui_state_value_uses_native_getter() {
use crate::ir::{clear_current_module_source, Expr, Stmt};
use crate::js_transform::fix_local_native_instances;

let source = r#"
import { State } from "perry/ui";

function main() {
const text = State("");
return text.value;
}
"#;
let module =
perry_parser::parse_typescript(source, "state_value.ts").expect("source should parse");
let mut hir =
super::lower_module(&module, "test", "state_value.ts").expect("source should lower");
clear_current_module_source();
fix_local_native_instances(&mut hir);

let main = hir
.functions
.iter()
.find(|function| function.name == "main")
.expect("main function");
let value = main.body.iter().find_map(|stmt| match stmt {
Stmt::Return(Some(expr)) => Some(expr),
_ => None,
});

assert!(
matches!(
value,
Some(Expr::NativeMethodCall {
module,
class_name: None,
object: Some(_),
method,
args,
}) if module == "perry/ui" && method == "value" && args.is_empty()
),
"State.value must lower through perry_ui_state_get, got: {value:#?}"
);
}

/// #6679: a NAMED class EXPRESSION's `.name` is its own explicit name
/// (`Named` in `const B = class Named {}`), not the outer binding name. Per
/// spec a named class expression is not an anonymous function definition, so
Expand Down
9 changes: 6 additions & 3 deletions crates/perry-runtime/src/pty/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ pub(crate) fn spawn_in_pty(req: &PtySpawnRequest) -> io::Result<PtyChild> {
// Child. Async-signal-safe calls ONLY from here to execve.
unsafe {
libc::setsid();
libc::ioctl(slave, libc::TIOCSCTTY as libc::c_ulong, 0);
// Infer libc's platform-specific `Ioctl` type. Android x86_64
// uses `c_int` while BSD/macOS uses `c_ulong`; forcing either
// concrete type makes the other platform fail to compile.
libc::ioctl(slave, libc::TIOCSCTTY as _, 0);
libc::dup2(slave, 0);
libc::dup2(slave, 1);
libc::dup2(slave, 2);
Expand Down Expand Up @@ -255,7 +258,7 @@ pub(crate) fn wait_child(pid: i32) -> (Option<i32>, Option<i32>) {
/// foreground process group.
pub(crate) fn resize_pty(master: RawFd, cols: u16, rows: u16) -> bool {
let ws = winsize(cols, rows);
unsafe { libc::ioctl(master, libc::TIOCSWINSZ as libc::c_ulong, &ws) == 0 }
unsafe { libc::ioctl(master, libc::TIOCSWINSZ as _, &ws) == 0 }
}

/// `kill(2)` — deliver `signo` to `pid`.
Expand All @@ -267,7 +270,7 @@ pub(crate) fn signal_pid(pid: i32, signo: i32) -> bool {
#[cfg(test)]
pub(crate) fn read_winsize(fd: RawFd) -> Option<(u16, u16)> {
let mut ws = winsize(0, 0);
let rc = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ as libc::c_ulong, &mut ws) };
let rc = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ as _, &mut ws) };
if rc == 0 {
Some((ws.ws_col, ws.ws_row))
} else {
Expand Down
74 changes: 61 additions & 13 deletions crates/perry-ui-windows/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ const TEST_EXIT_TIMER_ID: usize = 9997;
/// Global DPI scale factor (1.0 at 96 DPI, 1.5 at 144 DPI, 2.0 at 192 DPI).
static DPI_SCALE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

/// Process DPI awareness must be selected before the first HWND is created.
/// Widget constructors run while `App({ body: ... })` arguments are evaluated,
/// so the hidden parking HWND can predate `app_create` (#5884).
#[cfg(target_os = "windows")]
static DPI_INIT: std::sync::Once = std::sync::Once::new();

/// Get the system DPI for the primary monitor.
///
/// Routes through `crate::dpi_compat::get_system_dpi_compat`, which uses
Expand All @@ -66,8 +72,15 @@ fn set_dpi_scale(scale: f64) {
DPI_SCALE.store(scale.to_bits(), std::sync::atomic::Ordering::Relaxed);
}

/// Get the DPI scale factor. Returns 1.0 if not set.
fn dpi_scale_from_dpi(dpi: u32) -> f64 {
dpi.max(96) as f64 / 96.0
}

/// Get the DPI scale factor, initializing process awareness on first use.
pub fn get_dpi_scale() -> f64 {
#[cfg(target_os = "windows")]
ensure_dpi_initialized();

let bits = DPI_SCALE.load(std::sync::atomic::Ordering::Relaxed);
if bits == 0 {
1.0
Expand All @@ -76,6 +89,26 @@ pub fn get_dpi_scale() -> f64 {
}
}

/// Convert a logical 96-DPI dimension to physical pixels using the active
/// process/monitor scale.
pub(crate) fn scale_logical_px(px: f64) -> i32 {
scale_logical_px_by(px, get_dpi_scale())
}

fn scale_logical_px_by(px: f64, scale: f64) -> i32 {
(px * scale.max(1.0)).round() as i32
}

/// Select process DPI awareness and cache the initial scale before any window
/// (including the hidden widget parking window) is created.
#[cfg(target_os = "windows")]
pub(crate) fn ensure_dpi_initialized() {
DPI_INIT.call_once(|| {
crate::dpi_compat::set_process_dpi_awareness_compat();
set_dpi_scale(dpi_scale_from_dpi(get_system_dpi()));
});
}

thread_local! {
static TIMER_TICK_NEEDED: std::cell::Cell<bool> = std::cell::Cell::new(false);
}
Expand Down Expand Up @@ -210,22 +243,15 @@ fn to_wide(s: &str) -> Vec<u16> {
/// Create an app window. Returns app handle (1-based).
pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 {
let title = str_from_header(title_ptr);
let w = if width > 0.0 { width as i32 } else { 800 };
let h = if height > 0.0 { height as i32 } else { 600 };
let w = if width > 0.0 { width } else { 800.0 };
let h = if height > 0.0 { height } else { 600.0 };

#[cfg(target_os = "windows")]
{
unsafe {
// Enable DPI awareness — Win10 1607+ best, Win8.1 ok, Win7 silent
// fallback to system DPI. See dpi_compat.rs (issue #303).
crate::dpi_compat::set_process_dpi_awareness_compat();

// Scale window size by system DPI (96 = 100%, 144 = 150%, 192 = 200%)
let dpi = get_system_dpi();
let scale = dpi as f64 / 96.0;
let w = (w as f64 * scale) as i32;
let h = (h as f64 * scale) as i32;
set_dpi_scale(scale);
ensure_dpi_initialized();
let w = scale_logical_px(w);
let h = scale_logical_px(h);

// Initialize common controls
let icc = INITCOMMONCONTROLSEX {
Expand Down Expand Up @@ -1420,6 +1446,28 @@ unsafe extern "system" fn wnd_proc(
}
}

#[cfg(test)]
mod dpi_tests {
use super::{dpi_scale_from_dpi, scale_logical_px_by};

#[test]
fn converts_logical_window_sizes_at_common_scales() {
assert_eq!(dpi_scale_from_dpi(96), 1.0);
assert_eq!(dpi_scale_from_dpi(144), 1.5);
assert_eq!(dpi_scale_from_dpi(192), 2.0);
assert_eq!(scale_logical_px_by(800.0, 1.5), 1200);
assert_eq!(scale_logical_px_by(600.0, 1.5), 900);
assert_eq!(scale_logical_px_by(100.9, 1.5), 151);
}

#[test]
fn invalid_or_sub_96_dpi_values_do_not_shrink_ui() {
assert_eq!(dpi_scale_from_dpi(0), 1.0);
assert_eq!(dpi_scale_from_dpi(72), 1.0);
assert_eq!(scale_logical_px_by(100.0, 0.0), 100);
}
}

/// Register a system-wide global hotkey. Uses Win32 `RegisterHotKey` API.
/// `key_ptr` is a StringHeader pointer to the key (e.g., "s").
/// `modifiers` is a bitfield: 1=Cmd(->Ctrl), 2=Shift, 4=Option(->Alt), 8=Control(->Ctrl).
Expand Down
16 changes: 11 additions & 5 deletions crates/perry-ui-windows/src/widgets/bloomview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ pub fn create(width: f64, height: f64) -> i64 {
#[cfg(target_os = "windows")]
{
ensure_class_registered();
let parking_hwnd = super::get_parking_hwnd();
// Constructor dimensions are logical (96-DPI) pixels, matching App and
// widgetSetFixedWidth/Height. Creating the parking window first also
// selects process DPI awareness before either HWND exists.
let width_px = crate::app::scale_logical_px(width);
let height_px = crate::app::scale_logical_px(height);
let class_name = to_wide("PerryBloomView");
let window_text = to_wide("");
unsafe {
Expand All @@ -102,9 +108,9 @@ pub fn create(width: f64, height: f64) -> i64 {
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
0,
0,
width as i32,
height as i32,
Some(super::get_parking_hwnd()),
width_px,
height_px,
Some(parking_hwnd),
None,
Some(HINSTANCE::from(hinstance)),
None,
Expand All @@ -116,8 +122,8 @@ pub fn create(width: f64, height: f64) -> i64 {
let handle =
register_widget_with_layout(hwnd, WidgetKind::Image, 0.0, (0.0, 0.0, 0.0, 0.0));
// Reserve the requested size so the view is visible in a layout.
set_fixed_width(handle, width as i32);
set_fixed_height(handle, height as i32);
set_fixed_width(handle, width_px);
set_fixed_height(handle, height_px);
handle
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-ui-windows/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ pub fn get_parking_hwnd() -> HWND {
fn to_wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
// `App({ body: Widget() })` constructs the body before app_create runs.
// Select DPI awareness before this first hidden HWND locks the process
// into DPI-unaware virtualization (#5884).
crate::app::ensure_dpi_initialized();
PARKING_HWND.with(|cell| {
let mut opt = cell.borrow_mut();
if let Some(hwnd) = *opt {
Expand Down
2 changes: 2 additions & 0 deletions crates/perry/src/commands/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::OutputFormat;
// `compile/` directory. The `compile.rs` orchestrator stays as the
// public API surface; helpers move to focused modules so unrelated
// changes don't churn this file.
pub(crate) mod android_target;
mod app_metadata;
mod apple_codesign;
mod apple_info_plist;
Expand Down Expand Up @@ -50,6 +51,7 @@ mod strip_dedup;
mod targets;
pub mod well_known;
pub(crate) mod widget_build;
use android_target::{android_target, is_android_target};
use app_metadata::rust_target_triple;
// apple_info_plist helpers used through bundle_ios (no direct uses in
// compile.rs anymore now that the iOS bundle code moved out).
Expand Down
63 changes: 63 additions & 0 deletions crates/perry/src/commands/compile/android_target.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Canonical Android target properties used by the compile pipeline.
//!
//! Keep architecture-sensitive values together: adding a new Android target
//! must not let codegen, Cargo, clang, native manifests, and APK ABI placement
//! silently disagree.

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct AndroidTarget {
pub rust_triple: &'static str,
pub clang_target: &'static str,
pub manifest_arch: &'static str,
}

const ARM64: AndroidTarget = AndroidTarget {
rust_triple: "aarch64-linux-android",
clang_target: "aarch64-linux-android24",
manifest_arch: "arm64",
};

const X86_64: AndroidTarget = AndroidTarget {
rust_triple: "x86_64-linux-android",
clang_target: "x86_64-linux-android24",
manifest_arch: "x64",
};

pub(crate) fn android_target(target: Option<&str>) -> Option<AndroidTarget> {
match target {
// Wear OS currently uses the same arm64 NDK build as Android devices.
Some("android") | Some("wearos") => Some(ARM64),
Some("android-x86_64") => Some(X86_64),
_ => None,
}
}

pub(crate) fn is_android_target(target: Option<&str>) -> bool {
android_target(target).is_some()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn maps_every_android_architecture_consistently() {
let arm64 = android_target(Some("android")).unwrap();
assert_eq!(arm64.rust_triple, "aarch64-linux-android");
assert_eq!(arm64.clang_target, "aarch64-linux-android24");
assert_eq!(arm64.manifest_arch, "arm64");

let x86_64 = android_target(Some("android-x86_64")).unwrap();
assert_eq!(x86_64.rust_triple, "x86_64-linux-android");
assert_eq!(x86_64.clang_target, "x86_64-linux-android24");
assert_eq!(x86_64.manifest_arch, "x64");

assert_eq!(android_target(Some("wearos")), Some(arm64));
}

#[test]
fn does_not_treat_codegen_only_widget_target_as_native_android() {
assert!(!is_android_target(Some("android-widget")));
assert!(!is_android_target(None));
}
}
Loading
Loading