Compare commits

...

10 commits

Author SHA1 Message Date
ec2c0aaf15 feat(widgets): reexport optional Makepad sibling crates
Adds feature-gated optional deps and re-exports (makepad-test, makepad-csg,
makepad-gltf, makepad-mbtile-reader, makepad-fast-inflate) so a downstream
workspace can depend on makepad-widgets as its sole Makepad source.
2026-08-16 14:38:41 +03:00
7de0f215d9 feat(test): Android makepad_test via adb + in-process hub (legacy Java path)
Adds the Android test runtime to makepad_test: builds the APK with
cargo-makepad's standard Java path, installs and launches via adb with
makepad.STUDIO_* intent extras (incl. STUDIO_BUILD), connects the app to an
in-process hub over adb reverse, and waits for startup + responsiveness.
Adds clean in-process hub shutdown (HttpServerHandle + GatewayHandle Drop)
and the STUDIO_BUILD intent parsing on the app side. No native-activity or
NDK APK compilation code is included.
2026-08-16 14:36:10 +03:00
makepaddev
abd70f4716
Update README.md 2026-08-15 21:50:38 +02:00
Jason Yau
41b41f1d11
Drain platform_ops FIFO so host commands run in enqueue order. (#1183)
Vec::pop inverted CreateWindow/prepare/IME sequences; VecDeque pop_front matches causal order, and SetTopmost defer no longer livelocks on an empty Windows queue.

Co-authored-by: jasonqiu <jasonqiuchen@outlook.com>
2026-08-15 20:34:16 +02:00
Kevin Boos
941a3de88f
iOS: log panics to NSLog, and don't abort when one escapes (#1178)
A Rust panic raised while handling an iOS event unwinds out of the
`extern "C"` ObjC callback that delivered it, so the process aborts with
a bare SIGABRT. Phase-2 unwinding pops every frame between the panic and
that callback, and the default panic hook writes to stderr, which goes
nowhere on a device. Nothing about the panic survives: the TestFlight
report that prompted this shows only `abort` under
`-[UIWindow _sendTouchesForEvent:]`, with no message and no panic site.

* Install an iOS panic hook in `Cx::event_loop`, mirroring the Android
  one, that logs payload, location, thread and backtrace through
  `error!` (NSLog on iOS). Hooks run at the panic site before unwinding
  starts, so this captures the location the abort would otherwise erase.
* Wrap the app callback in `IosApp::do_callback` in `catch_unwind`.
  That's the one choke point every ObjC callback funnels through, so it
  covers touches, presses, timers, draws and text input at once. It also
  has to be the place: `do_callback` `take()`s the callback and only
  restores it on the normal path, so catching any further out would
  leave the app alive but permanently inert.
* Restore `Cx::executor` in `event_loop`'s callback even when a spawned
  task panics. It's `take()`n the same way, so without this the catch
  above would turn one abort into an `unwrap` panic on every later
  event. The panic still propagates once the executor is back.

macOS and tvOS take the executor the same way, but neither catches, so
the process dies either way and there's nothing to restore it for.
2026-08-14 20:08:52 +02:00
Kevin Boos
d0fe5f2b74
cargo_makepad: link std statically for shippable android builds (#1177)
Android builds passed `-C prefer-dynamic` unconditionally, so Rust shipped
`std` as a separate `libstd-<hash>.so`. That library is a rustup prebuilt whose
LOAD segments are only 4 KB-page aligned (`p_align 0x1000`), so it can't be
mapped on the 16 KB-page devices Android 15 allows. Google Play requires apps
targeting API 35+ to run there, so release apks were failing that bar even
though `libmakepad.so` itself was already linked with 16 KB alignment.

Only debug builds keep `prefer-dynamic` now, where the faster incremental
relink is worth having and nothing ships. Everything else links `std`
statically, which drops the separate library entirely and leaves a single
NDK-linked, 16 KB-aligned `.so`. The aab path already did this.
2026-08-13 10:04:11 +02:00
Kevin Boos
0c38e3b081
makepad_test: let a suite choose parallelism and the tick pump (#1176)
Two knobs, both defaulting to exactly what happens today.

MAKEPAD_TEST_PARALLEL opts out of the global TEST_MUTEX. Every test
currently takes that lock for its whole body, so `--test-threads=N` has no
effect at all and there is nothing in the API that says so. Serial is the
right default — each test drives a whole app process, and oversubscribing
the machine makes timing-sensitive assertions flaky — but it should be the
suite's call.

MAKEPAD_TEST_PUMP_TICKS sets how many Ticks are forwarded before each
query. Each one costs the app a full rendered frame whenever anything is
dirty, so the hardcoded 3 is a 3x multiplier on the cost of every
`widget_snapshot()`, which is the single most common thing a test does.

Reporting the measurements honestly, from a 55-test suite downstream:

- Parallel at 4-way took it from 67 min to 11-20 min, but 2-3 tests failed
  per run and the SET changed between runs — load-induced, not specific
  tests. Useful for local iteration, not something to turn on by default,
  which is why it is opt-in and documented as such rather than flipped.
- PUMP_TICKS=1 measured 1.47x on a fixed 10-test slice with no failures,
  but broke one drag-and-drop test elsewhere in a way I could not explain,
  so treat it as a tuning knob to try rather than a free win.

The flakiness above is a property of tests that wait by counting polls: how
much wall clock and how many frames a poll buys both change under load. That
is worth fixing in the tests, not by keeping the lock.
2026-08-13 10:03:47 +02:00
Kevin Boos
4f7abea39e
headless: stop recompiling every shader on every start (#1175)
The headless backend compiles each shader to a cdylib with `rustc -O` and
writes it to a path keyed by the hash of the generated source — then
recompiles all of them from scratch on the next process start, ignoring
what it just wrote. For host_launcher that is 68 shaders and 38.7s of
startup, paid again by every process. A headless test suite starts one
process per test, so it was paying it 55 times.

Reuse the dylib when it's already there, falling back to a compile if it
won't load or has no version symbol (truncated by a killed run, built by
another toolchain). Compiles now land on a pid-private path and get
renamed into place, so a crash can't leave a half-written dylib behind.

Cold start goes 38.7s -> 1.2s.

Two more things that only bite headless:

- The BGRA->RGBAf32 texture conversion cache was rebuilt per frame, so
  the whole glyph atlas was re-converted on every draw: 430ms of a 840ms
  frame. It already carries a signature and honours pending updates, so
  it was always meant to outlive a frame — park it on CxOs. 430ms -> 29ms.

- Headless dpi was pinned at 2.0, i.e. 4x the pixels through a SOFTWARE
  rasteriser. Still 2.0 by default, since a screenshot should match what a
  display shows, but MAKEPAD_HEADLESS_DPI=1 lets a suite that only asserts
  logical geometry do a quarter of the work. Raster 385ms -> 96ms.

Together a steady frame goes 840ms -> 127ms. Also reports texture time in
the MAKEPAD_HEADLESS_PROFILE line, which is how the atlas cost showed up.
2026-08-13 10:03:01 +02:00
Jason Yau
42a61ce7d7
Windows: overlapped main window (#1168)
* Android: silence unused VA/OpenXR warnings

Exclude desktop-only va_dmabuf_modifier from Android/OHOS builds, cfg-gate
gpu_texture pool imports, and fix OpenXR repaint locals unused without Vulkan.

* Move D3D11 texture COM calls into os/windows helpers for windows_strip

* regenerate windows-rs by windows-strip

* Windows: use overlapped custom chrome with extended client area

* Windows: fix overlapped chrome init sizing and avoid DWM work on every NCCALCSIZE

---------

Co-authored-by: jasonqiu <jasonqiuchen@outlook.com>
2026-08-13 10:01:52 +02:00
Kevin Boos
457e75a7ef
Area: don't panic on a stale Area::Rect (#1171)
`Area::Rect` holds a `rect_id` into its draw list's `rect_areas`, plus the
`redraw_id` of the draw that created it. A widget that hands out its area and
is then redrawn with fewer rect areas leaves that id past the end, so
`clipped_rect()` panicked with an out-of-bounds index (seen from robrix as
"the len is 105 but the index is 173" while hit-testing a message's children).

`rect()` already guarded this with `redraw_id`; `clipped_rect()`, `abs_to_rel()`
and `set_rect()` did not. All four now check the generation first and use
`get`/`get_mut` instead of indexing, falling back to the same values they
already return for an unknown area.
2026-08-12 21:20:37 +02:00
45 changed files with 1444 additions and 409 deletions

View file

@ -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

View file

@ -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,13 +17,11 @@ script_mod! {
flow: Down
spacing: 12
align: Center
on_render: ||{
counter_label := Label{
text: "Count: " + state.counter
text: "Count: 0"
draw_text.text_style.font_size: 24
}
}
}
increment_button := Button{
text: "Increment"
}
@ -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));
}
}
}

