Compare commits
10 commits
e0a5a23f2f
...
ec2c0aaf15
| Author | SHA1 | Date | |
|---|---|---|---|
| ec2c0aaf15 | |||
| 7de0f215d9 | |||
|
|
abd70f4716 |
||
|
|
41b41f1d11 |
||
|
|
941a3de88f |
||
|
|
d0fe5f2b74 |
||
|
|
0c38e3b081 |
||
|
|
4f7abea39e |
||
|
|
42a61ce7d7 |
||
|
|
457e75a7ef |
45 changed files with 1444 additions and 409 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,8 @@ app_main!(App);
|
|||
|
||||
script_mod! {
|
||||
use mod.prelude.widgets.*
|
||||
let state = {
|
||||
counter: 0
|
||||
}
|
||||
mod.state = state
|
||||
startup() do #(App::script_component(vm)){
|
||||
ui: Root{
|
||||
on_startup:||{ // right now render isnt called automatically yet
|
||||
ui.main_view.render()
|
||||
}
|
||||
main_window := Window{
|
||||
window.inner_size: vec2(420, 220)
|
||||
body +: {
|
||||
|
|
@ -24,11 +17,9 @@ script_mod! {
|
|||
flow: Down
|
||||
spacing: 12
|
||||
align: Center
|
||||
on_render: ||{
|
||||
counter_label := Label{
|
||||
text: "Count: " + state.counter
|
||||
draw_text.text_style.font_size: 24
|
||||
}
|
||||
counter_label := Label{
|
||||
text: "Count: 0"
|
||||
draw_text.text_style.font_size: 24
|
||||
}
|
||||
}
|
||||
increment_button := Button{
|
||||
|
|
@ -44,15 +35,15 @@ script_mod! {
|
|||
pub struct App {
|
||||
#[live]
|
||||
ui: WidgetRef,
|
||||
#[rust]
|
||||
counter: i32,
|
||||
}
|
||||
|
||||
impl MatchEvent for App {
|
||||
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
|
||||
if self.ui.button(cx, ids!(increment_button)).clicked(actions) {
|
||||
script_eval!(cx,{
|
||||
mod.state.counter += 1
|
||||
ui.main_view.render()
|
||||
});
|
||||
self.counter += 1;
|
||||
self.ui.label(cx, ids!(counter_label)).set_text(cx, &format!("Count: {}", self.counter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
// Tests are serialised by default: each one drives a whole app process, so
|
||||
// running several at once oversubscribes the machine and makes timing-
|
||||
// sensitive assertions flaky. A suite whose app is cheap enough can opt out.
|
||||
let _guard = (!parallel_tests_enabled()).then(|| {
|
||||
TEST_MUTEX
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
});
|
||||
|
||||
let app = TestApp::start(config)?;
|
||||
let result = catch_unwind(AssertUnwindSafe(|| test(app.clone()).into_test_result()));
|
||||
|
|
@ -1455,6 +1483,20 @@ fn snapshot_summary(widget: &WidgetSnapshot) -> String {
|
|||
fields.join(" ")
|
||||
}
|
||||
|
||||
/// Ticks forwarded before each query. Every one of them costs the app a full
|
||||
/// rendered frame when anything is dirty, so this is the multiplier on the cost
|
||||
/// of a snapshot — worth lowering for a suite whose app renders slowly.
|
||||
fn pump_ticks() -> usize {
|
||||
std::env::var("MAKEPAD_TEST_PUMP_TICKS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
println!("Incoming stream failure");
|
||||
continue;
|
||||
loop {
|
||||
if shutdown_rx.try_recv().is_ok() {
|
||||
break;
|
||||
}
|
||||
let mut tcp_stream = match listener.accept() {
|
||||
Ok((tcp_stream, _)) => tcp_stream,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
// No pending connection; poll the shutdown channel
|
||||
// periodically so a drop releases the port promptly.
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Incoming stream failure");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if tcp_stream.set_nonblocking(false).is_err() {
|
||||
continue;
|
||||
}
|
||||
let http_server = http_server.clone();
|
||||
connection_counter += 1;
|
||||
let _read_thread = std::thread::spawn(move || {
|
||||
|
|
@ -108,7 +134,10 @@ pub fn start_http_server(http_server: HttpServer) -> Option<std::thread::JoinHan
|
|||
}
|
||||
})
|
||||
};
|
||||
Some(listen_thread)
|
||||
Some(HttpServerHandle {
|
||||
thread: listen_thread,
|
||||
shutdown: shutdown_tx,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_post(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -241,15 +241,14 @@ impl Area {
|
|||
Area::Rect(ra) => {
|
||||
// we need to clip this drawlist too
|
||||
let draw_list = &cx.draw_lists[ra.draw_list_id];
|
||||
// Guard against a stale rect area (its draw list was rebuilt smaller,
|
||||
// e.g. a recycled/filtered list cell) — same check the Instance arm
|
||||
// uses — rather than indexing rect_areas out of bounds and panicking.
|
||||
if draw_list.redraw_id != ra.redraw_id
|
||||
|| ra.rect_id >= draw_list.rect_areas.len()
|
||||
{
|
||||
// A stale area outlives the draw that made it, and its rect_id
|
||||
// can then be past the end of a shorter list.
|
||||
if draw_list.redraw_id != ra.redraw_id {
|
||||
return Rect::default();
|
||||
}
|
||||
let rect_area = &draw_list.rect_areas[ra.rect_id];
|
||||
let Some(rect_area) = draw_list.rect_areas.get(ra.rect_id) else {
|
||||
return Rect::default();
|
||||
};
|
||||
if draw_list.draw_list_has_clip {
|
||||
let p3 = dvec2(
|
||||
draw_list.draw_list_uniforms.view_clip.x as f64,
|
||||
|
|
@ -315,8 +314,9 @@ impl Area {
|
|||
Area::Rect(ra) => {
|
||||
let draw_list = &cx.draw_lists[ra.draw_list_id];
|
||||
if draw_list.redraw_id == ra.redraw_id {
|
||||
let rect_area = &draw_list.rect_areas[ra.rect_id];
|
||||
return rect_area.rect;
|
||||
if let Some(rect_area) = draw_list.rect_areas.get(ra.rect_id) {
|
||||
return rect_area.rect;
|
||||
}
|
||||
}
|
||||
Rect::default()
|
||||
}
|
||||
|
|
@ -352,12 +352,12 @@ impl Area {
|
|||
}
|
||||
Area::Rect(ra) => {
|
||||
let draw_list = &cx.draw_lists[ra.draw_list_id];
|
||||
if draw_list.redraw_id != ra.redraw_id
|
||||
|| ra.rect_id >= draw_list.rect_areas.len()
|
||||
{
|
||||
if draw_list.redraw_id != ra.redraw_id {
|
||||
return abs;
|
||||
}
|
||||
let rect_area = &draw_list.rect_areas[ra.rect_id];
|
||||
let Some(rect_area) = draw_list.rect_areas.get(ra.rect_id) else {
|
||||
return abs;
|
||||
};
|
||||
Vec2d {
|
||||
x: abs.x - rect_area.rect.pos.x,
|
||||
y: abs.y - rect_area.rect.pos.y,
|
||||
|
|
@ -417,13 +417,12 @@ impl Area {
|
|||
}
|
||||
Area::Rect(ra) => {
|
||||
let draw_list = &mut cx.draw_lists[ra.draw_list_id];
|
||||
if draw_list.redraw_id != ra.redraw_id
|
||||
|| ra.rect_id >= draw_list.rect_areas.len()
|
||||
{
|
||||
if draw_list.redraw_id != ra.redraw_id {
|
||||
return;
|
||||
}
|
||||
let rect_area = &mut draw_list.rect_areas[ra.rect_id];
|
||||
rect_area.rect = *rect
|
||||
if let Some(rect_area) = draw_list.rect_areas.get_mut(ra.rect_id) {
|
||||
rect_area.rect = *rect
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,13 +35,18 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
texture::{
|
||||
CxTexturePool, Texture, TextureAlloc, TextureCategory, TextureFormat, TextureId,
|
||||
TexturePixel,
|
||||
},
|
||||
texture::{Texture, TextureAlloc, TextureCategory, TextureFormat, TexturePixel},
|
||||
Cx,
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "linux", not(any(target_env = "ohos", linux_direct))),
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
))]
|
||||
use crate::texture::{CxTexturePool, TextureId};
|
||||
|
||||
/// Serializes hard-decode / media GPU work with Makepad present copies on the
|
||||
/// shared D3D11 device. Recursive so the same thread may nest lock calls
|
||||
/// (common when a decoder callback re-enters present).
|
||||
|
|
@ -304,6 +309,7 @@ impl Drop for MetalNv12PresentCache {
|
|||
#[cfg(target_os = "windows")]
|
||||
mod windows_api {
|
||||
use super::*;
|
||||
use crate::os::windows::d3d11_texture;
|
||||
use windows::{
|
||||
core::Interface,
|
||||
Win32::Graphics::{
|
||||
|
|
@ -419,11 +425,15 @@ mod windows_api {
|
|||
.map_err(|e| format!("adopt_d3d11_bgra: cast to resource failed: {e}"))?;
|
||||
let mut out: Option<ID3D11ShaderResourceView> = None;
|
||||
unsafe {
|
||||
device
|
||||
.CreateShaderResourceView(&resource, None, Some(&mut out))
|
||||
.map_err(|e| {
|
||||
format!("adopt_d3d11_bgra: CreateShaderResourceView failed: {e:?}")
|
||||
})?;
|
||||
d3d11_texture::create_shader_resource_view(
|
||||
&device,
|
||||
&resource,
|
||||
None,
|
||||
Some(&mut out),
|
||||
)
|
||||
.map_err(|e| {
|
||||
format!("adopt_d3d11_bgra: CreateShaderResourceView failed: {e:?}")
|
||||
})?;
|
||||
}
|
||||
out.ok_or_else(|| {
|
||||
"adopt_d3d11_bgra: CreateShaderResourceView returned null".to_string()
|
||||
|
|
@ -503,7 +513,7 @@ mod windows_api {
|
|||
) -> Result<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,9 +551,13 @@ mod windows_api {
|
|||
|
||||
let mut out: Option<ID3D11ShaderResourceView> = None;
|
||||
unsafe {
|
||||
device
|
||||
.CreateShaderResourceView(&resource, Some(&desc), Some(&mut out))
|
||||
.map_err(|e| format!("NV12 plane SRV: CreateShaderResourceView failed: {e:?}"))?;
|
||||
d3d11_texture::create_shader_resource_view(
|
||||
device,
|
||||
&resource,
|
||||
Some(&desc),
|
||||
Some(&mut out),
|
||||
)
|
||||
.map_err(|e| format!("NV12 plane SRV: CreateShaderResourceView failed: {e:?}"))?;
|
||||
}
|
||||
out.ok_or_else(|| "NV12 plane SRV: null".into())
|
||||
}
|
||||
|
|
@ -558,7 +572,7 @@ mod windows_api {
|
|||
) -> Result<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!(
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -171,11 +171,14 @@ impl Cx {
|
|||
pub(crate) fn openxr_handle_repaint(
|
||||
&mut self,
|
||||
frame: &CxOpenXrFrame,
|
||||
#[cfg_attr(not(use_vulkan), allow(unused_variables))]
|
||||
xr_cpu: &mut XrFrameCpuBreakdown,
|
||||
) {
|
||||
//opengl_cx.make_current();
|
||||
let mut passes_todo = Vec::new();
|
||||
#[cfg(use_vulkan)]
|
||||
let mut xr_render_cpu_ms = 0.0f64;
|
||||
#[cfg(use_vulkan)]
|
||||
let mut saw_xr_vulkan_pass = false;
|
||||
self.compute_pass_repaint_order(&mut passes_todo);
|
||||
self.repaint_id += 1;
|
||||
|
|
@ -240,7 +243,14 @@ impl Cx {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.os.xr_render_cpu_time_ms = saw_xr_vulkan_pass.then_some(xr_render_cpu_ms);
|
||||
#[cfg(use_vulkan)]
|
||||
{
|
||||
self.os.xr_render_cpu_time_ms = saw_xr_vulkan_pass.then_some(xr_render_cpu_ms);
|
||||
}
|
||||
#[cfg(not(use_vulkan))]
|
||||
{
|
||||
self.os.xr_render_cpu_time_ms = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn openxr_handle_drawing(&mut self) {
|
||||
|
|
|
|||
|
|
@ -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(_) => {}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
64
platform/src/os/windows/d3d11_texture.rs
Normal file
64
platform/src/os/windows/d3d11_texture.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
if abs.y > rect.pos.y + rect.size.y - EDGE {
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NeswResize));
|
||||
return LRESULT(HTBOTTOMLEFT as isize);
|
||||
}
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::EwResize));
|
||||
return LRESULT(HTLEFT as isize);
|
||||
// Fully-client custom chrome (restored mains + popups): emulate
|
||||
// resize borders with HT* hits. Returning system sizing codes
|
||||
// still starts a resize drag; WM_SETCURSOR owns the cursor so we
|
||||
// only clear our cached cursor (avoids stuck resize arrows).
|
||||
if let Some(resize_hit) = window.hit_test_client_resize_edge(lparam) {
|
||||
with_win32_app(|app| app.current_cursor = None);
|
||||
return resize_hit;
|
||||
}
|
||||
if abs.x > rect.pos.x + rect.size.x - EDGE {
|
||||
if abs.y < rect.pos.y + EDGE {
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NeswResize));
|
||||
return LRESULT(HTTOPRIGHT as isize);
|
||||
}
|
||||
if abs.y > rect.pos.y + rect.size.y - EDGE {
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NwseResize));
|
||||
return LRESULT(HTBOTTOMRIGHT as isize);
|
||||
}
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::EwResize));
|
||||
return LRESULT(HTRIGHT as isize);
|
||||
}
|
||||
if abs.y < rect.pos.y + EDGE {
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NsResize));
|
||||
return LRESULT(HTTOP as isize);
|
||||
}
|
||||
if abs.y > rect.pos.y + rect.size.y - EDGE {
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::NsResize));
|
||||
return LRESULT(HTBOTTOM as isize);
|
||||
}
|
||||
// Dedupe: return the cached WindowDragQuery result for a repeated cursor position
|
||||
// (the loop is vsync-paced, so the OS sends several same-position hit-tests/frame).
|
||||
let response_val = match window.nc_dq_cache.get() {
|
||||
Some((lp, rv)) if lp == lparam.0 => rv,
|
||||
_ => {
|
||||
// Snapshot the cache generation: dispatching WindowDragQuery can reenter the
|
||||
// window proc (nested SendMessage) and invalidate the cache mid-flight; if it
|
||||
// does, we must NOT write our now-stale result back over that invalidation.
|
||||
let gen = window.nc_dq_gen.get();
|
||||
let response = Rc::new(Cell::new(WindowDragQueryResponse::NoAnswer));
|
||||
window.do_callback(Win32Event::WindowDragQuery(WindowDragQueryEvent {
|
||||
window_id: window.window_id,
|
||||
abs: window.get_mouse_pos_from_lparam(lparam) - rect.pos,
|
||||
response: response.clone(),
|
||||
}));
|
||||
let rv = response.get();
|
||||
if window.nc_dq_gen.get() == gen {
|
||||
window.nc_dq_cache.set(Some((lparam.0, rv)));
|
||||
}
|
||||
rv
|
||||
}
|
||||
};
|
||||
match response_val {
|
||||
WindowDragQueryResponse::Client => {
|
||||
return LRESULT(HTCLIENT as isize);
|
||||
}
|
||||
WindowDragQueryResponse::Caption => {
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::Default));
|
||||
return LRESULT(HTCAPTION as isize);
|
||||
}
|
||||
WindowDragQueryResponse::SysMenu => {
|
||||
with_win32_app(|app| app.set_mouse_cursor(MouseCursor::Default));
|
||||
return LRESULT(HTSYSMENU as isize);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
return LRESULT(HTCLIENT as isize);
|
||||
return window.hit_test_extended_client(lparam);
|
||||
}
|
||||
WM_ERASEBKGND => return LRESULT(1),
|
||||
WM_MOUSEMOVE => {
|
||||
|
|
@ -1093,11 +1301,7 @@ impl Win32Window {
|
|||
const BUTTON_H: f64 = 29.0;
|
||||
const BUTTON_COUNT: f64 = 3.0;
|
||||
const BUTTONS_W: f64 = BUTTON_W * BUTTON_COUNT;
|
||||
let inner_size = if self.get_is_maximized() {
|
||||
self.get_outer_size()
|
||||
} else {
|
||||
self.get_inner_size()
|
||||
};
|
||||
let inner_size = self.get_inner_size();
|
||||
WindowGeom {
|
||||
xr_is_presenting: false,
|
||||
can_fullscreen: false,
|
||||
|
|
@ -1122,16 +1326,10 @@ impl Win32Window {
|
|||
}
|
||||
|
||||
pub fn get_is_maximized(&self) -> bool {
|
||||
unsafe {
|
||||
let wp: mem::MaybeUninit<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();
|
||||
|
|
|
|||
|
|
@ -559,7 +559,7 @@ impl Cx {
|
|||
) -> EventFlow {
|
||||
let mut ret = EventFlow::Poll;
|
||||
let mut geom_changes = Vec::new();
|
||||
while let Some(op) = self.platform_ops.pop() {
|
||||
while let Some(op) = self.platform_ops.pop_front() {
|
||||
match op {
|
||||
CxOsOp::CreateWindow(window_id) => {
|
||||
let window = &mut self.windows[window_id];
|
||||
|
|
@ -694,9 +694,10 @@ impl Cx {
|
|||
CxOsOp::Quit => ret = EventFlow::Exit,
|
||||
CxOsOp::SetTopmost(window_id, is_topmost) => {
|
||||
if d3d11_windows.len() == 0 {
|
||||
self.platform_ops
|
||||
.insert(0, CxOsOp::SetTopmost(window_id, is_topmost));
|
||||
continue;
|
||||
if self.defer_platform_op(CxOsOp::SetTopmost(window_id, is_topmost)) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if let Some(window) =
|
||||
d3d11_windows.iter_mut().find(|w| w.window_id == window_id)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
38
studio/hub/src/bin/hub_server.rs
Normal file
38
studio/hub/src/bin/hub_server.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue