diff --git a/README.md b/README.md index 8c0b29f47..ce322c8d4 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ - Discord: https://discord.gg/adqBRq7Ece - Rik Arends: https://twitter.com/rikarends -- Eddy Bruel: - -- Sebastian Michailidis: https://bsky.app/profile/okpokpokp.bsky.social -Makepad is an AI-accelerated application development environment for Rust. It combines a high-performance UI runtime, a live-editable design language, and a fast iteration loop so you can build native and web apps with a tight feedback cycle. +Makepad is an AI-accelerated application and game development environment for Rust. It combines a high-performance UI runtime, a live-editable design language, and a fast iteration loop so you can build native and web apps with a tight feedback cycle. + +It also has a large set of AI backends integrated for embedding llms or generative AI models inside applications or run them easily on local hardware This repository contains the core engine, widgets, tools, and examples. @@ -17,6 +17,7 @@ This repository contains the core engine, widgets, tools, and examples. - A Rust-first framework with a scriptable UI DSL. - A studio app for running, inspecting, and iterating on examples and projects. - An AI-accelerated workflow: structure and tooling aimed at making code generation, refactoring, and iteration faster and safer. +- Simple forward 3D renderer for making games on Quest and all other supported platforms ## Features diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 1e63fa5d5..449f5b598 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -6,15 +6,8 @@ app_main!(App); script_mod! { use mod.prelude.widgets.* - let state = { - counter: 0 - } - mod.state = state startup() do #(App::script_component(vm)){ ui: Root{ - on_startup:||{ // right now render isnt called automatically yet - ui.main_view.render() - } main_window := Window{ window.inner_size: vec2(420, 220) body +: { @@ -24,11 +17,9 @@ script_mod! { flow: Down spacing: 12 align: Center - on_render: ||{ - counter_label := Label{ - text: "Count: " + state.counter - draw_text.text_style.font_size: 24 - } + counter_label := Label{ + text: "Count: 0" + draw_text.text_style.font_size: 24 } } increment_button := Button{ @@ -44,15 +35,15 @@ script_mod! { pub struct App { #[live] ui: WidgetRef, + #[rust] + counter: i32, } impl MatchEvent for App { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { if self.ui.button(cx, ids!(increment_button)).clicked(actions) { - script_eval!(cx,{ - mod.state.counter += 1 - ui.main_view.render() - }); + self.counter += 1; + self.ui.label(cx, ids!(counter_label)).set_text(cx, &format!("Count: {}", self.counter)); } } } diff --git a/libs/makepad_test/src/runtime.rs b/libs/makepad_test/src/runtime.rs index b49f224f2..0d591832d 100644 --- a/libs/makepad_test/src/runtime.rs +++ b/libs/makepad_test/src/runtime.rs @@ -85,6 +85,11 @@ pub struct TestConfig { pub startup_pause: Duration, pub action_delay: Duration, pub keep_open: Duration, + pub android: bool, + pub device_serial: Option, + pub adb_path: Option, + pub android_port: u16, + pub android_native_activity: bool, } impl TestConfig { @@ -126,6 +131,11 @@ impl TestConfig { startup_pause: env_duration_ms("MAKEPAD_TEST_STARTUP_DELAY_MS"), action_delay: env_duration_ms("MAKEPAD_TEST_ACTION_DELAY_MS"), keep_open: env_duration_ms("MAKEPAD_TEST_KEEP_OPEN_MS"), + android: android_test_enabled(), + device_serial: android_device_serial(), + adb_path: android_adb_path(), + android_port: android_hub_port(), + android_native_activity: env_truthy("MAKEPAD_TEST_NATIVE_ACTIVITY"), }) } @@ -157,6 +167,13 @@ impl TestConnection { Self::Remote(connection) => connection.recv_timeout(timeout), } } + + fn studio_addr(&self) -> Option { + match self { + Self::InProcess(connection) => connection.studio_addr(), + Self::Remote(_) => None, + } + } } struct TestAppInner { @@ -208,7 +225,9 @@ impl TestApp { } fn start_once(config: TestConfig) -> TestResult { - let (connection, build_id) = if visible_mode_enabled() { + let (connection, build_id) = if config.android { + start_android_app(&config)? + } else if visible_mode_enabled() { start_visible_app(&config)? } else { start_headless_app(&config)? @@ -529,7 +548,7 @@ impl TestApp { } fn try_pump_ui(&self) -> TestResult<()> { - self.try_forward((0..PUMP_TICKS).map(|_| StudioToApp::Tick).collect()) + self.try_forward((0..pump_ticks()).map(|_| StudioToApp::Tick).collect()) } fn wait_for_reply(&self, timeout: Duration, mut matcher: F) -> TestResult @@ -623,6 +642,10 @@ impl TestApp { if inner.build_stopped.is_some() { return; } + if inner.config.android { + let full_package = android_full_package_name(&inner.config.package_name); + let _ = adb_force_stop(&inner.config, &full_package); + } let build_id = inner.build_id; let _ = inner.connection.send(ClientToHub::ClearBuild { build_id }); } @@ -1090,10 +1113,15 @@ where F: FnOnce(TestApp) -> R, R: IntoTestResult, { - let test_lock = TEST_MUTEX.get_or_init(|| Mutex::new(())); - let _guard = test_lock - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + // Tests are serialised by default: each one drives a whole app process, so + // running several at once oversubscribes the machine and makes timing- + // sensitive assertions flaky. A suite whose app is cheap enough can opt out. + let _guard = (!parallel_tests_enabled()).then(|| { + TEST_MUTEX + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + }); let app = TestApp::start(config)?; let result = catch_unwind(AssertUnwindSafe(|| test(app.clone()).into_test_result())); @@ -1455,6 +1483,20 @@ fn snapshot_summary(widget: &WidgetSnapshot) -> String { fields.join(" ") } +/// Ticks forwarded before each query. Every one of them costs the app a full +/// rendered frame when anything is dirty, so this is the multiplier on the cost +/// of a snapshot — worth lowering for a suite whose app renders slowly. +fn pump_ticks() -> usize { + std::env::var("MAKEPAD_TEST_PUMP_TICKS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(PUMP_TICKS) +} + +fn parallel_tests_enabled() -> bool { + env_truthy("MAKEPAD_TEST_PARALLEL") +} + fn visible_mode_enabled() -> bool { env_truthy("MAKEPAD_TEST_VISIBLE") } @@ -1492,6 +1534,348 @@ fn env_duration_ms(name: &str) -> Duration { .unwrap_or(Duration::ZERO) } +// --------------------------------------------------------------------------- +// Android test helpers +// --------------------------------------------------------------------------- + +const DEFAULT_ANDROID_PORT: u16 = 8001; +const ANDROID_STARTUP_TIMEOUT: Duration = Duration::from_secs(120); +const ANDROID_LAUNCH_TIMEOUT: Duration = Duration::from_secs(60); + +fn android_test_enabled() -> bool { + env_truthy("MAKEPAD_TEST_ANDROID") +} + +fn android_device_serial() -> Option { + std::env::var("MAKEPAD_TEST_DEVICE") + .ok() + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) +} + +fn android_hub_port() -> u16 { + std::env::var("MAKEPAD_TEST_ANDROID_PORT") + .ok() + .and_then(|v| v.trim().parse().ok()) + .unwrap_or(DEFAULT_ANDROID_PORT) +} + +fn android_adb_path() -> Option { + std::env::var("MAKEPAD_TEST_ADB") + .ok() + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) +} + +fn adb_command(config: &TestConfig) -> std::process::Command { + let adb = config + .adb_path + .as_deref() + .unwrap_or("adb"); + let mut cmd = std::process::Command::new(adb); + if let Some(ref serial) = config.device_serial { + cmd.arg("-s").arg(serial); + } + cmd +} + +fn adb_exec(config: &TestConfig, args: &[&str]) -> TestResult { + let output = adb_command(config) + .args(args) + .output() + .map_err(|err| TestError::new(format!("failed to run adb: {err}")))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(TestError::new(format!( + "adb {} failed: {}", + args.join(" "), + stderr.trim() + ))); + } + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +fn android_full_package_name(package_name: &str) -> String { + let underscore = package_name.replace('-', "_"); + format!("dev.makepad.{underscore}") +} + +fn adb_forward(config: &TestConfig, port: u16) -> TestResult<()> { + let _ = adb_exec(config, &["reverse", "--remove", &format!("tcp:{port}")]); + adb_exec( + config, + &["reverse", &format!("tcp:{port}"), &format!("tcp:{port}")], + )?; + Ok(()) +} + +fn adb_install(config: &TestConfig, apk_path: &std::path::Path) -> TestResult<()> { + let output = adb_command(config) + .args(["install", "-r"]) + .arg(apk_path) + .output() + .map_err(|err| TestError::new(format!("failed to run adb install: {err}")))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(TestError::new(format!("adb install failed: {}", stderr.trim()))); + } + Ok(()) +} + +fn adb_launch( + config: &TestConfig, + package: &str, + build_id: u64, + crate_name: &str, + port: u16, +) -> TestResult<()> { + // Native-activity builds launch `android.app.NativeActivity` directly (no + // Java Activity). Legacy Java builds launch the generated `MakepadApp` + // subclass that bridges into `MakepadNative.activityOnCreate`. + let activity = if config.android_native_activity { + format!("{package}/android.app.NativeActivity") + } else { + format!("{package}/.MakepadApp") + }; + let studio_host = format!("127.0.0.1:{port}"); + let output = adb_command(config) + .args([ + "shell", "am", "start", + "-n", &activity, + "-e", "makepad.STUDIO_HOST", &studio_host, + "-e", "makepad.STUDIO_BUILD", &build_id.to_string(), + "-e", "makepad.STUDIO_CRATE", crate_name, + ]) + .output() + .map_err(|err| TestError::new(format!("failed to run adb shell am start: {err}")))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(TestError::new(format!("adb launch failed: {}", stderr.trim()))); + } + Ok(()) +} + +fn adb_force_stop(config: &TestConfig, package: &str) -> TestResult<()> { + let _ = adb_exec(config, &["shell", "am", "force-stop", package]); + Ok(()) +} + +fn resolve_workspace_root(manifest_dir: &std::path::Path) -> std::path::PathBuf { + if let Ok(env_root) = std::env::var("MAKEPAD_WORKSPACE_ROOT") { + let root = std::path::PathBuf::from(env_root); + if root.join("Cargo.toml").exists() { + return root; + } + } + let mut dir = manifest_dir.to_path_buf(); + loop { + let cargo_toml = dir.join("Cargo.toml"); + if cargo_toml.exists() { + if let Ok(content) = std::fs::read_to_string(&cargo_toml) { + if content.contains("tools/cargo_makepad") { + // skip Cargo.toml files that reference the tool as a path + } + let has_workspace = content.contains("[workspace]") + || content.contains("workspace.members") + || content.contains("workspace.package"); + if has_workspace && dir.join("tools/cargo_makepad").exists() { + return dir; + } + } + } + if !dir.pop() { + break; + } + } + manifest_dir.to_path_buf() +} + +fn build_android_apk(config: &TestConfig) -> TestResult { + let workspace_root = resolve_workspace_root(&config.manifest_dir); + let cargo_makepad = workspace_root + .join("target") + .join("release") + .join("cargo-makepad"); + if !cargo_makepad.exists() { + return Err(TestError::new(format!( + "cargo-makepad not found at {}. Run: cargo build --release -p cargo-makepad", + cargo_makepad.display() + ))); + } + let mut args = vec!["android"]; + if config.android_native_activity { + args.push("--native-activity"); + } + args.extend_from_slice(&["build", "-p", &config.package_name]); + let output = std::process::Command::new(&cargo_makepad) + .args(&args) + .current_dir(&workspace_root) + .output() + .map_err(|err| TestError::new(format!("failed to run cargo makepad: {err}")))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(TestError::new(format!( + "android build failed:\nstdout: {}\nstderr: {}", + stdout.trim(), + stderr.trim() + ))); + } + let apk_dir_name = config.package_name.replace('-', "_"); + let apk_dir = workspace_root + .join("target") + .join("android") + .join("makepad-android-apk") + .join(&apk_dir_name) + .join("apk"); + let apk_name = format!("{}.apk", &apk_dir_name); + let apk_path = apk_dir.join(&apk_name); + if !apk_path.exists() { + return Err(TestError::new(format!( + "APK not found at {}", + apk_path.display() + ))); + } + Ok(apk_path) +} + +fn wait_for_android_app_started( + connection: &TestConnection, + build_id: QueryId, + timeout: Duration, +) -> TestResult<()> { + let deadline = Instant::now() + timeout; + loop { + if Instant::now() >= deadline { + return Err(TestError::new( + "timed out waiting for Android app to connect to hub", + )); + } + let slice = cmp::min( + POLL_INTERVAL, + deadline.saturating_duration_since(Instant::now()), + ); + let Some(msg) = connection.recv_timeout(slice) else { + continue; + }; + match msg { + HubToClient::AppStarted { + build_id: msg_build_id, + } if msg_build_id == build_id => { + return Ok(()); + } + HubToClient::Error { message } => return Err(TestError::new(message)), + _ => {} + } + } +} + +fn wait_for_android_app_responsive( + connection: &mut TestConnection, + build_id: QueryId, + timeout: Duration, +) -> TestResult<()> { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + let query_id = connection.send(ClientToHub::WidgetTreeDump { build_id })?; + let attempt_deadline = Instant::now() + ACTION_TIMEOUT; + let mut replied = false; + while Instant::now() < attempt_deadline { + if Instant::now() >= deadline { + break; + } + let slice = cmp::min( + POLL_INTERVAL, + attempt_deadline.saturating_duration_since(Instant::now()), + ); + let Some(msg) = connection.recv_timeout(slice) else { + continue; + }; + if let HubToClient::WidgetTreeDump { + query_id: id, dump: _, .. + } = &msg + { + if *id == query_id { + replied = true; + break; + } + } + } + if replied { + return Ok(()); + } + thread::sleep(POLL_INTERVAL); + } + Err(TestError::new( + "timed out waiting for Android app to become responsive", + )) +} + +fn start_android_app(config: &TestConfig) -> TestResult<(TestConnection, QueryId)> { + let hub_port = config.android_port; + let listen_address = SocketAddr::from((Ipv4Addr::LOCALHOST, hub_port)); + let mut connection = TestConnection::InProcess( + StudioHub::start_in_process(HubConfig { + listen_address, + mounts: vec![MountConfig { + name: config.mount_name.clone(), + path: config.manifest_dir.clone(), + }], + enable_in_process_gateway: true, + ..Default::default() + }) + .map_err(TestError::new)?, + ); + + let build_id = QueryId(1); + let full_package = android_full_package_name(&config.package_name); + // The hub may have bound a fallback port (e.g. when a real Studio already + // owns `android_port`). Route adb and the app at the port actually bound, + // never a hardcoded one, or the app would dial a dead listener. + let hub_port = connection + .studio_addr() + .and_then(|addr| addr.rsplit_once(':').map(|(_, p)| p.to_string())) + .and_then(|p| p.parse::().ok()) + .unwrap_or(config.android_port); + + eprintln!("[makepad-test] Android: forwarding ADB port {hub_port}"); + adb_forward(config, hub_port)?; + + eprintln!("[makepad-test] Android: building APK for {}", config.package_name); + let apk_path = build_android_apk(config)?; + + eprintln!("[makepad-test] Android: installing APK"); + adb_install(config, &apk_path)?; + + eprintln!("[makepad-test] Android: force-stopping previous instance"); + let _ = adb_force_stop(config, &full_package); + thread::sleep(Duration::from_secs(1)); + + eprintln!("[makepad-test] Android: launching app"); + adb_launch(config, &full_package, build_id.0, &config.package_name, hub_port)?; + + thread::sleep(Duration::from_secs(2)); + + eprintln!("[makepad-test] Android: waiting for app to connect to hub"); + wait_for_android_app_started(&connection, build_id, ANDROID_STARTUP_TIMEOUT)?; + + eprintln!("[makepad-test] Android: app connected"); + + // The websocket connects on a background thread before the app's event + // loop is up, so a cold start can answer the handshake well before it can + // service hub requests. Legacy Java starts are the slowest: the first + // frame (and with it the main loop that drains requests) only comes after + // the SurfaceView surface materializes, which on a first launch after + // install can exceed the per-request action timeout. Settle until the app + // actually answers a request so the test's first query does not lose that + // boot race. + eprintln!("[makepad-test] Android: waiting for app to become responsive"); + wait_for_android_app_responsive(&mut connection, build_id, ANDROID_STARTUP_TIMEOUT)?; + + eprintln!("[makepad-test] Android: app responsive"); + Ok((connection, build_id)) +} + fn now_seconds() -> f64 { SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/libs/windows/windows-rs/src/Windows/mod.rs b/libs/windows/windows-rs/src/Windows/mod.rs index 2adf4535f..af77f8dac 100644 --- a/libs/windows/windows-rs/src/Windows/mod.rs +++ b/libs/windows/windows-rs/src/Windows/mod.rs @@ -29649,17 +29649,30 @@ pub unsafe fn DwmSetWindowAttribute(hwnd: super::super::Foundation::HWND, dwattr windows_core::link!("dwmapi.dll" "system" fn DwmSetWindowAttribute(hwnd : super::super::Foundation:: HWND, dwattribute : u32, pvattribute : *const core::ffi::c_void, cbattribute : u32) -> windows_core::HRESULT); unsafe { DwmSetWindowAttribute(hwnd, dwattribute.0 as _, pvattribute, cbattribute).ok() } } +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DWMNCRENDERINGPOLICY(pub i32); +pub const DWMNCRP_ENABLED: DWMNCRENDERINGPOLICY = DWMNCRENDERINGPOLICY(2i32); pub const DWMSBT_MAINWINDOW: DWM_SYSTEMBACKDROP_TYPE = DWM_SYSTEMBACKDROP_TYPE(2i32); pub const DWMSBT_NONE: DWM_SYSTEMBACKDROP_TYPE = DWM_SYSTEMBACKDROP_TYPE(1i32); pub const DWMSBT_TABBEDWINDOW: DWM_SYSTEMBACKDROP_TYPE = DWM_SYSTEMBACKDROP_TYPE(4i32); pub const DWMSBT_TRANSIENTWINDOW: DWM_SYSTEMBACKDROP_TYPE = DWM_SYSTEMBACKDROP_TYPE(3i32); +pub const DWMWA_BORDER_COLOR: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(34i32); +pub const DWMWA_COLOR_NONE: u32 = 4294967294u32; +pub const DWMWA_NCRENDERING_POLICY: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(2i32); pub const DWMWA_SYSTEMBACKDROP_TYPE: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(38i32); +pub const DWMWA_WINDOW_CORNER_PREFERENCE: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(33i32); +pub const DWMWCP_ROUND: DWM_WINDOW_CORNER_PREFERENCE = DWM_WINDOW_CORNER_PREFERENCE(2i32); +pub const DWMWCP_ROUNDSMALL: DWM_WINDOW_CORNER_PREFERENCE = DWM_WINDOW_CORNER_PREFERENCE(3i32); #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct DWMWINDOWATTRIBUTE(pub i32); #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct DWM_SYSTEMBACKDROP_TYPE(pub i32); +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DWM_WINDOW_CORNER_PREFERENCE(pub i32); } pub mod Dxgi{ #[inline] @@ -47161,12 +47174,6 @@ impl IRecordInfo_Vtbl { } #[cfg(all(feature = "Win32_System_Com", feature = "Win32_System_Variant"))] impl windows_core::RuntimeName for IRecordInfo {} -#[repr(C)] -#[cfg(all(feature = "Win32_System_Com", feature = "Win32_System_Variant"))] -pub struct PARAMDESCEX { - pub cBytes: u32, - pub varDefaultValue: super::Variant::VARIANT, -} #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct PARAMFLAGS(pub u16); @@ -47177,11 +47184,11 @@ pub struct PARAMDESC { pub pparamdescex: *mut PARAMDESCEX, pub wParamFlags: PARAMFLAGS, } +#[repr(C)] #[cfg(all(feature = "Win32_System_Com", feature = "Win32_System_Variant"))] -impl Clone for PARAMDESCEX { - fn clone(&self) -> Self { - unsafe { core::mem::transmute_copy(self) } - } +pub struct PARAMDESCEX { + pub cBytes: u32, + pub varDefaultValue: super::Variant::VARIANT, } impl PARAMFLAGS { pub const fn contains(&self, other: Self) -> bool { @@ -47195,9 +47202,9 @@ impl Default for PARAMDESC { } } #[cfg(all(feature = "Win32_System_Com", feature = "Win32_System_Variant"))] -impl Default for PARAMDESCEX { - fn default() -> Self { - unsafe { core::mem::zeroed() } +impl Clone for PARAMDESCEX { + fn clone(&self) -> Self { + unsafe { core::mem::transmute_copy(self) } } } impl core::ops::BitOr for PARAMFLAGS { @@ -47206,6 +47213,12 @@ impl core::ops::BitOr for PARAMFLAGS { Self(self.0 | other.0) } } +#[cfg(all(feature = "Win32_System_Com", feature = "Win32_System_Variant"))] +impl Default for PARAMDESCEX { + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} impl core::ops::BitAnd for PARAMFLAGS { type Output = Self; fn bitand(self, other: Self) -> Self { @@ -48353,6 +48366,11 @@ pub struct PSC_STATE(pub i32); } pub mod WindowsAndMessaging{ #[inline] +pub unsafe fn AdjustWindowRectEx(lprect: *mut super::super::Foundation::RECT, dwstyle: WINDOW_STYLE, bmenu: bool, dwexstyle: WINDOW_EX_STYLE) -> windows_core::Result<()> { + windows_core::link!("user32.dll" "system" fn AdjustWindowRectEx(lprect : *mut super::super::Foundation:: RECT, dwstyle : WINDOW_STYLE, bmenu : windows_core::BOOL, dwexstyle : WINDOW_EX_STYLE) -> windows_core::BOOL); + unsafe { AdjustWindowRectEx(lprect as _, dwstyle, bmenu.into(), dwexstyle).ok() } +} +#[inline] pub unsafe fn CreateWindowExW(dwexstyle: WINDOW_EX_STYLE, lpclassname: P1, lpwindowname: P2, dwstyle: WINDOW_STYLE, x: i32, y: i32, nwidth: i32, nheight: i32, hwndparent: Option, hmenu: Option, hinstance: Option, lpparam: Option<*const core::ffi::c_void>) -> windows_core::Result where P1: windows_core::Param, @@ -48558,6 +48576,7 @@ pub const CW_USEDEFAULT: i32 = -2147483648i32; pub struct GDI_IMAGE_TYPE(pub u32); pub const GWLP_USERDATA: WINDOW_LONG_PTR_INDEX = WINDOW_LONG_PTR_INDEX(-21i32); pub const GWL_EXSTYLE: WINDOW_LONG_PTR_INDEX = WINDOW_LONG_PTR_INDEX(-20i32); +pub const GWL_STYLE: WINDOW_LONG_PTR_INDEX = WINDOW_LONG_PTR_INDEX(-16i32); #[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct HCURSOR(pub *mut core::ffi::c_void); @@ -48746,6 +48765,17 @@ pub struct MSG { pub time: u32, pub pt: super::super::Foundation::POINT, } +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NCCALCSIZE_PARAMS { + pub rgrc: [super::super::Foundation::RECT; 3], + pub lppos: *mut WINDOWPOS, +} +impl Default for NCCALCSIZE_PARAMS { + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct PEEK_MESSAGE_REMOVE_TYPE(pub u32); @@ -48827,6 +48857,7 @@ pub const SM_CXICON: SYSTEM_METRICS_INDEX = SYSTEM_METRICS_INDEX(11i32); pub const SM_CXSMICON: SYSTEM_METRICS_INDEX = SYSTEM_METRICS_INDEX(49i32); pub const SM_CYICON: SYSTEM_METRICS_INDEX = SYSTEM_METRICS_INDEX(12i32); pub const SM_CYSMICON: SYSTEM_METRICS_INDEX = SYSTEM_METRICS_INDEX(50i32); +pub const SWP_FRAMECHANGED: SET_WINDOW_POS_FLAGS = SET_WINDOW_POS_FLAGS(32u32); pub const SWP_NOACTIVATE: SET_WINDOW_POS_FLAGS = SET_WINDOW_POS_FLAGS(16u32); pub const SWP_NOMOVE: SET_WINDOW_POS_FLAGS = SET_WINDOW_POS_FLAGS(2u32); pub const SWP_NOSIZE: SET_WINDOW_POS_FLAGS = SET_WINDOW_POS_FLAGS(1u32); @@ -48886,6 +48917,17 @@ impl core::ops::Not for WINDOWPLACEMENT_FLAGS { Self(self.0.not()) } } +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct WINDOWPOS { + pub hwnd: super::super::Foundation::HWND, + pub hwndInsertAfter: super::super::Foundation::HWND, + pub x: i32, + pub y: i32, + pub cx: i32, + pub cy: i32, + pub flags: SET_WINDOW_POS_FLAGS, +} #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct WINDOW_EX_STYLE(pub u32); @@ -49045,6 +49087,8 @@ impl core::ops::Not for WNDCLASS_STYLES { } } pub type WNDPROC = Option super::super::Foundation::LRESULT>; +pub const WS_BORDER: WINDOW_STYLE = WINDOW_STYLE(8388608u32); +pub const WS_CAPTION: WINDOW_STYLE = WINDOW_STYLE(12582912u32); pub const WS_CLIPCHILDREN: WINDOW_STYLE = WINDOW_STYLE(33554432u32); pub const WS_CLIPSIBLINGS: WINDOW_STYLE = WINDOW_STYLE(67108864u32); pub const WS_EX_ACCEPTFILES: WINDOW_EX_STYLE = WINDOW_EX_STYLE(16u32); @@ -49053,11 +49097,9 @@ pub const WS_EX_LAYERED: WINDOW_EX_STYLE = WINDOW_EX_STYLE(524288u32); pub const WS_EX_TOOLWINDOW: WINDOW_EX_STYLE = WINDOW_EX_STYLE(128u32); pub const WS_EX_TOPMOST: WINDOW_EX_STYLE = WINDOW_EX_STYLE(8u32); pub const WS_EX_WINDOWEDGE: WINDOW_EX_STYLE = WINDOW_EX_STYLE(256u32); -pub const WS_MAXIMIZEBOX: WINDOW_STYLE = WINDOW_STYLE(65536u32); -pub const WS_MINIMIZEBOX: WINDOW_STYLE = WINDOW_STYLE(131072u32); +pub const WS_OVERLAPPEDWINDOW: WINDOW_STYLE = WINDOW_STYLE(13565952u32); pub const WS_POPUP: WINDOW_STYLE = WINDOW_STYLE(2147483648u32); -pub const WS_SIZEBOX: WINDOW_STYLE = WINDOW_STYLE(262144u32); -pub const WS_SYSMENU: WINDOW_STYLE = WINDOW_STYLE(524288u32); +pub const WS_THICKFRAME: WINDOW_STYLE = WINDOW_STYLE(262144u32); } } } diff --git a/platform/network/src/http_server.rs b/platform/network/src/http_server.rs index 0af6e2cb1..188f7790f 100644 --- a/platform/network/src/http_server.rs +++ b/platform/network/src/http_server.rs @@ -59,24 +59,50 @@ pub enum HttpServerRequest { }, } -pub fn start_http_server(http_server: HttpServer) -> Option> { +/// Handle to a running HTTP server. Sending on `shutdown` (or dropping the +/// sender) makes the accept loop exit so the listen port is released and the +/// thread can be joined. +pub struct HttpServerHandle { + pub thread: std::thread::JoinHandle<()>, + pub shutdown: mpsc::Sender<()>, +} + +pub fn start_http_server(http_server: HttpServer) -> Option { let listener = if let Ok(listener) = TcpListener::bind(http_server.listen_address) { listener } else { println!("Cannot bind http server port"); return None; }; + if listener.set_nonblocking(true).is_err() { + println!("Cannot set http server non-blocking"); + return None; + } + let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(); let listen_thread = { std::thread::spawn(move || { let mut connection_counter = 0u64; - for tcp_stream in listener.incoming() { - let mut tcp_stream = if let Ok(tcp_stream) = tcp_stream { - tcp_stream - } else { - println!("Incoming stream failure"); - continue; + loop { + if shutdown_rx.try_recv().is_ok() { + break; + } + let mut tcp_stream = match listener.accept() { + Ok((tcp_stream, _)) => tcp_stream, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // No pending connection; poll the shutdown channel + // periodically so a drop releases the port promptly. + std::thread::sleep(std::time::Duration::from_millis(5)); + continue; + } + Err(_) => { + println!("Incoming stream failure"); + continue; + } }; + if tcp_stream.set_nonblocking(false).is_err() { + continue; + } let http_server = http_server.clone(); connection_counter += 1; let _read_thread = std::thread::spawn(move || { @@ -108,7 +134,10 @@ pub fn start_http_server(http_server: HttpServer) -> Option Option> { + ) -> Option { crate::http_server::start_http_server(http_server) } diff --git a/platform/src/area.rs b/platform/src/area.rs index a446b0c6d..c78d36709 100644 --- a/platform/src/area.rs +++ b/platform/src/area.rs @@ -241,15 +241,14 @@ impl Area { Area::Rect(ra) => { // we need to clip this drawlist too let draw_list = &cx.draw_lists[ra.draw_list_id]; - // Guard against a stale rect area (its draw list was rebuilt smaller, - // e.g. a recycled/filtered list cell) — same check the Instance arm - // uses — rather than indexing rect_areas out of bounds and panicking. - if draw_list.redraw_id != ra.redraw_id - || ra.rect_id >= draw_list.rect_areas.len() - { + // A stale area outlives the draw that made it, and its rect_id + // can then be past the end of a shorter list. + if draw_list.redraw_id != ra.redraw_id { return Rect::default(); } - let rect_area = &draw_list.rect_areas[ra.rect_id]; + let Some(rect_area) = draw_list.rect_areas.get(ra.rect_id) else { + return Rect::default(); + }; if draw_list.draw_list_has_clip { let p3 = dvec2( draw_list.draw_list_uniforms.view_clip.x as f64, @@ -315,8 +314,9 @@ impl Area { Area::Rect(ra) => { let draw_list = &cx.draw_lists[ra.draw_list_id]; if draw_list.redraw_id == ra.redraw_id { - let rect_area = &draw_list.rect_areas[ra.rect_id]; - return rect_area.rect; + if let Some(rect_area) = draw_list.rect_areas.get(ra.rect_id) { + return rect_area.rect; + } } Rect::default() } @@ -352,12 +352,12 @@ impl Area { } Area::Rect(ra) => { let draw_list = &cx.draw_lists[ra.draw_list_id]; - if draw_list.redraw_id != ra.redraw_id - || ra.rect_id >= draw_list.rect_areas.len() - { + if draw_list.redraw_id != ra.redraw_id { return abs; } - let rect_area = &draw_list.rect_areas[ra.rect_id]; + let Some(rect_area) = draw_list.rect_areas.get(ra.rect_id) else { + return abs; + }; Vec2d { x: abs.x - rect_area.rect.pos.x, y: abs.y - rect_area.rect.pos.y, @@ -417,13 +417,12 @@ impl Area { } Area::Rect(ra) => { let draw_list = &mut cx.draw_lists[ra.draw_list_id]; - if draw_list.redraw_id != ra.redraw_id - || ra.rect_id >= draw_list.rect_areas.len() - { + if draw_list.redraw_id != ra.redraw_id { return; } - let rect_area = &mut draw_list.rect_areas[ra.rect_id]; - rect_area.rect = *rect + if let Some(rect_area) = draw_list.rect_areas.get_mut(ra.rect_id) { + rect_area.rect = *rect + } } _ => (), } diff --git a/platform/src/cx.rs b/platform/src/cx.rs index 8dd2e09c4..7488af44c 100644 --- a/platform/src/cx.rs +++ b/platform/src/cx.rs @@ -38,7 +38,7 @@ use { std::{ any::{Any, TypeId}, cell::RefCell, - collections::{HashMap, HashSet}, + collections::{HashMap, HashSet, VecDeque}, rc::Rc, sync::Arc, }, @@ -104,7 +104,7 @@ pub struct Cx { pub keyboard_shift: f64, pub(crate) drag_drop: CxDragDrop, - pub(crate) platform_ops: Vec, + pub(crate) platform_ops: VecDeque, pub(crate) pending_camera_playbacks: Vec, pub(crate) new_next_frames: HashSet, diff --git a/platform/src/cx_api.rs b/platform/src/cx_api.rs index 73a5f5161..d50953ec8 100644 --- a/platform/src/cx_api.rs +++ b/platform/src/cx_api.rs @@ -30,6 +30,7 @@ use { }, std::{ any::{Any, TypeId}, + collections::VecDeque, ops::Range, rc::Rc, }, @@ -104,14 +105,14 @@ impl<'a> CxSystemBrowser<'a> { } pub fn spawn(&mut self, url: &str) { - self.cx.platform_ops.push(CxOsOp::SpawnSystemBrowser { + self.cx.platform_ops.push_back(CxOsOp::SpawnSystemBrowser { browser_id: self.id.0, url: url.to_string(), }); } pub fn update(&mut self, area: Area, visible: bool) { - self.cx.platform_ops.push(CxOsOp::UpdateSystemBrowser { + self.cx.platform_ops.push_back(CxOsOp::UpdateSystemBrowser { browser_id: self.id.0, area, visible, @@ -119,13 +120,13 @@ impl<'a> CxSystemBrowser<'a> { } pub fn detach(&mut self) { - self.cx.platform_ops.push(CxOsOp::DetachSystemBrowser { + self.cx.platform_ops.push_back(CxOsOp::DetachSystemBrowser { browser_id: self.id.0, }); } pub fn set_url(&mut self, url: &str, replace: bool) { - self.cx.platform_ops.push(CxOsOp::SetSystemBrowserUrl { + self.cx.platform_ops.push_back(CxOsOp::SetSystemBrowserUrl { browser_id: self.id.0, url: url.to_string(), replace, @@ -133,14 +134,14 @@ impl<'a> CxSystemBrowser<'a> { } pub fn history_go(&mut self, delta: i32) { - self.cx.platform_ops.push(CxOsOp::SystemBrowserHistoryGo { + self.cx.platform_ops.push_back(CxOsOp::SystemBrowserHistoryGo { browser_id: self.id.0, delta, }); } pub fn close(&mut self) { - self.cx.platform_ops.push(CxOsOp::CloseSystemBrowser { + self.cx.platform_ops.push_back(CxOsOp::CloseSystemBrowser { browser_id: self.id.0, }); } @@ -501,6 +502,15 @@ impl std::fmt::Debug for CxOsOp { } } } + +/// Requeue an OS op that cannot run yet. FIFO: append so remaining already-queued +/// ops still run first. Returns `true` if the drain should continue (`len() > 1`); +/// `false` if this is the only op left — break and retry on the next event. +pub(crate) fn defer_platform_op(platform_ops: &mut VecDeque, op: CxOsOp) -> bool { + platform_ops.push_back(op); + platform_ops.len() > 1 +} + impl Cx { pub fn in_draw_event(&self) -> bool { self.in_draw_event @@ -816,35 +826,35 @@ impl Cx { } pub fn update_macos_menu(&mut self, menu: MacosMenu) { - self.platform_ops.push(CxOsOp::UpdateMacosMenu(menu)); + self.platform_ops.push_back(CxOsOp::UpdateMacosMenu(menu)); } pub fn xr_start_presenting(&mut self) { - self.platform_ops.push(CxOsOp::XrStartPresenting); + self.platform_ops.push_back(CxOsOp::XrStartPresenting); } pub fn xr_set_render_scale(&mut self, scale: f32) { - self.platform_ops.push(CxOsOp::XrSetRenderScale(scale)); + self.platform_ops.push_back(CxOsOp::XrSetRenderScale(scale)); } pub fn xr_advertise_anchor(&mut self, anchor: XrAnchor) { - self.platform_ops.push(CxOsOp::XrAdvertiseAnchor(anchor)); + self.platform_ops.push_back(CxOsOp::XrAdvertiseAnchor(anchor)); } pub fn xr_set_local_anchor(&mut self, anchor: XrAnchor) { - self.platform_ops.push(CxOsOp::XrSetLocalAnchor(anchor)); + self.platform_ops.push_back(CxOsOp::XrSetLocalAnchor(anchor)); } pub fn xr_set_local_floor(&mut self, floor_y: f32) { - self.platform_ops.push(CxOsOp::XrSetLocalFloor(floor_y)); + self.platform_ops.push_back(CxOsOp::XrSetLocalFloor(floor_y)); } pub fn xr_discover_anchor(&mut self, id: u8) { - self.platform_ops.push(CxOsOp::XrDiscoverAnchor(id)); + self.platform_ops.push_back(CxOsOp::XrDiscoverAnchor(id)); } pub fn quit(&mut self) { - self.platform_ops.push(CxOsOp::Quit); + self.platform_ops.push_back(CxOsOp::Quit); } pub fn request_quit(&mut self, reason: QuitReason) -> bool { @@ -886,7 +896,7 @@ impl Cx { // Determines whether to show your application in the dock when it runs. The default value is true. // You can remove the dock icon by setting this value to false. pub fn show_in_dock(&mut self, show: bool) { - self.platform_ops.push(CxOsOp::ShowInDock(show)); + self.platform_ops.push_back(CxOsOp::ShowInDock(show)); } /// Controls how the system bars (status bar and navigation bar) icons and @@ -905,10 +915,19 @@ impl Cx { } pub fn push_unique_platform_op(&mut self, op: CxOsOp) { if self.platform_ops.iter().find(|o| **o == op).is_none() { - self.platform_ops.push(op); + self.platform_ops.push_back(op); } } + /// Requeue an OS op that cannot run yet (typical case: `SetTopmost` before + /// any native window exists). FIFO: append so remaining already-queued ops + /// still run first in this drain. Returns `true` if the drain should + /// continue (`len() > 1`); `false` if this is the only op left — break and + /// retry on the next event instead of spinning. + pub(crate) fn defer_platform_op(&mut self, op: CxOsOp) -> bool { + defer_platform_op(&mut self.platform_ops, op) + } + pub fn show_text_ime(&mut self, area: Area, pos: Vec2d) { // No line metrics from this entry point: a zero-height rect anchors the // IME at the bare point (same behavior as before the rect change). @@ -931,7 +950,7 @@ impl Cx { if !self.keyboard.text_ime_dismissed { self.ime_area = area; self.platform_ops - .push(CxOsOp::ShowTextIME(area, cursor_rect, config)); + .push_back(CxOsOp::ShowTextIME(area, cursor_rect, config)); } } @@ -941,7 +960,7 @@ impl Cx { selection: Range, composition: Option>, ) { - self.platform_ops.push(CxOsOp::SyncImeState { + self.platform_ops.push_back(CxOsOp::SyncImeState { text, selection, composition, @@ -950,12 +969,12 @@ impl Cx { pub fn hide_text_ime(&mut self) { self.keyboard.reset_text_ime_dismissed(); - self.platform_ops.push(CxOsOp::HideTextIME); + self.platform_ops.push_back(CxOsOp::HideTextIME); } pub fn text_ime_was_dismissed(&mut self) { self.keyboard.set_text_ime_dismissed(); - self.platform_ops.push(CxOsOp::HideTextIME); + self.platform_ops.push_back(CxOsOp::HideTextIME); } /// Set or clear a window's `dpi_override` at runtime. @@ -1024,7 +1043,7 @@ impl Cx { /// the text selection from Rust directly. The `has_selection` parameter is only /// used to determine which menu items to show, not for the operations themselves. pub fn show_clipboard_actions(&mut self, has_selection: bool, rect: Rect, keyboard_shift: f64) { - self.platform_ops.push(CxOsOp::ShowClipboardActions { + self.platform_ops.push_back(CxOsOp::ShowClipboardActions { has_selection, rect, keyboard_shift, @@ -1033,7 +1052,7 @@ impl Cx { /// Hides the clipboard actions menu pub fn hide_clipboard_actions(&mut self) { - self.platform_ops.push(CxOsOp::HideClipboardActions); + self.platform_ops.push_back(CxOsOp::HideClipboardActions); } /// Copies the given string to the clipboard. @@ -1041,14 +1060,14 @@ impl Cx { /// Due to lack of platform clipboard support, it does not work on Web or tvOS. pub fn copy_to_clipboard(&mut self, content: &str) { self.platform_ops - .push(CxOsOp::CopyToClipboard(content.to_owned())); + .push_back(CxOsOp::CopyToClipboard(content.to_owned())); } /// Sets the primary selection (Linux middle-click paste). /// No-op on non-Linux platforms. pub fn set_primary_selection(&mut self, content: &str) { self.platform_ops - .push(CxOsOp::SetPrimarySelection(content.to_owned())); + .push_back(CxOsOp::SetPrimarySelection(content.to_owned())); } /// Forward an accessibility tree update to the platform adapter. @@ -1057,7 +1076,7 @@ impl Cx { /// downcast it when an accessibility adapter is active. pub fn update_accessibility_tree(&mut self, update: Box) { self.platform_ops - .push(CxOsOp::AccessibilityUpdate(AccessibilityUpdatePayload( + .push_back(CxOsOp::AccessibilityUpdate(AccessibilityUpdatePayload( update, ))); } @@ -1065,18 +1084,18 @@ impl Cx { /// Show native selection handles at the given start and end positions (mobile). pub fn show_selection_handles(&mut self, start: Vec2d, end: Vec2d) { self.platform_ops - .push(CxOsOp::ShowSelectionHandles { start, end }); + .push_back(CxOsOp::ShowSelectionHandles { start, end }); } /// Update positions of visible selection handles (mobile). pub fn update_selection_handles(&mut self, start: Vec2d, end: Vec2d) { self.platform_ops - .push(CxOsOp::UpdateSelectionHandles { start, end }); + .push_back(CxOsOp::UpdateSelectionHandles { start, end }); } /// Hide selection handles (mobile). pub fn hide_selection_handles(&mut self) { - self.platform_ops.push(CxOsOp::HideSelectionHandles); + self.platform_ops.push_back(CxOsOp::HideSelectionHandles); } pub fn start_dragging(&mut self, items: Vec) { @@ -1085,7 +1104,7 @@ impl Cx { panic!("start drag twice"); } }); - self.platform_ops.push(CxOsOp::StartDragging(items)); + self.platform_ops.push_back(CxOsOp::StartDragging(items)); } pub fn set_cursor(&mut self, cursor: MouseCursor) { @@ -1096,7 +1115,7 @@ impl Cx { }) { *p = CxOsOp::SetCursor(cursor) } else { - self.platform_ops.push(CxOsOp::SetCursor(cursor)) + self.platform_ops.push_back(CxOsOp::SetCursor(cursor)) } } @@ -1153,7 +1172,7 @@ impl Cx { pub fn start_timeout(&mut self, delay: f64) -> Timer { self.timer_id += 1; - self.platform_ops.push(CxOsOp::StartTimer { + self.platform_ops.push_back(CxOsOp::StartTimer { timer_id: self.timer_id, interval: delay, repeats: false, @@ -1163,7 +1182,7 @@ impl Cx { pub fn start_interval(&mut self, interval: f64) -> Timer { self.timer_id += 1; - self.platform_ops.push(CxOsOp::StartTimer { + self.platform_ops.push_back(CxOsOp::StartTimer { timer_id: self.timer_id, interval, repeats: true, @@ -1173,13 +1192,13 @@ impl Cx { pub fn stop_timer(&mut self, timer: Timer) { if timer.0 != 0 { - self.platform_ops.push(CxOsOp::StopTimer(timer.0)); + self.platform_ops.push_back(CxOsOp::StopTimer(timer.0)); } } pub fn request_permission(&mut self, permission: crate::permission::Permission) -> i32 { self.permissions_request_id += 1; - self.platform_ops.push(CxOsOp::RequestPermission { + self.platform_ops.push_back(CxOsOp::RequestPermission { request_id: self.permissions_request_id, permission, }); @@ -1188,7 +1207,7 @@ impl Cx { pub fn check_permission(&mut self, permission: crate::permission::Permission) -> i32 { self.permissions_request_id += 1; - self.platform_ops.push(CxOsOp::CheckPermission { + self.platform_ops.push_back(CxOsOp::CheckPermission { request_id: self.permissions_request_id, permission, }); @@ -1202,12 +1221,12 @@ impl Cx { /// platform, failures arrive as [`Event::LocationError`]. Platforms /// without a location service log an error and stay silent. pub fn start_location_updates(&mut self) { - self.platform_ops.push(CxOsOp::StartLocationUpdates); + self.platform_ops.push_back(CxOsOp::StartLocationUpdates); } /// Stop streaming position fixes. pub fn stop_location_updates(&mut self) { - self.platform_ops.push(CxOsOp::StopLocationUpdates); + self.platform_ops.push_back(CxOsOp::StopLocationUpdates); } pub fn get_dpi_factor_of(&mut self, area: &Area) -> f64 { @@ -1504,14 +1523,14 @@ impl Cx { } /* pub fn web_socket_open(&mut self, request_id: LiveId, request: HttpRequest) { - self.platform_ops.push(CxOsOp::WebSocketOpen{ + self.platform_ops.push_back(CxOsOp::WebSocketOpen{ request, request_id, }); } pub fn web_socket_send_binary(&mut self, request_id: LiveId, data: Vec) { - self.platform_ops.push(CxOsOp::WebSocketSendBinary{ + self.platform_ops.push_back(CxOsOp::WebSocketSendBinary{ request_id, data, }); @@ -1587,7 +1606,7 @@ impl Cx { let _request_id = self.request_permission(permission); return; } - self.platform_ops.push(CxOsOp::PrepareVideoPlayback( + self.platform_ops.push_back(CxOsOp::PrepareVideoPlayback( video_id, source, camera_preview_mode, @@ -1616,7 +1635,7 @@ impl Cx { } match result.status { crate::permission::PermissionStatus::Granted => { - self.platform_ops.push(CxOsOp::PrepareVideoPlayback( + self.platform_ops.push_back(CxOsOp::PrepareVideoPlayback( p.video_id, p.source, p.camera_preview_mode, @@ -1645,11 +1664,11 @@ impl Cx { pub fn attach_camera_native_preview(&mut self, video_id: LiveId, area: Area) { self.platform_ops - .push(CxOsOp::AttachCameraNativePreview { video_id, area }); + .push_back(CxOsOp::AttachCameraNativePreview { video_id, area }); } pub fn update_camera_native_preview(&mut self, video_id: LiveId, area: Area, visible: bool) { - self.platform_ops.push(CxOsOp::UpdateCameraNativePreview { + self.platform_ops.push_back(CxOsOp::UpdateCameraNativePreview { video_id, area, visible, @@ -1658,36 +1677,36 @@ impl Cx { pub fn detach_camera_native_preview(&mut self, video_id: LiveId) { self.platform_ops - .push(CxOsOp::DetachCameraNativePreview { video_id }); + .push_back(CxOsOp::DetachCameraNativePreview { video_id }); } pub fn begin_video_playback(&mut self, video_id: LiveId) { self.drop_pending_video_transport(video_id); - self.platform_ops.push(CxOsOp::BeginVideoPlayback(video_id)); + self.platform_ops.push_back(CxOsOp::BeginVideoPlayback(video_id)); } pub fn pause_video_playback(&mut self, video_id: LiveId) { - // platform_ops are drained LIFO (`pop`). Without coalescing, a same-frame - // pause→resume becomes resume then pause and leaves playback stuck paused. + // Last-wins coalescing: one frame should apply a single play/pause + // intent even though the queue is FIFO. self.drop_pending_video_transport(video_id); - self.platform_ops.push(CxOsOp::PauseVideoPlayback(video_id)); + self.platform_ops.push_back(CxOsOp::PauseVideoPlayback(video_id)); } pub fn resume_video_playback(&mut self, video_id: LiveId) { self.drop_pending_video_transport(video_id); self.platform_ops - .push(CxOsOp::ResumeVideoPlayback(video_id)); + .push_back(CxOsOp::ResumeVideoPlayback(video_id)); } pub fn mute_video_playback(&mut self, video_id: LiveId) { self.drop_pending_video_mute(video_id); - self.platform_ops.push(CxOsOp::MuteVideoPlayback(video_id)); + self.platform_ops.push_back(CxOsOp::MuteVideoPlayback(video_id)); } pub fn unmute_video_playback(&mut self, video_id: LiveId) { self.drop_pending_video_mute(video_id); self.platform_ops - .push(CxOsOp::UnmuteVideoPlayback(video_id)); + .push_back(CxOsOp::UnmuteVideoPlayback(video_id)); } /// Keep only the latest play/pause/begin intent for `video_id`. @@ -1709,7 +1728,7 @@ impl Cx { pub fn cleanup_video_playback_resources(&mut self, video_id: LiveId) { self.platform_ops - .push(CxOsOp::CleanupVideoPlaybackResources(video_id)); + .push_back(CxOsOp::CleanupVideoPlaybackResources(video_id)); } pub fn cancel_pending_camera_playback(&mut self, video_id: LiveId) { @@ -1719,27 +1738,27 @@ impl Cx { pub fn seek_video_playback(&mut self, video_id: LiveId, position_ms: u64) { self.platform_ops - .push(CxOsOp::SeekVideoPlayback(video_id, position_ms)); + .push_back(CxOsOp::SeekVideoPlayback(video_id, position_ms)); } pub fn set_video_volume(&mut self, video_id: LiveId, volume: f64) { self.platform_ops - .push(CxOsOp::SetVideoVolume(video_id, volume)); + .push_back(CxOsOp::SetVideoVolume(video_id, volume)); } pub fn set_video_playback_rate(&mut self, video_id: LiveId, rate: f64) { self.platform_ops - .push(CxOsOp::SetVideoPlaybackRate(video_id, rate)); + .push_back(CxOsOp::SetVideoPlaybackRate(video_id, rate)); } pub fn select_video_track(&mut self, video_id: LiveId, index: usize) { self.platform_ops - .push(CxOsOp::SelectVideoTrack(video_id, index)); + .push_back(CxOsOp::SelectVideoTrack(video_id, index)); } pub fn select_audio_track(&mut self, video_id: LiveId, index: usize) { self.platform_ops - .push(CxOsOp::SelectAudioTrack(video_id, index)); + .push_back(CxOsOp::SelectAudioTrack(video_id, index)); } pub fn prepare_audio_playback( @@ -1749,7 +1768,7 @@ impl Cx { autoplay: bool, should_loop: bool, ) { - self.platform_ops.push(CxOsOp::PrepareAudioPlayback( + self.platform_ops.push_back(CxOsOp::PrepareAudioPlayback( video_id, source, autoplay, @@ -1763,22 +1782,22 @@ impl Cx { pub fn open_system_savefile_dialog(&mut self) { self.platform_ops - .push(CxOsOp::SaveFileDialog(FileDialog::new())); + .push_back(CxOsOp::SaveFileDialog(FileDialog::new())); } pub fn open_system_openfile_dialog(&mut self) { self.platform_ops - .push(CxOsOp::SelectFileDialog(FileDialog::new())); + .push_back(CxOsOp::SelectFileDialog(FileDialog::new())); } pub fn open_system_savefolder_dialog(&mut self) { self.platform_ops - .push(CxOsOp::SaveFolderDialog(FileDialog::new())); + .push_back(CxOsOp::SaveFolderDialog(FileDialog::new())); } pub fn open_system_openfolder_dialog(&mut self) { self.platform_ops - .push(CxOsOp::SelectFolderDialog(FileDialog::new())); + .push_back(CxOsOp::SelectFolderDialog(FileDialog::new())); } pub fn event_id(&self) -> u64 { @@ -1872,3 +1891,54 @@ macro_rules! register_component_factory { ); }; } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::VecDeque; + + #[test] + fn defer_platform_op_breaks_when_requeued_op_is_alone() { + let window_id = WindowId(0, 0); + let mut platform_ops = VecDeque::new(); + + assert!(!defer_platform_op( + &mut platform_ops, + CxOsOp::SetTopmost(window_id, true), + )); + assert_eq!(platform_ops, vec![CxOsOp::SetTopmost(window_id, true)]); + } + + #[test] + fn defer_platform_op_appends_so_pending_ops_still_run_first() { + let window_id = WindowId(0, 0); + let mut platform_ops = VecDeque::from(vec![CxOsOp::CreateWindow(window_id)]); + + assert!(defer_platform_op( + &mut platform_ops, + CxOsOp::SetTopmost(window_id, true), + )); + assert_eq!( + platform_ops, + vec![ + CxOsOp::CreateWindow(window_id), + CxOsOp::SetTopmost(window_id, true), + ] + ); + } + + #[test] + fn platform_ops_drain_fifo_create_window_then_set_topmost() { + let window_id = WindowId(0, 0); + let mut platform_ops = VecDeque::new(); + platform_ops.push_back(CxOsOp::CreateWindow(window_id)); + platform_ops.push_back(CxOsOp::SetTopmost(window_id, true)); + + let first = platform_ops.pop_front().unwrap(); + let second = platform_ops.pop_front().unwrap(); + assert!(matches!(first, CxOsOp::CreateWindow(_))); + assert!(matches!(second, CxOsOp::SetTopmost(_, true))); + assert!(platform_ops.is_empty()); + } +} + diff --git a/platform/src/gpu_texture.rs b/platform/src/gpu_texture.rs index 47641cf0c..337b87664 100644 --- a/platform/src/gpu_texture.rs +++ b/platform/src/gpu_texture.rs @@ -35,13 +35,18 @@ use std::{ }; use crate::{ - texture::{ - CxTexturePool, Texture, TextureAlloc, TextureCategory, TextureFormat, TextureId, - TexturePixel, - }, + texture::{Texture, TextureAlloc, TextureCategory, TextureFormat, TexturePixel}, Cx, }; +#[cfg(any( + target_os = "windows", + all(target_os = "linux", not(any(target_env = "ohos", linux_direct))), + target_os = "macos", + target_os = "ios", +))] +use crate::texture::{CxTexturePool, TextureId}; + /// Serializes hard-decode / media GPU work with Makepad present copies on the /// shared D3D11 device. Recursive so the same thread may nest lock calls /// (common when a decoder callback re-enters present). @@ -304,6 +309,7 @@ impl Drop for MetalNv12PresentCache { #[cfg(target_os = "windows")] mod windows_api { use super::*; + use crate::os::windows::d3d11_texture; use windows::{ core::Interface, Win32::Graphics::{ @@ -419,11 +425,15 @@ mod windows_api { .map_err(|e| format!("adopt_d3d11_bgra: cast to resource failed: {e}"))?; let mut out: Option = None; unsafe { - device - .CreateShaderResourceView(&resource, None, Some(&mut out)) - .map_err(|e| { - format!("adopt_d3d11_bgra: CreateShaderResourceView failed: {e:?}") - })?; + d3d11_texture::create_shader_resource_view( + &device, + &resource, + None, + Some(&mut out), + ) + .map_err(|e| { + format!("adopt_d3d11_bgra: CreateShaderResourceView failed: {e:?}") + })?; } out.ok_or_else(|| { "adopt_d3d11_bgra: CreateShaderResourceView returned null".to_string() @@ -503,7 +513,7 @@ mod windows_api { ) -> Result { let mut tex_desc = D3D11_TEXTURE2D_DESC::default(); unsafe { - texture.GetDesc(&mut tex_desc); + d3d11_texture::texture2d_get_desc(texture, &mut tex_desc); } if tex_desc.Format != DXGI_FORMAT_NV12 { return Err(format!( @@ -541,9 +551,13 @@ mod windows_api { let mut out: Option = None; unsafe { - device - .CreateShaderResourceView(&resource, Some(&desc), Some(&mut out)) - .map_err(|e| format!("NV12 plane SRV: CreateShaderResourceView failed: {e:?}"))?; + d3d11_texture::create_shader_resource_view( + device, + &resource, + Some(&desc), + Some(&mut out), + ) + .map_err(|e| format!("NV12 plane SRV: CreateShaderResourceView failed: {e:?}"))?; } out.ok_or_else(|| "NV12 plane SRV: null".into()) } @@ -558,7 +572,7 @@ mod windows_api { ) -> Result { let mut tex_desc = D3D11_TEXTURE2D_DESC::default(); unsafe { - texture.GetDesc(&mut tex_desc); + d3d11_texture::texture2d_get_desc(texture, &mut tex_desc); } if tex_desc.Format != DXGI_FORMAT_NV12 { return Err(format!( @@ -598,11 +612,13 @@ mod windows_api { let mut out: Option = None; unsafe { - device - .CreateShaderResourceView(&resource, Some(&desc), Some(&mut out)) - .map_err(|e| { - format!("NV12 array SRV: CreateShaderResourceView failed: {e:?}") - })?; + d3d11_texture::create_shader_resource_view( + device, + &resource, + Some(&desc), + Some(&mut out), + ) + .map_err(|e| format!("NV12 array SRV: CreateShaderResourceView failed: {e:?}"))?; } out.ok_or_else(|| "NV12 array SRV: null".into()) } @@ -640,8 +656,7 @@ mod windows_api { }; let mut tex: Option = None; unsafe { - device - .CreateTexture2D(&desc, None, Some(&mut tex)) + d3d11_texture::create_texture_2d(device, &desc, None, Some(&mut tex)) .map_err(|e| format!("NV12 present: CreateTexture2D failed: {e:?}"))?; } *slot = Some(tex.ok_or_else(|| "NV12 present: null texture".to_string())?); @@ -663,7 +678,7 @@ mod windows_api { ) -> Result<(ID3D11Texture2D, ID3D11Texture2D), String> { let mut src_desc = D3D11_TEXTURE2D_DESC::default(); unsafe { - src.GetDesc(&mut src_desc); + d3d11_texture::texture2d_get_desc(src, &mut src_desc); } if src_desc.Format != DXGI_FORMAT_NV12 { return Err(format!( @@ -697,7 +712,7 @@ mod windows_api { let y_tex = ensure_nv12_present_tex(device, &mut present.y_slots[write], copy_w, copy_h)?; let uv_tex = ensure_nv12_present_tex(device, &mut present.uv_slots[write], copy_w, copy_h)?; - let context = unsafe { device.GetImmediateContext() } + let context = unsafe { d3d11_texture::device_get_immediate_context(device) } .map_err(|e| format!("NV12 present: GetImmediateContext failed: {e:?}"))?; let src_res: ID3D11Resource = src @@ -721,7 +736,8 @@ mod windows_api { let src_sub = array_slice; // Hold the shared media lock so present copies do not race decoder GPU work. with_media_d3d11_lock(|| unsafe { - context.CopySubresourceRegion( + d3d11_texture::copy_subresource_region( + &context, &y_res, 0, 0, @@ -731,7 +747,8 @@ mod windows_api { src_sub, Some(&src_box as *const _), ); - context.CopySubresourceRegion( + d3d11_texture::copy_subresource_region( + &context, &uv_res, 0, 0, @@ -867,7 +884,7 @@ mod windows_api { ) -> Result<(), String> { let mut desc = D3D11_TEXTURE2D_DESC::default(); unsafe { - texture.GetDesc(&mut desc); + d3d11_texture::texture2d_get_desc(texture, &mut desc); } if desc.Format != DXGI_FORMAT_NV12 { return Err(format!( diff --git a/platform/src/os/apple/ios/ios.rs b/platform/src/os/apple/ios/ios.rs index b4b373166..32c1961a2 100644 --- a/platform/src/os/apple/ios/ios.rs +++ b/platform/src/os/apple/ios/ios.rs @@ -52,6 +52,7 @@ use { std::{ cell::RefCell, collections::HashMap, + panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, rc::Rc, sync::{ mpsc::{channel, Receiver, Sender}, @@ -419,8 +420,47 @@ impl Drop for IosNativeCameraPreview { } } +fn ios_panic_summary(info: &std::panic::PanicHookInfo<'_>) -> String { + let payload = if let Some(payload) = info.payload().downcast_ref::<&str>() { + (*payload).to_string() + } else if let Some(payload) = info.payload().downcast_ref::() { + payload.clone() + } else { + "non-string panic payload".to_string() + }; + let location = info + .location() + .map(|location| { + format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ) + }) + .unwrap_or_else(|| "".to_string()); + let thread = std::thread::current(); + let thread_name = thread.name().unwrap_or(""); + let backtrace = std::backtrace::Backtrace::force_capture(); + format!( + "iOS panic hook: thread={thread_name} location={location} payload={payload}\n{backtrace}" + ) +} + +/// The default hook writes to stderr, which goes nowhere on a device, so route +/// panics through `error!` (NSLog) while the panicking frame is still on the stack. +fn install_ios_panic_hook() { + let previous_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + crate::error!("{}", ios_panic_summary(info)); + previous_hook(info); + })); +} + impl Cx { pub fn event_loop(cx: Rc>) { + install_ios_panic_hook(); + let data_path = IosApp::get_ios_directory_paths(); // Get device info @@ -459,9 +499,15 @@ impl Cx { let event_flow = cx_ref.ios_event_callback(event, &mut metal_cx); let executor = cx_ref.executor.take().unwrap(); drop(cx_ref); - executor.run_until_stalled(); + // Put the executor back even if a spawned task panics, so + // the `take` above can't hand a `None` to the next event. + let stalled = catch_unwind(AssertUnwindSafe(|| executor.run_until_stalled())); let mut cx_ref = cx.borrow_mut(); cx_ref.executor = Some(executor); + drop(cx_ref); + if let Err(payload) = stalled { + resume_unwind(payload); + } event_flow } }), @@ -1035,7 +1081,7 @@ impl Cx { } fn handle_platform_ops(&mut self, metal_cx: &MetalCx) { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let window = &mut self.windows[window_id]; diff --git a/platform/src/os/apple/ios/ios_app.rs b/platform/src/os/apple/ios/ios_app.rs index 9481643ef..c2a722b3e 100644 --- a/platform/src/os/apple/ios/ios_app.rs +++ b/platform/src/os/apple/ios/ios_app.rs @@ -14,6 +14,7 @@ use { cell::{Cell, RefCell}, collections::HashMap, ffi::c_void, + panic::{catch_unwind, AssertUnwindSafe}, rc::Rc, time::Instant, }, @@ -1097,7 +1098,13 @@ impl IosApp { pub fn do_callback(event: IosEvent) -> bool { let cb = with_ios_app(|app| app.event_callback.take()); if let Some(mut callback) = cb { - let event_flow = callback(event); + // Every caller of this reaches us from an ObjC callback, so a panic + // escaping the app would unwind across `extern "C"` and abort. + let event_flow = catch_unwind(AssertUnwindSafe(|| callback(event))) + .unwrap_or_else(|_| { + crate::error!("Caught a panic while handling an iOS event, dropped it."); + EventFlow::Poll + }); let mtk_view = with_ios_app(|app| app.mtk_view.unwrap()); with_ios_app(|app| app.event_flow = event_flow); diff --git a/platform/src/os/apple/macos/macos.rs b/platform/src/os/apple/macos/macos.rs index f306bbe26..13439152a 100644 --- a/platform/src/os/apple/macos/macos.rs +++ b/platform/src/os/apple/macos/macos.rs @@ -243,11 +243,6 @@ impl MetalWindow { } } -fn defer_platform_op(platform_ops: &mut Vec, op: CxOsOp) -> bool { - platform_ops.insert(0, op); - platform_ops.len() > 1 -} - pub(crate) struct MacosNativeCameraPreview { input_id: crate::video::VideoInputId, format_id: crate::video::VideoFormatId, @@ -1157,7 +1152,7 @@ impl Cx { metal_windows: &mut Vec, metal_cx: &MetalCx, ) -> EventFlow { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let window = &mut self.windows[window_id]; @@ -1292,10 +1287,7 @@ impl Cx { } CxOsOp::SetTopmost(window_id, is_topmost) => { if metal_windows.is_empty() { - if defer_platform_op( - &mut self.platform_ops, - CxOsOp::SetTopmost(window_id, is_topmost), - ) { + if self.defer_platform_op(CxOsOp::SetTopmost(window_id, is_topmost)) { continue; } break; @@ -1864,41 +1856,6 @@ impl Cx { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn defer_platform_op_breaks_when_requeued_op_is_alone() { - let window_id = WindowId(0, 0); - let mut platform_ops = Vec::new(); - - assert!(!defer_platform_op( - &mut platform_ops, - CxOsOp::SetTopmost(window_id, true), - )); - assert_eq!(platform_ops, vec![CxOsOp::SetTopmost(window_id, true)]); - } - - #[test] - fn defer_platform_op_continues_when_other_ops_are_pending() { - let window_id = WindowId(0, 0); - let mut platform_ops = vec![CxOsOp::CreateWindow(window_id)]; - - assert!(defer_platform_op( - &mut platform_ops, - CxOsOp::SetTopmost(window_id, true), - )); - assert_eq!( - platform_ops, - vec![ - CxOsOp::SetTopmost(window_id, true), - CxOsOp::CreateWindow(window_id) - ] - ); - } -} - impl CxOsApi for Cx { fn pre_start() -> bool { init_apple_classes_global(); diff --git a/platform/src/os/apple/macos/macos_stdin.rs b/platform/src/os/apple/macos/macos_stdin.rs index b9a3eb05c..c3ead430e 100644 --- a/platform/src/os/apple/macos/macos_stdin.rs +++ b/platform/src/os/apple/macos/macos_stdin.rs @@ -371,7 +371,7 @@ impl Cx { _metal_cx: &MetalCx, stdin_windows: &mut Vec, ) { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { while window_id.id() >= stdin_windows.len() { diff --git a/platform/src/os/apple/tvos/tvos.rs b/platform/src/os/apple/tvos/tvos.rs index 7642ab3ed..937d31ba0 100644 --- a/platform/src/os/apple/tvos/tvos.rs +++ b/platform/src/os/apple/tvos/tvos.rs @@ -195,7 +195,7 @@ impl Cx { } fn handle_platform_ops(&mut self, _metal_cx: &MetalCx) { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let window = &mut self.windows[window_id]; diff --git a/platform/src/os/headless/event_loop.rs b/platform/src/os/headless/event_loop.rs index 9bd68e509..d832456a0 100644 --- a/platform/src/os/headless/event_loop.rs +++ b/platform/src/os/headless/event_loop.rs @@ -21,6 +21,19 @@ use std::{ time::Instant, }; +/// Backing-store scale for a headless window. Retina by default, because a +/// screenshot is expected to match what a real display would show. Rendering is +/// a software rasteriser here, so the cost is per PIXEL: a suite that only +/// asserts on logical geometry can set `MAKEPAD_HEADLESS_DPI=1` and do a +/// quarter of the work. +fn configured_headless_dpi() -> f64 { + std::env::var("MAKEPAD_HEADLESS_DPI") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|dpi| *dpi > 0.0) + .unwrap_or(2.0) +} + #[derive(Default)] struct HeadlessWindowState { created: bool, @@ -568,7 +581,7 @@ impl Cx { windows: &mut Vec, send_protocol: bool, ) -> bool { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { while window_id.id() >= windows.len() { @@ -580,7 +593,7 @@ impl Cx { .create_inner_size .unwrap_or_else(|| dvec2(1920.0, 1080.0)); let position = window.create_position.unwrap_or_else(|| dvec2(0.0, 0.0)); - let dpi_factor = 2.0; + let dpi_factor = configured_headless_dpi(); let state = &mut windows[window_id.id()]; state.created = true; diff --git a/platform/src/os/headless/jit.rs b/platform/src/os/headless/jit.rs index a44634319..9b87b1a83 100644 --- a/platform/src/os/headless/jit.rs +++ b/platform/src/os/headless/jit.rs @@ -39,6 +39,28 @@ impl HeadlessShaderJit { source: &str, ) -> Result { let shader_dir = self.root_dir.join(format!("shader_{source_hash:016x}")); + let cached_path = + shader_dir.join(format!("shader_{source_hash:016x}.{}", dylib_extension())); + + // The dylib is content-addressed by the hash of the source that made it, + // so one left by an earlier run is exactly what rustc would produce now. + // Reuse it: this compiles EVERY shader with `rustc -O` at startup, which + // costs tens of seconds per process — and a headless test suite starts a + // fresh process for every test. Anything unloadable (truncated by a killed + // run, built by a different toolchain) just falls through and recompiles. + if cached_path.is_file() { + if let Ok(loaded) = HeadlessLoadedModule::load(&cached_path) { + if let Ok(version) = loaded.shader_version() { + return Ok(HeadlessJitOutput { + dylib_path: cached_path, + module: Some(loaded), + shader_version: Some(version), + load_error: None, + }); + } + } + } + std::fs::create_dir_all(&shader_dir).map_err(|err| { format!( "failed to create headless shader output dir `{}`: {err}", @@ -54,8 +76,15 @@ impl HeadlessShaderJit { ) })?; - let dylib_path = - shader_dir.join(format!("shader_{source_hash:016x}.{}", dylib_extension())); + let dylib_path = cached_path; + // Compile to a private path and rename into place, so a crashed or + // killed run can never leave a half-written dylib for the next one to + // find (and so two processes building the same shader can't interleave). + let staging_path = shader_dir.join(format!( + "shader_{source_hash:016x}.{}.{}", + std::process::id(), + dylib_extension() + )); let crate_name = format!("makepad_headless_shader_{source_hash:016x}"); let output = Command::new("rustc") @@ -67,13 +96,14 @@ impl HeadlessShaderJit { .arg("-O") .arg(&source_path) .arg("-o") - .arg(&dylib_path) + .arg(&staging_path) .output() .map_err(|err| { format!("failed to run rustc for headless shader JIT `{crate_name}`: {err}") })?; if !output.status.success() { + let _ = std::fs::remove_file(&staging_path); let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!( "headless shader JIT compile failed for `{}`:\n{}", @@ -82,6 +112,13 @@ impl HeadlessShaderJit { )); } + std::fs::rename(&staging_path, &dylib_path).map_err(|err| { + format!( + "failed to publish headless shader dylib `{}`: {err}", + dylib_path.display() + ) + })?; + let mut load_error = None; let mut shader_version = None; let mut module = None; diff --git a/platform/src/os/headless/mod.rs b/platform/src/os/headless/mod.rs index 96b884415..a920362e9 100644 --- a/platform/src/os/headless/mod.rs +++ b/platform/src/os/headless/mod.rs @@ -69,6 +69,11 @@ pub struct CxOs { pub(crate) draw_cycles: Option, pub(crate) render_pool: Option>, pub(crate) render_pool_threads: usize, + /// BGRA -> RGBAf32 conversions of sampled textures, kept ACROSS frames. + /// Rebuilding this per frame re-converted the whole glyph atlas on every + /// draw, which cost more than rasterising the window did. Entries carry a + /// signature and are redone when the texture reports pending updates. + pub(crate) texture_conversions: crate::os::headless::raster::TextureConversionCache, } impl Default for CxOs { @@ -83,6 +88,7 @@ impl Default for CxOs { draw_cycles: None, render_pool: None, render_pool_threads: 0, + texture_conversions: Default::default(), } } } diff --git a/platform/src/os/headless/raster.rs b/platform/src/os/headless/raster.rs index ed3b7e335..18edeb715 100644 --- a/platform/src/os/headless/raster.rs +++ b/platform/src/os/headless/raster.rs @@ -123,12 +123,12 @@ struct TextureConversionSignature { data_len: usize, } -struct CachedTextureConversion { +pub(crate) struct CachedTextureConversion { signature: TextureConversionSignature, rgba: Vec, } -type TextureConversionCache = HashMap; +pub(crate) type TextureConversionCache = HashMap; fn headless_texture_info( texture_index: usize, @@ -329,6 +329,7 @@ struct RenderProfile { total_triangles: usize, vertex_ms: f64, raster_ms: f64, + texture_ms: f64, } #[allow(clippy::too_many_arguments)] @@ -617,7 +618,7 @@ impl Cx { self.headless_ensure_render_pool(render_threads); let mut results = Vec::new(); - let mut texture_cache = TextureConversionCache::new(); + let mut texture_cache = std::mem::take(&mut self.os.texture_conversions); for draw_pass_id in &passes_todo { self.passes[*draw_pass_id].paint_dirty = false; @@ -664,6 +665,9 @@ impl Cx { } } + // Hand the conversions back for the next frame to reuse. + self.os.texture_conversions = texture_cache; + let elapsed = frame_start.elapsed(); if profile_enabled { crate::log!( @@ -673,14 +677,15 @@ impl Cx { } if profile_enabled { crate::log!( - "[headless][profile] draws={} serial={} parallel={} inst={} tris={} vertex={:.1}ms raster={:.1}ms", + "[headless][profile] draws={} serial={} parallel={} inst={} tris={} vertex={:.1}ms raster={:.1}ms texture={:.1}ms", profile.draw_calls, profile.serial_draw_calls, profile.parallel_draw_calls, profile.total_instances, profile.total_triangles, profile.vertex_ms, - profile.raster_ms + profile.raster_ms, + profile.texture_ms ); } @@ -899,8 +904,12 @@ impl Cx { if let Some(texture) = &draw_call.texture_slots[tex_idx] { let texture_id = texture.texture_id(); let cxtexture = &self.textures[texture_id]; - if let Some(info) = - headless_texture_info(texture_id.0, cxtexture, texture_cache) + let __tex_t0 = std::time::Instant::now(); + let __info = headless_texture_info(texture_id.0, cxtexture, texture_cache); + if let Some(p) = profile.as_deref_mut() { + p.texture_ms += __tex_t0.elapsed().as_secs_f64() * 1000.0; + } + if let Some(info) = __info { tex_infos.push(info); } else { diff --git a/platform/src/os/linux/android/android.rs b/platform/src/os/linux/android/android.rs index 83e0fed79..4171b68c7 100644 --- a/platform/src/os/linux/android/android.rs +++ b/platform/src/os/linux/android/android.rs @@ -2396,7 +2396,7 @@ impl Cx { } fn handle_platform_ops(&mut self) -> EventFlow { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let window = &mut self.windows[window_id]; diff --git a/platform/src/os/linux/android/android_jni.rs b/platform/src/os/linux/android/android_jni.rs index c5a2b82e8..dd5afb70a 100644 --- a/platform/src/os/linux/android/android_jni.rs +++ b/platform/src/os/linux/android/android_jni.rs @@ -331,6 +331,7 @@ unsafe fn get_intent_string_extra( const MAKEPAD_PREFS_NAME: &str = "makepad"; const MAKEPAD_STUDIO_HOST_PREF_KEY: &str = "studio_host"; const MAKEPAD_STUDIO_CRATE_PREF_KEY: &str = "studio_crate"; +const MAKEPAD_STUDIO_BUILD_PREF_KEY: &str = "studio_build"; const ANDROID_MODE_PRIVATE: i32 = 0; unsafe fn new_jstring(env: *mut jni_sys::JNIEnv, value: &str) -> Option { @@ -453,6 +454,8 @@ pub unsafe fn apply_studio_env_from_activity(activity: *const std::ffi::c_void) .filter(|v| !v.trim().is_empty()); let intent_studio_crate = get_intent_string_extra(env, activity, "makepad.STUDIO_CRATE") .filter(|v| !v.trim().is_empty()); + let intent_studio_build = get_intent_string_extra(env, activity, "makepad.STUDIO_BUILD") + .filter(|v| !v.trim().is_empty()); if let Some(studio_host) = intent_studio_host { let _ = persist_string_pref(env, activity, MAKEPAD_STUDIO_HOST_PREF_KEY, &studio_host); @@ -471,6 +474,15 @@ pub unsafe fn apply_studio_env_from_activity(activity: *const std::ffi::c_void) { std::env::set_var("STUDIO_CRATE", &studio_crate); } + + if let Some(studio_build) = intent_studio_build { + let _ = persist_string_pref(env, activity, MAKEPAD_STUDIO_BUILD_PREF_KEY, &studio_build); + std::env::set_var("STUDIO_BUILD", &studio_build); + } else if let Some(studio_build) = + get_persisted_string_pref(env, activity, MAKEPAD_STUDIO_BUILD_PREF_KEY) + { + std::env::set_var("STUDIO_BUILD", &studio_build); + } } pub unsafe fn attach_jni_env() -> *mut jni_sys::JNIEnv { diff --git a/platform/src/os/linux/direct/linux_direct.rs b/platform/src/os/linux/direct/linux_direct.rs index 1695acd2b..9493137bd 100644 --- a/platform/src/os/linux/direct/linux_direct.rs +++ b/platform/src/os/linux/direct/linux_direct.rs @@ -284,7 +284,7 @@ impl Cx { } fn handle_platform_ops(&mut self, direct_app: &mut DirectApp) -> EventFlow { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let window = &mut self.windows[window_id]; diff --git a/platform/src/os/linux/mod.rs b/platform/src/os/linux/mod.rs index 97f0e1b51..fcb558c2e 100644 --- a/platform/src/os/linux/mod.rs +++ b/platform/src/os/linux/mod.rs @@ -28,6 +28,7 @@ pub mod egl_sys; #[macro_use] pub mod gl_sys; pub(crate) mod gl_video_upload; +#[cfg(not(any(target_env = "ohos", target_os = "android")))] pub(crate) mod va_dmabuf_modifier; pub mod libc_sys; pub mod module_loader; diff --git a/platform/src/os/linux/open_harmony/open_harmony.rs b/platform/src/os/linux/open_harmony/open_harmony.rs index d1361a3f5..a1fe0aeca 100644 --- a/platform/src/os/linux/open_harmony/open_harmony.rs +++ b/platform/src/os/linux/open_harmony/open_harmony.rs @@ -517,7 +517,7 @@ impl Cx { } fn handle_platform_ops(&mut self) -> EventFlow { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { //crate::log!("============ handle_platform_ops"); match op { CxOsOp::CreateWindow(window_id) => { diff --git a/platform/src/os/linux/openxr.rs b/platform/src/os/linux/openxr.rs index 49a586ad1..4ee9f2579 100644 --- a/platform/src/os/linux/openxr.rs +++ b/platform/src/os/linux/openxr.rs @@ -171,11 +171,14 @@ impl Cx { pub(crate) fn openxr_handle_repaint( &mut self, frame: &CxOpenXrFrame, + #[cfg_attr(not(use_vulkan), allow(unused_variables))] xr_cpu: &mut XrFrameCpuBreakdown, ) { //opengl_cx.make_current(); let mut passes_todo = Vec::new(); + #[cfg(use_vulkan)] let mut xr_render_cpu_ms = 0.0f64; + #[cfg(use_vulkan)] let mut saw_xr_vulkan_pass = false; self.compute_pass_repaint_order(&mut passes_todo); self.repaint_id += 1; @@ -240,7 +243,14 @@ impl Cx { } } } - self.os.xr_render_cpu_time_ms = saw_xr_vulkan_pass.then_some(xr_render_cpu_ms); + #[cfg(use_vulkan)] + { + self.os.xr_render_cpu_time_ms = saw_xr_vulkan_pass.then_some(xr_render_cpu_ms); + } + #[cfg(not(use_vulkan))] + { + self.os.xr_render_cpu_time_ms = None; + } } pub(crate) fn openxr_handle_drawing(&mut self) { diff --git a/platform/src/os/linux/wayland/linux_wayland.rs b/platform/src/os/linux/wayland/linux_wayland.rs index 27da0eb7f..ba6f5d1f2 100644 --- a/platform/src/os/linux/wayland/linux_wayland.rs +++ b/platform/src/os/linux/wayland/linux_wayland.rs @@ -621,7 +621,7 @@ impl WaylandCx { if cx.platform_ops.is_empty() { return EventFlow::Poll; } - while let Some(op) = cx.platform_ops.pop() { + while let Some(op) = cx.platform_ops.pop_front() { match op { CxOsOp::SetCursor(_) | CxOsOp::StartTimer { .. } | CxOsOp::StopTimer(_) => {} _ => { diff --git a/platform/src/os/linux/x11/linux_x11.rs b/platform/src/os/linux/x11/linux_x11.rs index a2a38a0e1..1d59c1012 100644 --- a/platform/src/os/linux/x11/linux_x11.rs +++ b/platform/src/os/linux/x11/linux_x11.rs @@ -545,7 +545,7 @@ impl X11Cx { ) -> EventFlow { let mut ret = EventFlow::Poll; let mut cx = self.cx.borrow_mut(); - while let Some(op) = cx.platform_ops.pop() { + while let Some(op) = cx.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let gl_cx = cx.os.opengl_cx.as_ref().unwrap(); diff --git a/platform/src/os/linux/x11/linux_x11_stdin.rs b/platform/src/os/linux/x11/linux_x11_stdin.rs index 6f83bb79a..91165dae5 100644 --- a/platform/src/os/linux/x11/linux_x11_stdin.rs +++ b/platform/src/os/linux/x11/linux_x11_stdin.rs @@ -543,7 +543,7 @@ impl Cx { } fn stdin_handle_platform_ops(&mut self, stdin_windows: &mut Vec) { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { while window_id.id() >= stdin_windows.len() { diff --git a/platform/src/os/web/web.rs b/platform/src/os/web/web.rs index b55060a25..cf2309d66 100644 --- a/platform/src/os/web/web.rs +++ b/platform/src/os/web/web.rs @@ -602,7 +602,7 @@ impl Cx { } fn handle_platform_ops(&mut self) { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let title = { diff --git a/platform/src/os/windows/d3d11_texture.rs b/platform/src/os/windows/d3d11_texture.rs new file mode 100644 index 000000000..5839f28b2 --- /dev/null +++ b/platform/src/os/windows/d3d11_texture.rs @@ -0,0 +1,64 @@ +//! Thin D3D11 texture helpers for cross-platform GPU texture code. +//! +//! COM method call sites live here so `windows_strip` (which scans +//! `os/windows/*.rs`) keeps the corresponding vendored methods. + +use windows::{ + core::Result as WinResult, + Win32::Graphics::Direct3D11::{ + ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11ShaderResourceView, + ID3D11Texture2D, D3D11_BOX, D3D11_SHADER_RESOURCE_VIEW_DESC, D3D11_SUBRESOURCE_DATA, + D3D11_TEXTURE2D_DESC, + }, +}; + +pub unsafe fn texture2d_get_desc(texture: &ID3D11Texture2D, out: &mut D3D11_TEXTURE2D_DESC) { + texture.GetDesc(out); +} + +pub unsafe fn copy_subresource_region( + context: &ID3D11DeviceContext, + dst: &ID3D11Resource, + dst_subresource: u32, + dst_x: u32, + dst_y: u32, + dst_z: u32, + src: &ID3D11Resource, + src_subresource: u32, + src_box: Option<*const D3D11_BOX>, +) { + context.CopySubresourceRegion( + dst, + dst_subresource, + dst_x, + dst_y, + dst_z, + src, + src_subresource, + src_box, + ); +} + +pub unsafe fn create_texture_2d( + device: &ID3D11Device, + desc: &D3D11_TEXTURE2D_DESC, + initial_data: Option<*const D3D11_SUBRESOURCE_DATA>, + texture_out: Option<*mut Option>, +) -> WinResult<()> { + device.CreateTexture2D(desc, initial_data, texture_out) +} + +pub unsafe fn create_shader_resource_view( + device: &ID3D11Device, + resource: &ID3D11Resource, + desc: Option<*const D3D11_SHADER_RESOURCE_VIEW_DESC>, + srv_out: Option<*mut Option>, +) -> WinResult<()> { + device.CreateShaderResourceView(resource, desc, srv_out) +} + +pub unsafe fn device_get_immediate_context( + device: &ID3D11Device, +) -> WinResult { + device.GetImmediateContext() +} diff --git a/platform/src/os/windows/mod.rs b/platform/src/os/windows/mod.rs index 40b66c6fa..1da0efe0b 100644 --- a/platform/src/os/windows/mod.rs +++ b/platform/src/os/windows/mod.rs @@ -20,6 +20,7 @@ pub mod winrt_midi; //pub mod com_sys; pub mod angle; pub mod d3d11; +pub mod d3d11_texture; pub mod windows; pub mod windows_game_input; pub mod windows_stdin; diff --git a/platform/src/os/windows/win32_app.rs b/platform/src/os/windows/win32_app.rs index 3ac44223e..f7bd30632 100644 --- a/platform/src/os/windows/win32_app.rs +++ b/platform/src/os/windows/win32_app.rs @@ -255,6 +255,7 @@ impl Win32App { hInstance: unsafe { GetModuleHandleW(None).unwrap().into() }, hIcon: hicon_big, hIconSm: hicon_small, + hCursor: unsafe { LoadCursorW(None, IDC_ARROW).unwrap_or_default() }, lpszClassName: PCWSTR(window_class_name.as_ptr()), hbrBackground: unsafe { CreateSolidBrush(COLORREF(0x3f3f3f3f)) }, ..Default::default() @@ -758,6 +759,13 @@ type SetProcessDpiAwareness = unsafe extern "system" fn(value: PROCESS_DPI_AWARE type SetProcessDpiAwarenessContext = unsafe extern "system" fn(value: DPI_AWARENESS_CONTEXT) -> BOOL; type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; +type AdjustWindowRectExForDpi = unsafe extern "system" fn( + lp_rect: *mut crate::windows::Win32::Foundation::RECT, + dw_style: u32, + b_menu: BOOL, + dw_ex_style: u32, + dpi: u32, +) -> BOOL; type GetDpiForMonitor = unsafe extern "system" fn( hmonitor: HMONITOR, dpi_type: MONITOR_DPI_TYPE, @@ -812,6 +820,7 @@ pub fn post_signal_to_hwnd(hwnd:HWND, signal:Signal){ */ pub struct DpiFunctions { get_dpi_for_window: Option, + adjust_window_rect_ex_for_dpi: Option, get_dpi_for_monitor: Option, enable_nonclient_dpi_scaling: Option, set_process_dpi_awareness_context: Option, @@ -825,6 +834,7 @@ impl DpiFunctions { fn new() -> DpiFunctions { DpiFunctions { get_dpi_for_window: get_function!("user32.dll", GetDpiForWindow), + adjust_window_rect_ex_for_dpi: get_function!("user32.dll", AdjustWindowRectExForDpi), get_dpi_for_monitor: get_function!("shcore.dll", GetDpiForMonitor), enable_nonclient_dpi_scaling: get_function!("user32.dll", EnableNonClientDpiScaling), set_process_dpi_awareness_context: get_function!( @@ -867,6 +877,36 @@ impl DpiFunctions { } } + /// DPI-aware frame insets for a zero client rect when available (Win10 1607+). + /// Falls back to `AdjustWindowRectEx` on older systems. + pub fn adjust_window_rect_ex( + &self, + hwnd: HWND, + style: u32, + ex_style: u32, + rect: &mut crate::windows::Win32::Foundation::RECT, + ) { + unsafe { + if let (Some(adjust), Some(get_dpi)) = ( + self.adjust_window_rect_ex_for_dpi, + self.get_dpi_for_window, + ) { + let dpi = match get_dpi(hwnd) { + 0 => BASE_DPI, + d => d, + }; + let _ = adjust(rect, style, FALSE, ex_style, dpi); + return; + } + let _ = crate::windows::Win32::UI::WindowsAndMessaging::AdjustWindowRectEx( + rect, + crate::windows::Win32::UI::WindowsAndMessaging::WINDOW_STYLE(style), + false, + crate::windows::Win32::UI::WindowsAndMessaging::WINDOW_EX_STYLE(ex_style), + ); + } + } + pub fn system_dpi_factor(&self) -> f32 { unsafe { let hdc = GetDC(None); diff --git a/platform/src/os/windows/win32_window.rs b/platform/src/os/windows/win32_window.rs index 1166e124e..3c339bf69 100644 --- a/platform/src/os/windows/win32_window.rs +++ b/platform/src/os/windows/win32_window.rs @@ -23,9 +23,11 @@ use { }, Graphics::{ Dwm::{ - DwmExtendFrameIntoClientArea, DwmSetWindowAttribute, DWMSBT_MAINWINDOW, - DWMSBT_NONE, DWMSBT_TABBEDWINDOW, DWMSBT_TRANSIENTWINDOW, - DWMWA_SYSTEMBACKDROP_TYPE, + DwmExtendFrameIntoClientArea, DwmSetWindowAttribute, DWMNCRP_ENABLED, + DWMSBT_MAINWINDOW, DWMSBT_NONE, DWMSBT_TABBEDWINDOW, + DWMSBT_TRANSIENTWINDOW, DWMWA_BORDER_COLOR, DWMWA_COLOR_NONE, + DWMWA_NCRENDERING_POLICY, DWMWA_SYSTEMBACKDROP_TYPE, + DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_ROUND, DWMWCP_ROUNDSMALL, }, Gdi::ScreenToClient, }, @@ -73,23 +75,23 @@ use { }, WindowsAndMessaging::{ CreateWindowExW, DefWindowProcW, DestroyWindow, GetClientRect, - GetWindowLongPtrW, GetWindowPlacement, GetWindowRect, MoveWindow, - PostMessageW, SetLayeredWindowAttributes, SetWindowLongPtrW, SetWindowPos, - ShowWindow, CW_USEDEFAULT, GWLP_USERDATA, GWL_EXSTYLE, HTBOTTOM, + GetWindowLongPtrW, GetWindowRect, MoveWindow, PostMessageW, + SetLayeredWindowAttributes, SetWindowLongPtrW, SetWindowPos, ShowWindow, + CW_USEDEFAULT, GWLP_USERDATA, GWL_EXSTYLE, GWL_STYLE, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTCLIENT, HTLEFT, HTRIGHT, HTSYSMENU, HTTOP, HTTOPLEFT, HTTOPRIGHT, HWND_NOTOPMOST, HWND_TOPMOST, - LWA_ALPHA, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, - SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, - SW_SHOW, WA_ACTIVE, WINDOWPLACEMENT, WM_ACTIVATE, WM_CHAR, WM_CLOSE, - WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_ERASEBKGND, - WM_EXITSIZEMOVE, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, - WM_IME_STARTCOMPOSITION, WM_KEYDOWN, WM_KEYUP, + LWA_ALPHA, NCCALCSIZE_PARAMS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, + SWP_NOSIZE, SWP_NOZORDER, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, + WA_ACTIVE, WM_ACTIVATE, WM_CHAR, WM_CLOSE, WM_DESTROY, WM_DPICHANGED, + WM_ENTERSIZEMOVE, WM_ERASEBKGND, WM_EXITSIZEMOVE, WM_IME_COMPOSITION, + WM_IME_ENDCOMPOSITION, WM_IME_STARTCOMPOSITION, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCCALCSIZE, WM_NCHITTEST, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SIZE, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP, - WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, - WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_WINDOWEDGE, - WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, + WS_BORDER, WS_CAPTION, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, + WS_EX_APPWINDOW, WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, + WS_EX_WINDOWEDGE, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_THICKFRAME, + WINDOW_EX_STYLE, WINDOW_STYLE, }, }, }, @@ -210,6 +212,284 @@ pub struct Win32Window { } impl Win32Window { + /// Opt into Win11 rounded corners for overlapped custom-chrome windows. + /// No-ops on older Windows (DwmSetWindowAttribute returns an error). + fn apply_win11_window_shape(hwnd: HWND, small_radius: bool) { + let preference = if small_radius { + DWMWCP_ROUNDSMALL + } else { + DWMWCP_ROUND + }; + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + DWMWA_WINDOW_CORNER_PREFERENCE, + &preference as *const _ as *const c_void, + std::mem::size_of_val(&preference) as u32, + ); + let border = DWMWA_COLOR_NONE; + let _ = DwmSetWindowAttribute( + hwnd, + DWMWA_BORDER_COLOR, + &border as *const _ as *const c_void, + std::mem::size_of_val(&border) as u32, + ); + } + } + + fn set_nc_rendering_enabled(hwnd: HWND) { + let policy = DWMNCRP_ENABLED; + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + DWMWA_NCRENDERING_POLICY, + &policy as *const _ as *const c_void, + std::mem::size_of_val(&policy) as u32, + ); + } + } + + fn get_style(&self) -> WINDOW_STYLE { + unsafe { WINDOW_STYLE(GetWindowLongPtrW(self.hwnd, GWL_STYLE) as u32) } + } + + fn get_ex_style(&self) -> WINDOW_EX_STYLE { + unsafe { WINDOW_EX_STYLE(GetWindowLongPtrW(self.hwnd, GWL_EXSTYLE) as u32) } + } + + /// Frame insets via DPI-aware `AdjustWindowRectEx*` on a zero client rect. + /// `left`/`top` are negative; `right`/`bottom` are positive. + fn frame_border_thickness(&self, style: WINDOW_STYLE, ex_style: WINDOW_EX_STYLE) -> RECT { + let mut thickness = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + with_win32_app(|app| { + app.dpi_functions.adjust_window_rect_ex( + self.hwnd, + style.0, + ex_style.0, + &mut thickness, + ); + }); + thickness + } + + /// Non-client insets for custom chrome. + /// + /// Restored (non-maximized) mains use a **fully client-sized** frame: no + /// thickframe strip outside the swap chain. That strip was showing as a + /// light/white edge while resizing because D3D only paints the client. + /// Resize is emulated via `WM_NCHITTEST` `HT*` returns instead. + /// + /// Maximized windows still keep border+thickframe insets so the client + /// matches the monitor work area. + fn extended_client_border_thickness(&self) -> RECT { + let style = self.get_style(); + let ex_style = self.get_ex_style(); + if (style.0 & WS_CAPTION.0) == WS_CAPTION.0 && self.get_is_maximized() { + // Caption is drawn into the client; keep only border+thickframe for work-area. + self.frame_border_thickness( + WINDOW_STYLE((style.0 & !WS_CAPTION.0) | WS_BORDER.0 | WS_THICKFRAME.0), + ex_style, + ) + } else { + RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + } + } + } + + /// Expand the client for custom chrome. Mutates `NCCALCSIZE_PARAMS.rgrc[0]`. + unsafe fn apply_extended_client_nccalcsize(&self, lparam: LPARAM) { + let params = &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS); + let rect = &mut params.rgrc[0]; + let border = self.extended_client_border_thickness(); + + // `rgrc[0]` arrives as the proposed *window* rect. Subtracting + // `AdjustWindowRectEx` insets converts window → client. + rect.left -= border.left; + rect.top -= border.top; + rect.right -= border.right; + rect.bottom -= border.bottom; + } + + fn extend_frame_for_custom_chrome(&self) { + // Opaque custom chrome: zero margins (no 1px glass hairline). DWM still + // paints NC shadows/corners because NCRP is ENABLED. + let margins = MARGINS { + cxLeftWidth: 0, + cxRightWidth: 0, + cyTopHeight: 0, + cyBottomHeight: 0, + }; + unsafe { + let _ = DwmExtendFrameIntoClientArea(self.hwnd, &margins); + } + Self::set_nc_rendering_enabled(self.hwnd); + Self::apply_win11_window_shape(self.hwnd, self.is_popup); + } + + /// Physical-pixel frame insets (left, top, right, bottom) for converting + /// window rect <-> client size under custom chrome. + fn client_frame_insets_px(&self) -> (i32, i32, i32, i32) { + let border = self.extended_client_border_thickness(); + (-border.left, -border.top, border.right, border.bottom) + } + + /// Force a `WM_NCCALCSIZE` pass now that `GWLP_USERDATA` is set so our + /// extended-client handler runs (CreateWindow still used DefWindowProc). + fn force_frame_change(&self) { + unsafe { + let _ = SetWindowPos( + self.hwnd, + None, + 0, + 0, + 0, + 0, + SWP_NOMOVE + | SWP_NOSIZE + | SWP_NOZORDER + | SWP_NOACTIVATE + | SWP_FRAMECHANGED, + ); + } + } + + /// Logical-pixel resize hit band for fully-client custom chrome / popups. + fn resize_edge_logical(&self) -> f64 { + // Thick enough to grab reliably; not so thick it steals caption clicks. + const EDGE: f64 = 8.0; + EDGE + } + + /// Same geometry as `WindowGeom.window_chrome_buttons` (logical, client-relative). + fn chrome_buttons_rect_logical(&self) -> Rect { + const BUTTON_W: f64 = 46.0; + const BUTTON_H: f64 = 29.0; + const BUTTON_COUNT: f64 = 3.0; + let inner = self.get_inner_size(); + Rect { + pos: dvec2(inner.x - BUTTON_W * BUTTON_COUNT, 0.0), + size: dvec2(BUTTON_W * BUTTON_COUNT, BUTTON_H), + } + } + + /// `HTTOP` / `HTLEFT` / … for a fully client-sized window. Returns `None` + /// when the cursor is outside the resize band, over caption buttons, or + /// when maximized. + fn hit_test_client_resize_edge(&self, lparam: LPARAM) -> Option { + if self.get_is_maximized() { + return None; + } + let dpi = self.get_dpi_factor(); + let edge = self.resize_edge_logical(); + let abs = self.get_mouse_pos_from_lparam(lparam); + let mut window_rect = RECT { + left: 0, + top: 0, + bottom: 0, + right: 0, + }; + unsafe { + GetWindowRect(self.hwnd, &mut window_rect).unwrap(); + } + let origin = dvec2(window_rect.left as f64 / dpi, window_rect.top as f64 / dpi); + let size = dvec2( + (window_rect.right - window_rect.left) as f64 / dpi, + (window_rect.bottom - window_rect.top) as f64 / dpi, + ); + let local = abs - origin; + + // Don't steal hits from the system-style caption buttons (close/max/min). + if !self.is_popup && self.chrome_buttons_rect_logical().contains(local) { + return None; + } + + let on_left = abs.x < origin.x + edge; + let on_right = abs.x > origin.x + size.x - edge; + let on_top = abs.y < origin.y + edge; + let on_bottom = abs.y > origin.y + size.y - edge; + + let hit = match (on_left, on_right, on_top, on_bottom) { + (true, _, true, _) => HTTOPLEFT, + (true, _, _, true) => HTBOTTOMLEFT, + (_, true, true, _) => HTTOPRIGHT, + (_, true, _, true) => HTBOTTOMRIGHT, + (true, _, _, _) => HTLEFT, + (_, true, _, _) => HTRIGHT, + (_, _, true, _) => HTTOP, + (_, _, _, true) => HTBOTTOM, + _ => return None, + }; + Some(LRESULT(hit as isize)) + } + + /// Caption / client hit-test for the custom-chrome client area. + fn hit_test_extended_client(&mut self, lparam: LPARAM) -> LRESULT { + let dpi = self.get_dpi_factor(); + let mut window_rect = RECT { + left: 0, + top: 0, + bottom: 0, + right: 0, + }; + unsafe { + GetWindowRect(self.hwnd, &mut window_rect).unwrap(); + } + let origin = dvec2(window_rect.left as f64 / dpi, window_rect.top as f64 / dpi); + + // Dedupe: return the cached WindowDragQuery result for a repeated cursor + // position (the loop is vsync-paced, so the OS sends several same-position + // hit-tests/frame). + let response_val = match self.nc_dq_cache.get() { + Some((lp, rv)) if lp == lparam.0 => rv, + _ => { + // Snapshot the cache generation: dispatching WindowDragQuery can + // reenter the window proc (nested SendMessage) and invalidate the + // cache mid-flight; if it does, we must NOT write our now-stale + // result back over that invalidation. + let gen = self.nc_dq_gen.get(); + let response = Rc::new(Cell::new(WindowDragQueryResponse::NoAnswer)); + self.do_callback(Win32Event::WindowDragQuery(WindowDragQueryEvent { + window_id: self.window_id, + abs: self.get_mouse_pos_from_lparam(lparam) - origin, + response: response.clone(), + })); + let rv = response.get(); + if self.nc_dq_gen.get() == gen { + self.nc_dq_cache.set(Some((lparam.0, rv))); + } + rv + } + }; + match response_val { + WindowDragQueryResponse::Caption => { + with_win32_app(|app| app.set_mouse_cursor(MouseCursor::Default)); + LRESULT(HTCAPTION as isize) + } + WindowDragQueryResponse::SysMenu => { + with_win32_app(|app| app.set_mouse_cursor(MouseCursor::Default)); + LRESULT(HTSYSMENU as isize) + } + WindowDragQueryResponse::Client | WindowDragQueryResponse::NoAnswer => { + // Caption already restores Default above. Client must too: after + // HTLEFT/HTRIGHT the system size cursor sticks otherwise, and the + // window class cursor alone is not enough once we cleared + // `current_cursor` on the resize edge. Widgets re-apply Text/etc. + // on the following WM_MOUSEMOVE. + with_win32_app(|app| app.set_mouse_cursor(MouseCursor::Default)); + LRESULT(HTCLIENT as isize) + } + } + } + // 2-stage initialization (new and init) to connect GWLP_USERDATA // create window structure and register drag/drop @@ -221,13 +501,9 @@ impl Win32Window { ) -> Win32Window { let title = encode_wide(title); - let style = WS_SIZEBOX - | WS_MAXIMIZEBOX - | WS_MINIMIZEBOX - | WS_POPUP - | WS_CLIPSIBLINGS - | WS_CLIPCHILDREN - | WS_SYSMENU; + // Overlapped top-level window with app-drawn chrome. Restored size is + // fully client-sized (WM_NCCALCSIZE); maximize keeps work-area insets. + let style = WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS | WS_CLIPCHILDREN; let style_ex = WS_EX_WINDOWEDGE | WS_EX_APPWINDOW | WS_EX_ACCEPTFILES; @@ -254,6 +530,10 @@ impl Win32Window { ) .unwrap() }; + // DWM chrome is applied in `init` after USERDATA is set (so NCCALCSIZE + // can use our handler). Shape/NCRP here only covers the CreateWindow gap. + Self::apply_win11_window_shape(hwnd, false); + Self::set_nc_rendering_enabled(hwnd); // create DropTarget object that accesses the same data object, convert to COM and give to Microsoft let drop_target: IDropTarget = DropTarget { @@ -314,6 +594,7 @@ impl Win32Window { ) .unwrap() }; + Self::apply_win11_window_shape(hwnd, true); Win32Window { window_id, @@ -337,13 +618,22 @@ impl Win32Window { } } - // initialize GWLP_USERDATA and registration of global stuff, and set outer size + // initialize GWLP_USERDATA and registration of global stuff, then set inner size pub fn init(&mut self, size: Vec2d) { unsafe { SetWindowLongPtrW(self.hwnd, GWLP_USERDATA, self as *const _ as isize) }; with_win32_app(|app| app.dpi_functions.enable_non_client_dpi_scaling(self.hwnd)); with_win32_app(|app| app.all_windows.push(self.hwnd)); - self.set_outer_size(size); + + if !self.is_popup { + // CreateWindow ran NCCALCSIZE via DefWindowProc (no USERDATA yet). + // Apply DWM chrome, then force our extended-client frame before sizing. + self.extend_frame_for_custom_chrome(); + self.force_frame_change(); + } + + // `size` is the app's desired client (inner) size (`create_inner_size`). + self.set_inner_size(size); if self.is_fullscreen { self.maximize(); } @@ -412,111 +702,29 @@ impl Win32Window { } } WM_NCCALCSIZE => { - // check if we are maximised - if window.get_is_maximized() { - return DefWindowProcW(hwnd, msg, wparam, lparam); - } if wparam == WPARAM(1) { - let margins = MARGINS { - cxLeftWidth: 0, - cxRightWidth: 0, - cyTopHeight: 0, - cyBottomHeight: 1, - }; - DwmExtendFrameIntoClientArea(hwnd, &margins).unwrap(); + if window.is_popup { + // Popups stay fully client-sized. + return LRESULT(0); + } + // Custom chrome: restored = fully client-sized; maximized keeps + // work-area insets. DWM extend/shape is done in init / visuals. + unsafe { + window.apply_extended_client_nccalcsize(lparam); + } return LRESULT(0); } } WM_NCHITTEST => { - let abs = window.get_mouse_pos_from_lparam(lparam); - let mut rect = RECT { - left: 0, - top: 0, - bottom: 0, - right: 0, - }; - const EDGE: f64 = 4.0; - // WM_NCHITTEST is OS-sent on every mouse move (uncoalesced); `get_dpi_factor()` is - // now cached so this (and the two calls inside `get_mouse_pos_from_lparam`) no - // longer syscall `GetDeviceCaps` per move. - let dpi = window.get_dpi_factor(); - GetWindowRect(hwnd, &mut rect).unwrap(); - let rect = Rect { - pos: dvec2(rect.left as f64 / dpi, rect.top as f64 / dpi), - size: dvec2( - (rect.right - rect.left) as f64 / dpi, - (rect.bottom - rect.top) as f64 / dpi, - ), - }; - if abs.x < rect.pos.x + EDGE { - if abs.y < rect.pos.y + EDGE { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NwseResize)); - return LRESULT(HTTOPLEFT as isize); - } - if abs.y > rect.pos.y + rect.size.y - EDGE { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NeswResize)); - return LRESULT(HTBOTTOMLEFT as isize); - } - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::EwResize)); - return LRESULT(HTLEFT as isize); + // Fully-client custom chrome (restored mains + popups): emulate + // resize borders with HT* hits. Returning system sizing codes + // still starts a resize drag; WM_SETCURSOR owns the cursor so we + // only clear our cached cursor (avoids stuck resize arrows). + if let Some(resize_hit) = window.hit_test_client_resize_edge(lparam) { + with_win32_app(|app| app.current_cursor = None); + return resize_hit; } - if abs.x > rect.pos.x + rect.size.x - EDGE { - if abs.y < rect.pos.y + EDGE { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NeswResize)); - return LRESULT(HTTOPRIGHT as isize); - } - if abs.y > rect.pos.y + rect.size.y - EDGE { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NwseResize)); - return LRESULT(HTBOTTOMRIGHT as isize); - } - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::EwResize)); - return LRESULT(HTRIGHT as isize); - } - if abs.y < rect.pos.y + EDGE { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NsResize)); - return LRESULT(HTTOP as isize); - } - if abs.y > rect.pos.y + rect.size.y - EDGE { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NsResize)); - return LRESULT(HTBOTTOM as isize); - } - // Dedupe: return the cached WindowDragQuery result for a repeated cursor position - // (the loop is vsync-paced, so the OS sends several same-position hit-tests/frame). - let response_val = match window.nc_dq_cache.get() { - Some((lp, rv)) if lp == lparam.0 => rv, - _ => { - // Snapshot the cache generation: dispatching WindowDragQuery can reenter the - // window proc (nested SendMessage) and invalidate the cache mid-flight; if it - // does, we must NOT write our now-stale result back over that invalidation. - let gen = window.nc_dq_gen.get(); - let response = Rc::new(Cell::new(WindowDragQueryResponse::NoAnswer)); - window.do_callback(Win32Event::WindowDragQuery(WindowDragQueryEvent { - window_id: window.window_id, - abs: window.get_mouse_pos_from_lparam(lparam) - rect.pos, - response: response.clone(), - })); - let rv = response.get(); - if window.nc_dq_gen.get() == gen { - window.nc_dq_cache.set(Some((lparam.0, rv))); - } - rv - } - }; - match response_val { - WindowDragQueryResponse::Client => { - return LRESULT(HTCLIENT as isize); - } - WindowDragQueryResponse::Caption => { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::Default)); - return LRESULT(HTCAPTION as isize); - } - WindowDragQueryResponse::SysMenu => { - with_win32_app(|app| app.set_mouse_cursor(MouseCursor::Default)); - return LRESULT(HTSYSMENU as isize); - } - _ => (), - } - return LRESULT(HTCLIENT as isize); + return window.hit_test_extended_client(lparam); } WM_ERASEBKGND => return LRESULT(1), WM_MOUSEMOVE => { @@ -1093,11 +1301,7 @@ impl Win32Window { const BUTTON_H: f64 = 29.0; const BUTTON_COUNT: f64 = 3.0; const BUTTONS_W: f64 = BUTTON_W * BUTTON_COUNT; - let inner_size = if self.get_is_maximized() { - self.get_outer_size() - } else { - self.get_inner_size() - }; + let inner_size = self.get_inner_size(); WindowGeom { xr_is_presenting: false, can_fullscreen: false, @@ -1122,16 +1326,10 @@ impl Win32Window { } pub fn get_is_maximized(&self) -> bool { - unsafe { - let wp: mem::MaybeUninit = mem::MaybeUninit::uninit(); - let mut wp = wp.assume_init(); - wp.length = mem::size_of::() as u32; - GetWindowPlacement(self.hwnd, &mut wp).unwrap(); - if wp.showCmd == SW_MAXIMIZE.0 as u32 { - return true; - } - return false; - } + // Prefer the live WS_MAXIMIZE style bit — more accurate during + // WM_NCCALCSIZE than WINDOWPLACEMENT while maximize/restore is in flight. + const WS_MAXIMIZE: u32 = 0x0100_0000; + (self.get_style().0 & WS_MAXIMIZE) != 0 } pub fn time_now(&self) -> f64 { @@ -1280,6 +1478,13 @@ impl Win32Window { DwmExtendFrameIntoClientArea(self.hwnd, &margins).unwrap(); } + if !self.is_popup { + Self::set_nc_rendering_enabled(self.hwnd); + Self::apply_win11_window_shape(self.hwnd, false); + } else { + Self::apply_win11_window_shape(self.hwnd, true); + } + let hr = unsafe { DwmSetWindowAttribute( self.hwnd, @@ -1324,24 +1529,17 @@ impl Win32Window { right: 0, }; GetWindowRect(self.hwnd, &mut window_rect).unwrap(); - let mut client_rect = RECT { - left: 0, - top: 0, - bottom: 0, - right: 0, - }; - GetClientRect(self.hwnd, &mut client_rect).unwrap(); let dpi = self.get_dpi_factor(); + // Use the same inset model as WM_NCCALCSIZE / send_sizing_event so + // we do not depend on the current client rect (which may still be + // DefWindowProc-sized before the first FRAMECHANGED). + let (l, t, r, b) = self.client_frame_insets_px(); MoveWindow( self.hwnd, window_rect.left, window_rect.top, - (size.x * dpi) as i32 - + ((window_rect.right - window_rect.left) - - (client_rect.right - client_rect.left)), - (size.y * dpi) as i32 - + ((window_rect.bottom - window_rect.top) - - (client_rect.bottom - client_rect.top)), + (size.x * dpi) as i32 + l + r, + (size.y * dpi) as i32 + t + b, false, ) .unwrap(); @@ -1392,15 +1590,19 @@ impl Win32Window { /// the empty-edge gap that appears when growing the window. pub fn send_sizing_event(&mut self, proposed_rect: &RECT) { let dpi = self.get_dpi_factor(); - let proposed_size = Vec2d { + let outer_size = Vec2d { x: (proposed_rect.right - proposed_rect.left) as f64 / dpi, y: (proposed_rect.bottom - proposed_rect.top) as f64 / dpi, }; + let (l, t, r, b) = self.client_frame_insets_px(); + let inner_size = Vec2d { + x: ((proposed_rect.right - proposed_rect.left) - l - r).max(0) as f64 / dpi, + y: ((proposed_rect.bottom - proposed_rect.top) - t - b).max(0) as f64 / dpi, + }; let mut new_geom = self.last_window_geom.clone(); - // For custom chrome, inner size == outer size. - new_geom.inner_size = proposed_size; - new_geom.outer_size = proposed_size; + new_geom.inner_size = inner_size; + new_geom.outer_size = outer_size; new_geom.position = Vec2d { x: proposed_rect.left as f64, y: proposed_rect.top as f64, @@ -1411,7 +1613,7 @@ impl Win32Window { return; // Size didn't change (e.g. just a move), nothing to pre-render. } // Skip degenerate sizes — ResizeBuffers rejects zero dimensions. - if proposed_size.x < 1.0 || proposed_size.y < 1.0 { + if inner_size.x < 1.0 || inner_size.y < 1.0 { return; } self.last_window_geom = new_geom.clone(); diff --git a/platform/src/os/windows/windows.rs b/platform/src/os/windows/windows.rs index 7148dc0f0..efcc85354 100644 --- a/platform/src/os/windows/windows.rs +++ b/platform/src/os/windows/windows.rs @@ -559,7 +559,7 @@ impl Cx { ) -> EventFlow { let mut ret = EventFlow::Poll; let mut geom_changes = Vec::new(); - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { let window = &mut self.windows[window_id]; @@ -694,9 +694,10 @@ impl Cx { CxOsOp::Quit => ret = EventFlow::Exit, CxOsOp::SetTopmost(window_id, is_topmost) => { if d3d11_windows.len() == 0 { - self.platform_ops - .insert(0, CxOsOp::SetTopmost(window_id, is_topmost)); - continue; + if self.defer_platform_op(CxOsOp::SetTopmost(window_id, is_topmost)) { + continue; + } + break; } if let Some(window) = d3d11_windows.iter_mut().find(|w| w.window_id == window_id) diff --git a/platform/src/os/windows/windows_stdin.rs b/platform/src/os/windows/windows_stdin.rs index fe7ec8032..8a3bcbc04 100644 --- a/platform/src/os/windows/windows_stdin.rs +++ b/platform/src/os/windows/windows_stdin.rs @@ -367,7 +367,7 @@ impl Cx { } fn stdin_handle_platform_ops(&mut self, stdin_windows: &mut Vec) { - while let Some(op) = self.platform_ops.pop() { + while let Some(op) = self.platform_ops.pop_front() { match op { CxOsOp::CreateWindow(window_id) => { while window_id.id() >= stdin_windows.len() { diff --git a/platform/src/window.rs b/platform/src/window.rs index 8e6e9a9d4..8ccb22fd2 100644 --- a/platform/src/window.rs +++ b/platform/src/window.rs @@ -343,7 +343,7 @@ impl WindowHandle { cxwindow.popup_size = None; cxwindow.popup_grab_keyboard = true; cx.platform_ops - .push(CxOsOp::CreateWindow(window.window_id())); + .push_back(CxOsOp::CreateWindow(window.window_id())); window } @@ -370,7 +370,7 @@ impl WindowHandle { cxwindow.popup_grab_keyboard = true; cxwindow.popup_grab_keyboard }; - cx.platform_ops.push(CxOsOp::CreatePopupWindow { + cx.platform_ops.push_back(CxOsOp::CreatePopupWindow { window_id, parent_window_id: parent, position, @@ -1220,4 +1220,17 @@ mod tests { assert_eq!(cx.platform_ops.len(), 1); assert!(matches!(cx.platform_ops[0], CxOsOp::SetTopmost(_, true))); } + + #[test] + fn create_window_then_set_topmost_queues_fifo() { + let mut cx = test_cx(); + let mut window = WindowHandle::new(&mut cx); + window.set_topmost(&mut cx, true); + + let first = cx.platform_ops.pop_front().unwrap(); + let second = cx.platform_ops.pop_front().unwrap(); + assert!(matches!(first, CxOsOp::CreateWindow(_))); + assert!(matches!(second, CxOsOp::SetTopmost(_, true))); + assert!(cx.platform_ops.is_empty()); + } } diff --git a/studio/hub/Cargo.toml b/studio/hub/Cargo.toml index 900af0548..620789b69 100644 --- a/studio/hub/Cargo.toml +++ b/studio/hub/Cargo.toml @@ -5,6 +5,10 @@ edition = "2021" description = "Studio2 hub (protocol + gateway + virtual fs)" license = "MIT OR Apache-2.0" +[[bin]] +name = "hub-server" +path = "src/bin/hub_server.rs" + [dependencies] makepad-script-std = { path = "../../platform/script/std", version = "1.0.0" } makepad-studio-protocol = { path = "../../platform/studio", version = "0.1.0" } diff --git a/studio/hub/src/bin/hub_server.rs b/studio/hub/src/bin/hub_server.rs new file mode 100644 index 000000000..f222d918b --- /dev/null +++ b/studio/hub/src/bin/hub_server.rs @@ -0,0 +1,38 @@ +use makepad_studio_hub::{HubConfig, MountConfig, StudioHub}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; + +fn main() { + let port: u16 = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(8001); + + let listen_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); + + let mount_path = std::env::args() + .nth(2) + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap()); + + println!("[hub-server] Listening on {}", listen_address); + println!("[hub-server] Mount path: {}", mount_path.display()); + + let _handle = StudioHub::start_headless(HubConfig { + listen_address, + mounts: vec![MountConfig { + name: "makepad".into(), + path: mount_path, + }], + enable_in_process_gateway: true, + ..Default::default() + }) + .expect("Failed to start hub"); + + println!("[hub-server] Hub started successfully on {}", listen_address); + println!("[hub-server] Waiting for connections..."); + + loop { + std::thread::sleep(std::time::Duration::from_secs(3600)); + } +} diff --git a/studio/hub/src/gateway.rs b/studio/hub/src/gateway.rs index b0cc131ce..1e2cc010d 100644 --- a/studio/hub/src/gateway.rs +++ b/studio/hub/src/gateway.rs @@ -1,7 +1,8 @@ use crate::dispatch::HubEvent; use makepad_micro_serde::SerBin; use makepad_script_std::makepad_network::{ - start_http_server, HttpServer, HttpServerRequest, HttpServerResponse, ToUISender, + start_http_server, HttpServer, HttpServerHandle, HttpServerRequest, HttpServerResponse, + ToUISender, }; use makepad_studio_protocol::hub_protocol::{HubToClient, QueryId}; use std::collections::HashMap; @@ -19,8 +20,21 @@ enum SocketRole { pub struct GatewayHandle { pub listen_address: SocketAddr, - pub request_thread: JoinHandle<()>, - pub http_thread: JoinHandle<()>, + pub request_thread: Option>, + pub http_thread: Option>, + http_shutdown: Sender<()>, +} + +impl Drop for GatewayHandle { + fn drop(&mut self) { + // Stop the accept loop so the listen port is released. Dropping the + // request channel senders then makes the request thread exit, which + // drops its hub event sender and lets the hub core shut down too. + let _ = self.http_shutdown.send(()); + if let Some(http_thread) = self.http_thread.take() { + let _ = http_thread.join(); + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -39,7 +53,10 @@ pub fn start_http_gateway( event_tx: Sender, ) -> Result { let (request_tx, request_rx) = mpsc::channel::(); - let http_thread = start_http_server(HttpServer { + let HttpServerHandle { + thread: http_thread, + shutdown: http_shutdown, + } = start_http_server(HttpServer { listen_address, request: request_tx, post_max_size, @@ -218,8 +235,9 @@ pub fn start_http_gateway( Ok(GatewayHandle { listen_address, - request_thread, - http_thread, + request_thread: Some(request_thread), + http_thread: Some(http_thread), + http_shutdown, }) } diff --git a/tools/cargo_makepad/src/android/compile.rs b/tools/cargo_makepad/src/android/compile.rs index 1b16ed782..51576406d 100644 --- a/tools/cargo_makepad/src/android/compile.rs +++ b/tools/cargo_makepad/src/android/compile.rs @@ -753,8 +753,8 @@ fn rust_build( /// Builds the `RUSTFLAGS` value for an Android `cargo rustc` invocation. /// -/// `prefer_dynamic`: APK/dev builds pass `true` (dynamically linked `std` -/// keeps incremental relinks fast). AAB builds pass `false` — `prefer-dynamic` +/// `prefer_dynamic`: debug builds pass `true` (dynamically linked `std` keeps +/// incremental relinks fast). Release and AAB builds pass `false` — `prefer-dynamic` /// makes Rust ship `std` as a separate `libstd-.so`, and the toolchain's /// prebuilt copy of that library is only 4 KB-page aligned, which fails Google /// Play's 16 KB page-size requirement for apps targeting Android 15+. Static @@ -2754,8 +2754,9 @@ pub fn build( android_targets, variant, urls, - // APK/dev builds keep `-C prefer-dynamic` for faster incremental relinks. - true, + // Only debug builds keep `-C prefer-dynamic`, for faster incremental + // relinks. Nobody ships a debug apk, and anything else might be shipped. + get_profile_from_args(args) == "debug", )?; // For APK builds, debuggable matches the cargo profile: release -> false, // anything else -> true (matches the historical behavior of `cargo makepad diff --git a/tools/web_server/src/main.rs b/tools/web_server/src/main.rs index 80c866150..6250e1565 100644 --- a/tools/web_server/src/main.rs +++ b/tools/web_server/src/main.rs @@ -24,7 +24,7 @@ fn main() { } let root_path = args[1].clone(); - net.start_http_server(HttpServer{ + let _http_server = net.start_http_server(HttpServer{ listen_address:addr, post_max_size: 1024*1024, request: tx_request diff --git a/widgets/Cargo.toml b/widgets/Cargo.toml index cf7db5b25..0cbd97070 100644 --- a/widgets/Cargo.toml +++ b/widgets/Cargo.toml @@ -18,6 +18,11 @@ i_overlay = { path = "../libs/i_overlay", version = "7.0.3", optional = true, de makepad-fast-inflate = { path = "../libs/fast_inflate", optional = true } makepad-voice = { path = "../libs/voice", version = "0.1.0", optional = true } makepad-cef = { path = "../libs/cef", optional = true } +# Public optional sibling crates. These are re-exported from lib.rs so an +# application can depend on makepad-widgets as its sole Makepad source. +makepad-gltf = { path = "../libs/gltf", optional = true } +makepad-csg = { path = "../libs/csg/csg", optional = true } +makepad-test = { path = "../libs/makepad_test", optional = true } makepad-html = { path = "../libs/html", version = "1.0.0" } unicode-segmentation = { version = "1.12.0", path = "../libs/unicode/unicode-segmentation" } @@ -33,6 +38,9 @@ default = [] voice = ["dep:makepad-voice"] maps = ["dep:makepad-mbtile-reader", "dep:makepad-fast-inflate", "dep:i_overlay"] +gltf = ["dep:makepad-gltf"] +csg = ["dep:makepad-csg"] +test = ["dep:makepad-test"] pdf = ["dep:makepad-pdf-parse"] cef = ["dep:makepad-cef"] diff --git a/widgets/src/lib.rs b/widgets/src/lib.rs index 641589d83..743954573 100644 --- a/widgets/src/lib.rs +++ b/widgets/src/lib.rs @@ -15,6 +15,20 @@ pub use makepad_pdf_parse; pub use makepad_draw::makepad_zune_jpeg; pub use makepad_draw::makepad_zune_png; +// Optional sibling Makepad workspace crates. These re-exports permit a +// downstream application to depend on makepad-widgets as the single Makepad +// source while keeping all extra APIs feature-gated. +#[cfg(feature = "maps")] +pub use makepad_fast_inflate; +#[cfg(feature = "maps")] +pub use makepad_mbtile_reader; +#[cfg(feature = "gltf")] +pub use makepad_gltf; +#[cfg(feature = "csg")] +pub use makepad_csg; +#[cfg(feature = "test")] +pub use makepad_test; + // Core modules (used internally first) pub mod animator; pub mod theme_desktop_dark; diff --git a/widgets/src/video.rs b/widgets/src/video.rs index a5a71ca93..05dbe1f77 100644 --- a/widgets/src/video.rs +++ b/widgets/src/video.rs @@ -2124,11 +2124,11 @@ impl Video { } } PlaybackState::Completed => { - // platform_ops is LIFO (`pop`). Transport ops are coalesced, but - // seek is separate — push resume *before* seek so drain order is - // seek → resume (not resume-from-EOS then seek). - cx.resume_video_playback(self.id); + // Seek is not coalesced with transport ops. Queue seek then + // resume so FIFO drain is seek → resume (not resume-from-EOS + // then seek). cx.seek_video_playback(self.id, 0); + cx.resume_video_playback(self.id); self.current_position_ms = 0; self.seek_cooldown = 5; self.playback_state = PlaybackState::Playing;