View file

@ -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<String>,
pub adb_path: Option<String>,
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<String> {
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<Self> {
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<T, F>(&self, timeout: Duration, mut matcher: F) -> TestResult<T>
@ -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
// 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());
.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::<usize>().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<String> {
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<String> {
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<String> {
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<std::path::PathBuf> {
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::<u16>().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)

View file

@ -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<P1, P2>(dwexstyle: WINDOW_EX_STYLE, lpclassname: P1, lpwindowname: P2, dwstyle: WINDOW_STYLE, x: i32, y: i32, nwidth: i32, nheight: i32, hwndparent: Option<super::super::Foundation::HWND>, hmenu: Option<HMENU>, hinstance: Option<super::super::Foundation::HINSTANCE>, lpparam: Option<*const core::ffi::c_void>) -> windows_core::Result<super::super::Foundation::HWND>
where
P1: windows_core::Param<windows_core::PCWSTR>,
@ -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<unsafe extern "system" fn(param0: super::super::Foundation::HWND, param1: u32, param2: super::super::Foundation::WPARAM, param3: super::super::Foundation::LPARAM) -> 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);
}
}
}

View file

@ -59,24 +59,50 @@ pub enum HttpServerRequest {
},
}
pub fn start_http_server(http_server: HttpServer) -> Option<std::thread::JoinHandle<()>> {
/// 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<HttpServerHandle> {
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 {
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<std::thread::JoinHan
}
})
};
Some(listen_thread)
Some(HttpServerHandle {
thread: listen_thread,
shutdown: shutdown_tx,
})
}
fn handle_post(

View file

@ -11,7 +11,7 @@ pub mod web_socket_parser;
pub use crate::backend::{EventSink, NetworkBackend, UnsupportedBackend};
pub use crate::http_server::{
start_http_server, HttpServer, HttpServerRequest, HttpServerResponse,
start_http_server, HttpServer, HttpServerHandle, HttpServerRequest, HttpServerResponse,
};
pub use crate::runtime::{NetworkConfig, NetworkRuntime};
pub use crate::socket_stream::SocketStream;

View file

@ -5,7 +5,7 @@ use std::time::Duration;
use makepad_live_id::LiveId;
use crate::backend::{default_backend, EventSink, NetworkBackend};
use crate::http_server::HttpServer;
use crate::http_server::{HttpServer, HttpServerHandle};
use crate::types::{HttpRequest, NetworkError, NetworkResponse, WsSend};
#[derive(Default)]
@ -62,7 +62,7 @@ impl NetworkRuntime {
pub fn start_http_server(
&self,
http_server: HttpServer,
) -> Option<std::thread::JoinHandle<()>> {
) -> Option<HttpServerHandle> {
crate::http_server::start_http_server(http_server)
}

View file

@ -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,9 +314,10 @@ 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];
if let Some(rect_area) = draw_list.rect_areas.get(ra.rect_id) {
return rect_area.rect;
}
}
Rect::default()
}
_ => 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,14 +417,13 @@ 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];
if let Some(rect_area) = draw_list.rect_areas.get_mut(ra.rect_id) {
rect_area.rect = *rect
}
}
_ => (),
}
}

View file

@ -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<CxOsOp>,
pub(crate) platform_ops: VecDeque<CxOsOp>,
pub(crate) pending_camera_playbacks: Vec<PendingCameraPlayback>,
pub(crate) new_next_frames: HashSet<NextFrame>,

View file

@ -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<CxOsOp>, 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<CharOffset>,
composition: Option<Range<CharOffset>>,
) {
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<dyn std::any::Any + Send>) {
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<DragItem>) {
@ -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<u8>) {
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());
}
}

View file

@ -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,8 +425,12 @@ mod windows_api {
.map_err(|e| format!("adopt_d3d11_bgra: cast to resource failed: {e}"))?;
let mut out: Option<ID3D11ShaderResourceView> = None;
unsafe {
device
.CreateShaderResourceView(&resource, None, Some(&mut out))
d3d11_texture::create_shader_resource_view(
&device,
&resource,
None,
Some(&mut out),
)
.map_err(|e| {
format!("adopt_d3d11_bgra: CreateShaderResourceView failed: {e:?}")
})?;
@ -503,7 +513,7 @@ mod windows_api {
) -> Result<ID3D11ShaderResourceView, String> {
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,8 +551,12 @@ mod windows_api {
let mut out: Option<ID3D11ShaderResourceView> = None;
unsafe {
device
.CreateShaderResourceView(&resource, Some(&desc), Some(&mut out))
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<ID3D11ShaderResourceView, String> {
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<ID3D11ShaderResourceView> = 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<ID3D11Texture2D> = 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!(

View file

@ -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::<String>() {
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(|| "<unknown>".to_string());
let thread = std::thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
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<RefCell<Cx>>) {
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];

View file

@ -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);

View file

@ -243,11 +243,6 @@ impl MetalWindow {
}
}
fn defer_platform_op(platform_ops: &mut Vec<CxOsOp>, 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<MetalWindow>,
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();

View file

@ -371,7 +371,7 @@ impl Cx {
_metal_cx: &MetalCx,
stdin_windows: &mut Vec<StdinWindow>,
) {
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() {

View file

@ -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];

View file

@ -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::<f64>().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<HeadlessWindowState>,
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;

View file

@ -39,6 +39,28 @@ impl HeadlessShaderJit {
source: &str,
) -> Result<HeadlessJitOutput, String> {
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;

View file

@ -69,6 +69,11 @@ pub struct CxOs {
pub(crate) draw_cycles: Option<usize>,
pub(crate) render_pool: Option<MessageThreadPool<()>>,
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(),
}
}
}

View file

@ -123,12 +123,12 @@ struct TextureConversionSignature {
data_len: usize,
}
struct CachedTextureConversion {
pub(crate) struct CachedTextureConversion {
signature: TextureConversionSignature,
rgba: Vec<f32>,
}
type TextureConversionCache = HashMap<usize, CachedTextureConversion>;
pub(crate) type TextureConversionCache = HashMap<usize, CachedTextureConversion>;
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 {

View file

@ -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];

View file

@ -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<jni_sys::jstring> {
@ -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 {

View file

@ -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];

View file

@ -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;

View file

@ -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) => {

View file

@ -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,8 +243,15 @@ impl Cx {
}
}
}
#[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) {
let xr_frame_started = Instant::now();

View file

@ -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(_) => {}
_ => {

View file

@ -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();

View file

@ -543,7 +543,7 @@ impl Cx {
}
fn stdin_handle_platform_ops(&mut self, stdin_windows: &mut Vec<StdinWindow>) {
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() {

View file

@ -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 = {

View file

@ -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<ID3D11Texture2D>>,
) -> 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<ID3D11ShaderResourceView>>,
) -> WinResult<()> {
device.CreateShaderResourceView(resource, desc, srv_out)
}
pub unsafe fn device_get_immediate_context(
device: &ID3D11Device,
) -> WinResult<ID3D11DeviceContext> {
device.GetImmediateContext()
}

View file

@ -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;

View file

@ -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<GetDpiForWindow>,
adjust_window_rect_ex_for_dpi: Option<AdjustWindowRectExForDpi>,
get_dpi_for_monitor: Option<GetDpiForMonitor>,
enable_nonclient_dpi_scaling: Option<EnableNonClientDpiScaling>,
set_process_dpi_awareness_context: Option<SetProcessDpiAwarenessContext>,
@ -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);

View file

@ -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<LRESULT> {
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);
// 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.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);
}
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<WINDOWPLACEMENT> = mem::MaybeUninit::uninit();
let mut wp = wp.assume_init();
wp.length = mem::size_of::<WINDOWPLACEMENT>() 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();

View file

@ -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,10 +694,11 @@ 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));
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)
{

View file

@ -367,7 +367,7 @@ impl Cx {
}
fn stdin_handle_platform_ops(&mut self, stdin_windows: &mut Vec<StdinWindow>) {
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() {

View file

@ -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());
}
}

View file

@ -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" }

View file

@ -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));
}
}

View file

@ -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<JoinHandle<()>>,
pub http_thread: Option<JoinHandle<()>>,
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<HubEvent>,
) -> Result<GatewayHandle, String> {
let (request_tx, request_rx) = mpsc::channel::<HttpServerRequest>();
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,
})
}

View file

@ -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-<hash>.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

View file

@ -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

View file

@ -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"]

View file

@ -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;

View file

@ -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;