diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ee15243..a8705023 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: type-stubs - path: crates/processing_pyo3/mewnala/__init__.pyi + path: crates/processing_pyo3/mewnala/*.pyi linux: runs-on: ubuntu-22.04 diff --git a/Cargo.lock b/Cargo.lock index 56229146..b227d056 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,7 +518,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bevy" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_internal", ] @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "bevy_a11y" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "accesskit", "bevy_app", @@ -538,7 +538,7 @@ dependencies = [ [[package]] name = "bevy_android" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "android-activity", ] @@ -546,7 +546,7 @@ dependencies = [ [[package]] name = "bevy_animation" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_animation_macros", "bevy_app", @@ -578,7 +578,7 @@ dependencies = [ [[package]] name = "bevy_animation_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "quote", @@ -588,7 +588,7 @@ dependencies = [ [[package]] name = "bevy_anti_alias" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -609,7 +609,7 @@ dependencies = [ [[package]] name = "bevy_app" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_derive", "bevy_ecs", @@ -630,7 +630,7 @@ dependencies = [ [[package]] name = "bevy_asset" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "async-broadcast", "async-channel", @@ -673,7 +673,7 @@ dependencies = [ [[package]] name = "bevy_asset_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -684,7 +684,7 @@ dependencies = [ [[package]] name = "bevy_audio" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "bevy_camera" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -724,7 +724,7 @@ dependencies = [ [[package]] name = "bevy_camera_controller" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_camera", @@ -738,10 +738,24 @@ dependencies = [ "bevy_window", ] +[[package]] +name = "bevy_clipboard" +version = "0.19.0-dev" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_log", + "bevy_platform", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "bevy_color" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_math", "bevy_reflect", @@ -756,7 +770,7 @@ dependencies = [ [[package]] name = "bevy_core_pipeline" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -798,7 +812,7 @@ dependencies = [ [[package]] name = "bevy_derive" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "quote", @@ -808,7 +822,7 @@ dependencies = [ [[package]] name = "bevy_dev_tools" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -839,7 +853,7 @@ dependencies = [ [[package]] name = "bevy_diagnostic" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "atomic-waker", "bevy_app", @@ -856,7 +870,7 @@ dependencies = [ [[package]] name = "bevy_ecs" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "arrayvec", "bevy_ecs_macros", @@ -883,7 +897,7 @@ dependencies = [ [[package]] name = "bevy_ecs_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -894,7 +908,7 @@ dependencies = [ [[package]] name = "bevy_encase_derive" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "encase_derive_impl", @@ -903,7 +917,7 @@ dependencies = [ [[package]] name = "bevy_feathers" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "accesskit", "bevy_a11y", @@ -934,7 +948,7 @@ dependencies = [ [[package]] name = "bevy_gilrs" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_ecs", @@ -949,7 +963,7 @@ dependencies = [ [[package]] name = "bevy_gizmos" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -971,7 +985,7 @@ dependencies = [ [[package]] name = "bevy_gizmos_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "quote", @@ -981,7 +995,7 @@ dependencies = [ [[package]] name = "bevy_gizmos_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1009,7 +1023,7 @@ dependencies = [ [[package]] name = "bevy_gltf" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "async-lock", "base64", @@ -1044,7 +1058,7 @@ dependencies = [ [[package]] name = "bevy_image" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1072,7 +1086,7 @@ dependencies = [ [[package]] name = "bevy_input" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1088,7 +1102,7 @@ dependencies = [ [[package]] name = "bevy_input_focus" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1104,7 +1118,7 @@ dependencies = [ [[package]] name = "bevy_internal" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_a11y", "bevy_android", @@ -1115,6 +1129,7 @@ dependencies = [ "bevy_audio", "bevy_camera", "bevy_camera_controller", + "bevy_clipboard", "bevy_color", "bevy_core_pipeline", "bevy_derive", @@ -1162,7 +1177,7 @@ dependencies = [ [[package]] name = "bevy_light" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1186,7 +1201,7 @@ dependencies = [ [[package]] name = "bevy_log" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "android_log-sys", "bevy_app", @@ -1203,7 +1218,7 @@ dependencies = [ [[package]] name = "bevy_macro_utils" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "proc-macro2", "quote", @@ -1214,7 +1229,7 @@ dependencies = [ [[package]] name = "bevy_material" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_asset", "bevy_derive", @@ -1236,7 +1251,7 @@ dependencies = [ [[package]] name = "bevy_material_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "quote", @@ -1246,7 +1261,7 @@ dependencies = [ [[package]] name = "bevy_math" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "approx", "arrayvec", @@ -1265,7 +1280,7 @@ dependencies = [ [[package]] name = "bevy_mesh" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1323,7 +1338,7 @@ dependencies = [ [[package]] name = "bevy_pbr" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "arrayvec", "bevy_app", @@ -1365,7 +1380,7 @@ dependencies = [ [[package]] name = "bevy_picking" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1388,7 +1403,7 @@ dependencies = [ [[package]] name = "bevy_platform" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "critical-section", "foldhash 0.2.0", @@ -1403,13 +1418,13 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-time", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "bevy_post_process" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1433,12 +1448,12 @@ dependencies = [ [[package]] name = "bevy_ptr" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" [[package]] name = "bevy_reflect" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "assert_type_match", "bevy_platform", @@ -1466,7 +1481,7 @@ dependencies = [ [[package]] name = "bevy_reflect_derive" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "indexmap", @@ -1479,7 +1494,7 @@ dependencies = [ [[package]] name = "bevy_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "async-channel", "bevy_app", @@ -1532,7 +1547,7 @@ dependencies = [ [[package]] name = "bevy_render_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -1543,7 +1558,7 @@ dependencies = [ [[package]] name = "bevy_scene" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1562,7 +1577,7 @@ dependencies = [ [[package]] name = "bevy_scene_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -1573,7 +1588,7 @@ dependencies = [ [[package]] name = "bevy_shader" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_asset", "bevy_platform", @@ -1592,7 +1607,7 @@ dependencies = [ [[package]] name = "bevy_sprite" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1617,7 +1632,7 @@ dependencies = [ [[package]] name = "bevy_sprite_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1649,7 +1664,7 @@ dependencies = [ [[package]] name = "bevy_state" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1664,7 +1679,7 @@ dependencies = [ [[package]] name = "bevy_state_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_macro_utils", "quote", @@ -1674,7 +1689,7 @@ dependencies = [ [[package]] name = "bevy_tasks" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "async-channel", "async-executor", @@ -1692,10 +1707,11 @@ dependencies = [ [[package]] name = "bevy_text" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", + "bevy_clipboard", "bevy_color", "bevy_derive", "bevy_ecs", @@ -1719,7 +1735,7 @@ dependencies = [ [[package]] name = "bevy_time" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1733,7 +1749,7 @@ dependencies = [ [[package]] name = "bevy_transform" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1749,7 +1765,7 @@ dependencies = [ [[package]] name = "bevy_ui" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "accesskit", "bevy_a11y", @@ -1786,7 +1802,7 @@ dependencies = [ [[package]] name = "bevy_ui_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1796,6 +1812,7 @@ dependencies = [ "bevy_derive", "bevy_ecs", "bevy_image", + "bevy_input_focus", "bevy_math", "bevy_mesh", "bevy_platform", @@ -1817,7 +1834,7 @@ dependencies = [ [[package]] name = "bevy_ui_widgets" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "accesskit", "bevy_a11y", @@ -1840,7 +1857,7 @@ dependencies = [ [[package]] name = "bevy_utils" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "async-channel", "bevy_platform", @@ -1852,7 +1869,7 @@ dependencies = [ [[package]] name = "bevy_window" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -1870,7 +1887,7 @@ dependencies = [ [[package]] name = "bevy_winit" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "accesskit", "accesskit_winit", @@ -1902,7 +1919,7 @@ dependencies = [ [[package]] name = "bevy_world_serialization" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#4dc8fb49199fea2065db519fc3f06fdadb5348e1" +source = "git+https://github.com/processing/bevy?branch=main#d53cefeaa9802a28c9b7e40f70489e4d817cb9eb" dependencies = [ "bevy_app", "bevy_asset", @@ -3077,7 +3094,7 @@ dependencies = [ "linebender_resource_handle", "memmap2", "parlance", - "read-fonts 0.39.1", + "read-fonts 0.39.2", "smallvec", ] @@ -3509,7 +3526,7 @@ dependencies = [ "bitflags 2.11.1", "bytemuck", "core_maths", - "read-fonts 0.39.1", + "read-fonts 0.39.2", "smallvec", ] @@ -5403,7 +5420,7 @@ dependencies = [ "linebender_resource_handle", "parlance", "parley_data", - "skrifa 0.42.0", + "skrifa 0.42.1", ] [[package]] @@ -5710,7 +5727,7 @@ dependencies = [ [[package]] name = "processing_pyo3" -version = "0.0.2" +version = "0.0.3" dependencies = [ "bevy", "png", @@ -5719,6 +5736,8 @@ dependencies = [ "processing_glfw", "processing_webcam", "pyo3", + "rand 0.10.1", + "rand_distr", ] [[package]] @@ -6069,9 +6088,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.39.1" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d7821ebf634094f5e5ae9d43c3109407f420f03a7a2e021d3a5d955a4f09fc" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" dependencies = [ "bytemuck", "core_maths", @@ -6431,12 +6450,12 @@ dependencies = [ [[package]] name = "skrifa" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c844316c7318cf6ae9034ee45edd2c432076ef910fdc4bc598183b31f88d03c3" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" dependencies = [ "bytemuck", - "read-fonts 0.39.1", + "read-fonts 0.39.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index da1c6254..a8136520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ type_complexity = "allow" too_many_arguments = "allow" [workspace.dependencies] -bevy = { git = "https://github.com/bevyengine/bevy", branch = "main", features = ["file_watcher", "shader_format_wesl", "free_camera", "pan_camera"] } +bevy = { git = "https://github.com/processing/bevy", branch = "main", features = ["file_watcher", "shader_format_wesl", "free_camera", "pan_camera"] } bevy_naga_reflect = { git = "https://github.com/tychedelia/bevy_naga_reflect" } bevy_cuda = { git = "https://github.com/tychedelia/bevy_cuda" } naga = { version = "29", features = ["wgsl-in"] } @@ -39,6 +39,8 @@ processing_midi = { path = "crates/processing_midi" } processing_input = { path = "crates/processing_input" } processing_glfw = { path = "crates/processing_glfw" } processing_webcam = { path = "crates/processing_webcam" } +rand = "0.10" +rand_distr = "0.6" [dependencies] bevy = { workspace = true } @@ -58,11 +60,14 @@ web-sys = { version = "0.3", features = ["Window"] } [dev-dependencies] processing_glfw = { workspace = true } -rand = "0.10.0" +rand = { workspace = true } [target.'cfg(target_os = "linux")'.dev-dependencies] processing_glfw = { workspace = true, features = ["wayland"] } +[patch."https://github.com/bevyengine/bevy"] +bevy = { git = "https://github.com/processing/bevy", branch = "main" } + [[example]] name = "rectangle" path = "examples/rectangle.rs" diff --git a/README.md b/README.md index 2708b475..8119f8c9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ # libprocessing +> [!WARNING] +> This project is very much R&D and highly unstable + libprocessing is an experimental native library with the goal of supporting the implementation of the core Processing API in a variety of languages. The library is written in the [Rust programming language](https://rust-lang.org/) and built on top of the [Bevy game engine](https://bevy.org/). libprocessing uses [WebGPU](https://webgpu.org/) as its rendering backend and is designed to (eventually) support desktop, mobile, and web targets. +You can learn more about this project from this [talk at LibreGraphicsMeeting 2026](https://app.media.ccc.de/v/lgm-2026-110668-expanding-processing-s-future-with-a-rust-rendering-engine) + ## Getting started ### mewnala (the python library) diff --git a/crates/processing_glfw/src/lib.rs b/crates/processing_glfw/src/lib.rs index 975ddd9e..b8d844e5 100644 --- a/crates/processing_glfw/src/lib.rs +++ b/crates/processing_glfw/src/lib.rs @@ -1,19 +1,60 @@ use bevy::input::keyboard::{KeyCode, NativeKeyCode}; use bevy::input::mouse::MouseButton; +use bevy::math::{IRect, IVec2}; use bevy::prelude::Entity; +use bevy::window::{ + Monitor as BevyMonitor, MonitorSelection, PrimaryMonitor, VideoMode as BevyVideoMode, + Window as BevyWindow, WindowLevel as BevyWindowLevel, WindowMode as BevyWindowMode, + WindowPosition, +}; use glfw::{Action, Glfw, GlfwReceiver, PWindow, WindowEvent, WindowMode}; +use processing_core::app_mut; use processing_core::error::Result; use processing_input::{ input_cursor_grab_mode, input_cursor_visible, input_flush, input_set_char, input_set_cursor_enter, input_set_cursor_leave, input_set_focus, input_set_key, input_set_mouse_button, input_set_mouse_move, input_set_scroll, }; +use processing_render::surface::{MonitorWorkarea, WindowControls}; pub struct GlfwContext { glfw: Glfw, window: PWindow, events: GlfwReceiver<(f64, WindowEvent)>, surface: Option, + last_applied: AppliedWindow, + windowed_geometry: Option<(i32, i32, u32, u32)>, +} + +/// What we last pushed to the OS window, diffed against [`BevyWindow`] each tick so we +/// only call into GLFW when something actually changed. +#[derive(Clone, Debug)] +struct AppliedWindow { + title: String, + position: IVec2, + size: bevy::math::UVec2, + visible: bool, + resizable: bool, + decorations: bool, + window_level: BevyWindowLevel, + fullscreen_on: Option, + opacity: f32, +} + +impl Default for AppliedWindow { + fn default() -> Self { + Self { + title: String::new(), + position: IVec2::ZERO, + size: bevy::math::UVec2::ZERO, + visible: true, + resizable: true, + decorations: true, + window_level: BevyWindowLevel::Normal, + fullscreen_on: None, + opacity: 1.0, + } + } } impl GlfwContext { @@ -22,6 +63,7 @@ impl GlfwContext { glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi)); glfw.window_hint(glfw::WindowHint::Visible(false)); + glfw.window_hint(glfw::WindowHint::TransparentFramebuffer(true)); let (mut window, events) = glfw .create_window(width, height, "Processing", WindowMode::Windowed) @@ -35,9 +77,107 @@ impl GlfwContext { window, events, surface: None, + last_applied: AppliedWindow::default(), + windowed_geometry: None, }) } + fn sync_monitors(&mut self) { + let primary_name = self + .glfw + .with_primary_monitor(|_, monitor| monitor.and_then(|m| m.get_name())); + + self.glfw.with_connected_monitors(|_, monitors| { + let _ = app_mut(|app| { + let world = app.world_mut(); + let mut existing: std::collections::HashMap = world + .iter_entities() + .filter_map(|e| { + let name = e.get::()?.name.clone()?; + Some((name, e.id())) + }) + .collect(); + + for monitor in monitors { + let name = monitor.get_name(); + let video_mode = monitor.get_video_mode(); + let (width, height) = video_mode + .as_ref() + .map(|v| (v.width, v.height)) + .unwrap_or((0, 0)); + let refresh_millihz = video_mode.as_ref().map(|v| v.refresh_rate * 1000); + let (x, y) = monitor.get_pos(); + let (wx, wy, ww, wh) = monitor.get_workarea(); + let (scale, _) = monitor.get_content_scale(); + let position = IVec2::new(x, y); + let workarea = + IRect::from_corners(IVec2::new(wx, wy), IVec2::new(wx + ww, wy + wh)); + let video_modes: Vec = monitor + .get_video_modes() + .into_iter() + .map(|v| BevyVideoMode { + physical_size: bevy::math::UVec2::new(v.width, v.height), + bit_depth: (v.red_bits + v.green_bits + v.blue_bits) as u16, + refresh_rate_millihertz: v.refresh_rate * 1000, + }) + .collect(); + + let entity = match name.as_ref().and_then(|n| existing.remove(n)) { + Some(entity) => { + if let Some(mut bevy_monitor) = world.get_mut::(entity) { + bevy_monitor.physical_width = width; + bevy_monitor.physical_height = height; + bevy_monitor.physical_position = position; + bevy_monitor.refresh_rate_millihertz = refresh_millihz; + bevy_monitor.scale_factor = scale as f64; + bevy_monitor.video_modes = video_modes; + } + match world.get_mut::(entity) { + Some(mut current) => current.0 = workarea, + None => { + world.entity_mut(entity).insert(MonitorWorkarea(workarea)); + } + } + entity + } + None => world + .spawn(( + BevyMonitor { + name: name.clone(), + physical_height: height, + physical_width: width, + physical_position: position, + refresh_rate_millihertz: refresh_millihz, + scale_factor: scale as f64, + video_modes, + }, + MonitorWorkarea(workarea), + )) + .id(), + }; + + let is_primary = name.is_some() && name == primary_name; + let was_primary = world.get::(entity).is_some(); + match (is_primary, was_primary) { + (true, false) => { + world.entity_mut(entity).insert(PrimaryMonitor); + } + (false, true) => { + world.entity_mut(entity).remove::(); + } + _ => {} + } + } + + for (_, entity) in existing { + world.entity_mut(entity).despawn(); + } + + Ok(()) + }); + }); + } + #[cfg(target_os = "macos")] pub fn create_surface(&mut self, width: u32, height: u32) -> Result { use processing_render::surface_create_macos; @@ -173,10 +313,142 @@ impl GlfwContext { return false; }; self.sync_cursor(surface); + self.sync_monitors(); + self.sync_window(surface); true } + fn sync_window(&mut self, surface: Entity) { + let Some(desired) = read_desired_window(surface) else { + return; + }; + + self.apply_window(&desired); + + if desired.iconify { + self.window.iconify(); + } + if desired.restore { + self.window.restore(); + } + if desired.maximize { + self.window.maximize(); + } + if desired.focus { + self.window.focus(); + } + + let (cx, cy) = self.window.get_pos(); + let (inset_l, inset_t, _, _) = self.window.get_frame_size(); + let frame_pos = IVec2::new(cx - inset_l, cy - inset_t); + let _ = app_mut(|app| { + let world = app.world_mut(); + if let Some(mut window) = world.get_mut::(surface) { + window.position = WindowPosition::At(frame_pos); + } + if let Some(mut controls) = world.get_mut::(surface) { + controls.pending_iconify = false; + controls.pending_restore = false; + controls.pending_maximize = false; + controls.pending_focus = false; + } + Ok(()) + }); + self.last_applied.position = frame_pos; + } + + fn apply_window(&mut self, desired: &DesiredWindow) { + let last = &mut self.last_applied; + + if desired.title != last.title { + self.window.set_title(&desired.title); + last.title.clone_from(&desired.title); + } + if let Some(pos) = desired.position + && pos != last.position + { + let (inset_l, inset_t, _, _) = self.window.get_frame_size(); + self.window.set_pos(pos.x + inset_l, pos.y + inset_t); + last.position = pos; + } + if desired.size != last.size && desired.size.x > 0 && desired.size.y > 0 { + self.window + .set_size(desired.size.x as i32, desired.size.y as i32); + last.size = desired.size; + } + if desired.visible != last.visible { + if desired.visible { + self.window.show(); + } else { + self.window.hide(); + } + last.visible = desired.visible; + } + if desired.resizable != last.resizable { + self.window.set_resizable(desired.resizable); + last.resizable = desired.resizable; + } + if desired.decorations != last.decorations { + self.window.set_decorated(desired.decorations); + last.decorations = desired.decorations; + } + if desired.window_level != last.window_level { + self.window + .set_floating(matches!(desired.window_level, BevyWindowLevel::AlwaysOnTop)); + last.window_level = desired.window_level; + } + if let Some(opacity) = desired.opacity + && (opacity - last.opacity).abs() > f32::EPSILON + { + self.window.set_opacity(opacity); + last.opacity = opacity; + } + if desired.fullscreen_on != last.fullscreen_on { + self.apply_fullscreen(desired.fullscreen_on); + } + } + + fn apply_fullscreen(&mut self, target: Option) { + match target { + Some(monitor_entity) => { + if self.last_applied.fullscreen_on.is_none() { + let (x, y) = self.window.get_pos(); + let (w, h) = self.window.get_size(); + self.windowed_geometry = Some((x, y, w as u32, h as u32)); + } + let target_name = monitor_name(monitor_entity); + let window = &mut self.window; + let applied = self.glfw.with_connected_monitors(|_, monitors| { + let Some(monitor) = monitors + .iter() + .find(|m| m.get_name() == target_name) + .map(|m| &**m) + else { + return false; + }; + let (w, h, refresh) = monitor + .get_video_mode() + .map(|v| (v.width, v.height, Some(v.refresh_rate))) + .unwrap_or((1920, 1080, None)); + window.set_monitor(WindowMode::FullScreen(monitor), 0, 0, w, h, refresh); + true + }); + self.last_applied.fullscreen_on = applied.then_some(monitor_entity); + } + None => { + let (x, y, w, h) = self.windowed_geometry.take().unwrap_or_else(|| { + let (x, y) = self.window.get_pos(); + let (w, h) = self.window.get_size(); + (x, y, w as u32, h as u32) + }); + self.window + .set_monitor(WindowMode::Windowed, x, y, w, h, None); + self.last_applied.fullscreen_on = None; + } + } + } + pub fn content_scale(&self) -> f32 { let (s, _) = self.window.get_content_scale(); s @@ -200,6 +472,95 @@ impl GlfwContext { } } +#[derive(Clone, Debug)] +struct DesiredWindow { + title: String, + position: Option, + size: bevy::math::UVec2, + visible: bool, + resizable: bool, + decorations: bool, + window_level: BevyWindowLevel, + fullscreen_on: Option, + opacity: Option, + iconify: bool, + restore: bool, + maximize: bool, + focus: bool, +} + +fn read_desired_window(surface: Entity) -> Option { + app_mut(|app| { + let world = app.world(); + let Some(window) = world.get::(surface) else { + return Ok(None); + }; + let controls = world + .get::(surface) + .cloned() + .unwrap_or_default(); + let fullscreen_on = match window.mode { + BevyWindowMode::Windowed => None, + BevyWindowMode::BorderlessFullscreen(sel) | BevyWindowMode::Fullscreen(sel, _) => { + resolve_monitor(world, sel) + } + }; + Ok(Some(DesiredWindow { + title: window.title.clone(), + position: match window.position { + WindowPosition::At(p) => Some(p), + _ => None, + }, + size: bevy::math::UVec2::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ), + visible: window.visible, + resizable: window.resizable, + decorations: window.decorations, + window_level: window.window_level, + fullscreen_on, + opacity: controls.opacity, + iconify: controls.pending_iconify, + restore: controls.pending_restore, + maximize: controls.pending_maximize, + focus: controls.pending_focus, + })) + }) + .ok() + .flatten() +} + +fn resolve_monitor(world: &bevy::ecs::world::World, sel: MonitorSelection) -> Option { + match sel { + MonitorSelection::Entity(e) => world.get::(e).map(|_| e), + MonitorSelection::Primary | MonitorSelection::Current => world + .iter_entities() + .find(|e| e.contains::() && e.contains::()) + .map(|e| e.id()), + MonitorSelection::Index(idx) => { + let mut entities: Vec = world + .iter_entities() + .filter(|e| e.contains::()) + .map(|e| e.id()) + .collect(); + entities.sort(); + entities.get(idx).copied() + } + } +} + +fn monitor_name(entity: Entity) -> Option { + app_mut(|app| { + Ok(app + .world() + .get::(entity) + .and_then(|m| m.name.clone())) + }) + .ok() + .flatten() +} + fn glfw_button_to_bevy(button: glfw::MouseButton) -> Option { match button { glfw::MouseButtonLeft => Some(MouseButton::Left), diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index 69c845af..13b74236 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing_pyo3" -version = "0.0.3" +version = "0.0.4" edition = "2024" [lints] @@ -26,3 +26,5 @@ processing_glfw = { workspace = true } bevy = { workspace = true, features = ["file_watcher"] } png = "0.18" processing_cuda = { workspace = true, optional = true } +rand = { workspace = true } +rand_distr = { workspace = true } diff --git a/crates/processing_pyo3/examples/window_controls.py b/crates/processing_pyo3/examples/window_controls.py new file mode 100644 index 00000000..cef60bd5 --- /dev/null +++ b/crates/processing_pyo3/examples/window_controls.py @@ -0,0 +1,127 @@ +from mewnala import * + +TITLES = ["window controls", "🪟 hello", "still alive"] +POSITIONS = [(100, 100), (300, 200), (50, 50)] +SIZES = [(640, 480), (800, 600), (320, 240)] +OPACITIES = [1.0, 0.85, 0.5] + +state = { + "title": 0, + "position": 0, + "size": 0, + "opacity": 0, + "resizable": True, + "decorated": True, + "on_top": False, + "fullscreen": False, + "show_at": -1, +} + + +def setup(): + size(640, 480) + window_title(TITLES[0]) + + +def log(label): + print(f"[frame {frame_count}] {label}") + + +def cycle(key, label): + state[key] = (state[key] + 1) % len(label) + return state[key] + + +def draw(): + if 0 <= state["show_at"] <= frame_count: + window_visible(True) + state["show_at"] = -1 + log("re-shown") + + if key_just_pressed(KEY_T): + idx = cycle("title", TITLES) + window_title(TITLES[idx]) + log(f"title -> {TITLES[idx]!r}") + + if key_just_pressed(KEY_M): + idx = cycle("position", POSITIONS) + x, y = POSITIONS[idx] + window_move(x, y) + log(f"moved to ({x}, {y})") + + if key_just_pressed(KEY_W): + idx = cycle("size", SIZES) + w, h = SIZES[idx] + window_resize(w, h) + log(f"resized to {w}x{h}") + + if key_just_pressed(KEY_O): + idx = cycle("opacity", OPACITIES) + window_opacity(OPACITIES[idx]) + log(f"opacity -> {OPACITIES[idx]}") + + if key_just_pressed(KEY_R): + state["resizable"] = not state["resizable"] + window_resizable(state["resizable"]) + log(f"resizable -> {state['resizable']}") + + if key_just_pressed(KEY_D): + state["decorated"] = not state["decorated"] + window_decorated(state["decorated"]) + log(f"decorated -> {state['decorated']}") + + if key_just_pressed(KEY_A): + state["on_top"] = not state["on_top"] + window_always_on_top(state["on_top"]) + log(f"always-on-top -> {state['on_top']}") + + if key_just_pressed(KEY_V): + window_visible(False) + state["show_at"] = frame_count + 60 + log("hidden for ~1s") + + if key_just_pressed(KEY_I): + window_iconify() + log("iconified") + + if key_just_pressed(KEY_X): + window_maximize() + log("maximized") + + if key_just_pressed(KEY_N): + window_restore() + log("restored") + + if key_just_pressed(KEY_F): + state["fullscreen"] = not state["fullscreen"] + full_screen(primary_monitor() if state["fullscreen"] else None) + log(f"fullscreen -> {state['fullscreen']}") + + if key_just_pressed(KEY_C): + if (m := primary_monitor()) is not None: + window_center_on(m) + log(f"centered on {m.name!r}") + + if key_just_pressed(KEY_P): + if (m := primary_monitor()) is not None: + window_position_on(m, 10, 10) + log(f"workarea +(10, 10) — workarea={m.workarea}") + + background(24) + no_stroke() + fill(80, 200, 200) + rect(20, 20, width - 40, 40) + fill(200, 80, 120) + rect(20, 80, width - 40, 60) + fill(120, 80, 200) + rect(20, 160, width - 40, 60) + fill(80, 200, 120) + rect(20, 240, width - 40, 60) + + # Yellow dot tracks window_x / window_y wrapped into the canvas. + fill(255, 220, 60) + if width > 0 and height > 0: + circle(window_x % width, window_y % height, 12) + + +run() diff --git a/crates/processing_pyo3/mewnala/__init__.py b/crates/processing_pyo3/mewnala/__init__.py index 4427ced4..13b1b81a 100644 --- a/crates/processing_pyo3/mewnala/__init__.py +++ b/crates/processing_pyo3/mewnala/__init__.py @@ -17,32 +17,54 @@ "width", "height", "focused", + "pixel_density", "pixel_width", "pixel_height", - "pixel_density", -) -_DYNAMIC_FUNCTIONS = ( "mouse_x", "mouse_y", "pmouse_x", "pmouse_y", + "mouse_is_pressed", + "mouse_button", + "mouse_wheel", + "moved_x", + "moved_y", + "key", + "key_code", + "key_is_pressed", +) + +_DYNAMIC_TIME_ATTRS = ( "frame_count", "delta_time", "elapsed_time", ) + +_DEFAULT_GRAPHICS_VALUES = { + "width": 100, + "height": 100, + "focused": False, + "pixel_density": 1.0, + "pixel_width": 100, + "pixel_height": 100, + "mouse_x": 0.0, + "mouse_y": 0.0, + "pmouse_x": 0.0, + "pmouse_y": 0.0, + "mouse_is_pressed": False, + "mouse_button": None, + "mouse_wheel": 0.0, + "moved_x": 0.0, + "moved_y": 0.0, + "key": None, + "key_code": None, + "key_is_pressed": False, +} + _DYNAMIC = ( - _DYNAMIC_GRAPHICS_ATTRS + _DYNAMIC_FUNCTIONS + ( - "mouse_is_pressed", - "mouse_button", - "moved_x", - "moved_y", - "mouse_wheel", - "key", - "key_code", - "key_is_pressed", - "display_width", - "display_height", - ) + _DYNAMIC_GRAPHICS_ATTRS + + _DYNAMIC_TIME_ATTRS + + ("display_width", "display_height", "window_x", "window_y") ) @@ -55,15 +77,29 @@ def __getattr__(name): g = _get_graphics() if g is not None: return getattr(g, name) - if name in _DYNAMIC_FUNCTIONS: - fn = getattr(_native, name, None) - if callable(fn): + return _DEFAULT_GRAPHICS_VALUES[name] + if name in _DYNAMIC_TIME_ATTRS: + fn = getattr(_native, f"_dyn_{name}", None) + if not callable(fn): + return 0 + try: return fn() + except RuntimeError: + return 0 if name == "frame_count" else 0.0 if name in ("display_width", "display_height"): - mon = getattr(_native, "primary_monitor", lambda: None)() + try: + mon = getattr(_native, "primary_monitor", lambda: None)() + except RuntimeError: + return 0 if mon is None: return 0 return mon.width if name == "display_width" else mon.height + if name in ("window_x", "window_y"): + g = _get_graphics() + if g is None: + return 0 + x, y = g.surface.position + return x if name == "window_x" else y raise AttributeError(f"module {__name__!r} has no attribute {name!r}") @@ -71,4 +107,8 @@ def __dir__(): return sorted(set(list(globals().keys()) + list(_DYNAMIC))) +__all__ = sorted( + {n for n in dir(_native) if not n.startswith("_")} | set(_DYNAMIC) +) + del _sys, _name, _sub diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index bac01371..cafd305d 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -1181,22 +1181,22 @@ impl Graphics { #[getter] fn mouse_x(&self) -> PyResult { - input::mouse_x(self.surface.entity) + input::mouse_x(self.surface.entity, self.width) } #[getter] fn mouse_y(&self) -> PyResult { - input::mouse_y(self.surface.entity) + input::mouse_y(self.surface.entity, self.height) } #[getter] fn pmouse_x(&self) -> PyResult { - input::pmouse_x(self.surface.entity) + input::pmouse_x(self.surface.entity, self.width) } #[getter] fn pmouse_y(&self) -> PyResult { - input::pmouse_y(self.surface.entity) + input::pmouse_y(self.surface.entity, self.height) } #[getter] diff --git a/crates/processing_pyo3/src/input.rs b/crates/processing_pyo3/src/input.rs index 5096d5cb..de40a2ef 100644 --- a/crates/processing_pyo3/src/input.rs +++ b/crates/processing_pyo3/src/input.rs @@ -5,22 +5,28 @@ use pyo3::{ prelude::*, }; -pub fn mouse_x(surface: Entity) -> PyResult { - processing::prelude::input_mouse_x(surface).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) +pub fn mouse_x(surface: Entity, width: u32) -> PyResult { + let raw = processing::prelude::input_mouse_x(surface) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(raw.clamp(0.0, width as f32)) } -pub fn mouse_y(surface: Entity) -> PyResult { - processing::prelude::input_mouse_y(surface).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) +pub fn mouse_y(surface: Entity, height: u32) -> PyResult { + let raw = processing::prelude::input_mouse_y(surface) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(raw.clamp(0.0, height as f32)) } -pub fn pmouse_x(surface: Entity) -> PyResult { - processing::prelude::input_pmouse_x(surface) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) +pub fn pmouse_x(surface: Entity, width: u32) -> PyResult { + let raw = processing::prelude::input_pmouse_x(surface) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(raw.clamp(0.0, width as f32)) } -pub fn pmouse_y(surface: Entity) -> PyResult { - processing::prelude::input_pmouse_y(surface) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) +pub fn pmouse_y(surface: Entity, height: u32) -> PyResult { + let raw = processing::prelude::input_pmouse_y(surface) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(raw.clamp(0.0, height as f32)) } pub fn mouse_is_pressed() -> PyResult { @@ -80,12 +86,17 @@ pub fn key_code() -> PyResult> { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } -pub fn sync_globals(globals: &Bound<'_, PyAny>, surface: Entity) -> PyResult<()> { +pub fn sync_globals( + globals: &Bound<'_, PyAny>, + surface: Entity, + canvas_width: u32, + canvas_height: u32, +) -> PyResult<()> { use crate::set_tracked; - set_tracked(globals, "mouse_x", mouse_x(surface)?)?; - set_tracked(globals, "mouse_y", mouse_y(surface)?)?; - set_tracked(globals, "pmouse_x", pmouse_x(surface)?)?; - set_tracked(globals, "pmouse_y", pmouse_y(surface)?)?; + set_tracked(globals, "mouse_x", mouse_x(surface, canvas_width)?)?; + set_tracked(globals, "mouse_y", mouse_y(surface, canvas_height)?)?; + set_tracked(globals, "pmouse_x", pmouse_x(surface, canvas_width)?)?; + set_tracked(globals, "pmouse_y", pmouse_y(surface, canvas_height)?)?; set_tracked(globals, "mouse_is_pressed", mouse_is_pressed()?)?; set_tracked(globals, "mouse_button", mouse_button()?)?; set_tracked(globals, "moved_x", moved_x()?)?; diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 2e889c0f..79029796 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -55,7 +55,7 @@ impl Default for LoopState { fn default() -> Self { Self { looping: true, - redraw_requested: true, + redraw_requested: false, } } } @@ -116,7 +116,12 @@ pub(crate) fn reset_tracked_globals() { fn sync_globals(module: &Bound<'_, PyModule>, globals: &Bound<'_, PyAny>) -> PyResult<()> { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - input::sync_globals(globals, graphics.surface.entity)?; + input::sync_globals( + globals, + graphics.surface.entity, + graphics.width, + graphics.height, + )?; surface::sync_globals(globals, &graphics.surface, graphics.width, graphics.height)?; time::sync_globals(globals)?; Ok(()) @@ -598,9 +603,6 @@ mod mewnala { fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { use processing::prelude::BlendMode; - module.add("width", super::DEFAULT_WIDTH)?; - module.add("height", super::DEFAULT_HEIGHT)?; - module.add("BLEND", PyBlendMode::from_preset(BlendMode::Blend))?; module.add("ADD", PyBlendMode::from_preset(BlendMode::Add))?; module.add("SUBTRACT", PyBlendMode::from_preset(BlendMode::Subtract))?; @@ -839,6 +841,144 @@ mod mewnala { Ok(()) } + #[pyfunction] + #[pyo3(pass_module)] + fn window_title(module: &Bound<'_, PyModule>, title: &str) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_title(title) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_move(module: &Bound<'_, PyModule>, x: i32, y: i32) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_position(x, y) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_resize(module: &Bound<'_, PyModule>, w: u32, h: u32) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + ::processing::prelude::surface_resize(graphics.surface.entity, w, h) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_resizable(module: &Bound<'_, PyModule>, resizable: bool) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_resizable(resizable) + } + + #[pyfunction] + #[pyo3(pass_module, signature = (monitor=None))] + fn full_screen( + module: &Bound<'_, PyModule>, + monitor: Option<&crate::monitor::Monitor>, + ) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_fullscreen(monitor) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_visible(module: &Bound<'_, PyModule>, visible: bool) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_visible(visible) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_decorated(module: &Bound<'_, PyModule>, decorated: bool) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_decorated(decorated) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_always_on_top(module: &Bound<'_, PyModule>, on_top: bool) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_always_on_top(on_top) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_opacity(module: &Bound<'_, PyModule>, opacity: f32) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.set_opacity(opacity) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_iconify(module: &Bound<'_, PyModule>) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.iconify() + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_restore(module: &Bound<'_, PyModule>) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.restore() + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_maximize(module: &Bound<'_, PyModule>) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.maximize() + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_center_on( + module: &Bound<'_, PyModule>, + monitor: &crate::monitor::Monitor, + ) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.center_on(monitor) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn window_position_on( + module: &Bound<'_, PyModule>, + monitor: &crate::monitor::Monitor, + x: i32, + y: i32, + ) -> PyResult<()> { + let Some(graphics) = get_graphics(module)? else { + return Ok(()); + }; + graphics.surface.position_on(monitor, x, y) + } + #[pyfunction] #[pyo3(pass_module)] fn size(module: &Bound<'_, PyModule>, width: u32, height: u32) -> PyResult<()> { @@ -915,6 +1055,7 @@ mod mewnala { return Ok(()); } let draw_fn_ref = draw_fn.as_mut().expect("checked above"); + let mut first_frame = true; loop { { @@ -939,6 +1080,7 @@ mod mewnala { *draw_fn_ref = locals.get_item("draw").unwrap().unwrap(); globals = draw_fn_ref.getattr("__globals__")?; reset_tracked_globals(); + first_frame = true; dbg!(locals); } @@ -950,15 +1092,17 @@ mod mewnala { dispatch_event_callbacks(&locals)?; - let should_draw = LOOP_STATE.with(|s| { - let state = s.get(); - state.looping || state.redraw_requested - }); + let should_draw = first_frame + || LOOP_STATE.with(|s| { + let state = s.get(); + state.looping || state.redraw_requested + }); if !should_draw { std::thread::sleep(std::time::Duration::from_millis(16)); continue; } + first_frame = false; get_graphics_mut(module)? .ok_or_else(|| PyRuntimeError::new_err("call size() first"))? @@ -1615,38 +1759,6 @@ mod mewnala { midi::play_notes(note, duration) } - #[pyfunction] - #[pyo3(pass_module)] - fn mouse_x(module: &Bound<'_, PyModule>) -> PyResult { - let graphics = - get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - input::mouse_x(graphics.surface.entity) - } - - #[pyfunction] - #[pyo3(pass_module)] - fn mouse_y(module: &Bound<'_, PyModule>) -> PyResult { - let graphics = - get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - input::mouse_y(graphics.surface.entity) - } - - #[pyfunction] - #[pyo3(pass_module)] - fn pmouse_x(module: &Bound<'_, PyModule>) -> PyResult { - let graphics = - get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - input::pmouse_x(graphics.surface.entity) - } - - #[pyfunction] - #[pyo3(pass_module)] - fn pmouse_y(module: &Bound<'_, PyModule>) -> PyResult { - let graphics = - get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - input::pmouse_y(graphics.surface.entity) - } - #[pyfunction] fn key_is_down(key_code: u32) -> PyResult { input::key_is_down(key_code) @@ -1658,9 +1770,24 @@ mod mewnala { } #[pyfunction] - #[pyo3(pass_module)] - fn pixel_density(module: &Bound<'_, PyModule>, density: f32) -> PyResult<()> { - graphics!(module).surface.set_pixel_density(density) + #[pyo3(pass_module, signature = (density=None))] + fn pixel_density<'py>( + module: &Bound<'py, PyModule>, + density: Option, + ) -> PyResult> { + let py = module.py(); + match density { + Some(d) => { + graphics!(module).surface.set_pixel_density(d)?; + Ok(py.None()) + } + None => { + let graphics = get_graphics(module)? + .ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; + let current = graphics.surface.pixel_density()?; + Ok(current.into_pyobject(py)?.into_any().unbind()) + } + } } #[pyfunction] @@ -1671,18 +1798,20 @@ mod mewnala { graphics.surface.display_density() } + // private stuff + #[pyfunction] - fn frame_count() -> PyResult { + fn _dyn_frame_count() -> PyResult { time::frame_count() } #[pyfunction] - fn delta_time() -> PyResult { + fn _dyn_delta_time() -> PyResult { time::delta_time() } #[pyfunction] - fn elapsed_time() -> PyResult { + fn _dyn_elapsed_time() -> PyResult { time::elapsed_time() } diff --git a/crates/processing_pyo3/src/math.rs b/crates/processing_pyo3/src/math.rs index bf166ca4..92d3c5cc 100644 --- a/crates/processing_pyo3/src/math.rs +++ b/crates/processing_pyo3/src/math.rs @@ -1,7 +1,11 @@ use std::hash::{Hash, Hasher}; -use bevy::math::{EulerRot, Quat, Vec2, Vec3, Vec4}; -use pyo3::{exceptions::PyTypeError, prelude::*, types::PyTuple}; +use bevy::math::{EulerRot, Quat, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use pyo3::{ + exceptions::{PyAttributeError, PyTypeError}, + prelude::*, + types::PyTuple, +}; pub fn hash_f32(val: f32, state: &mut impl Hasher) { if val == 0.0 { @@ -291,6 +295,50 @@ macro_rules! impl_py_vec { } } + fn __getattr__<'py>( + slf: PyRef<'py, Self>, + name: &str, + ) -> PyResult> { + let py = slf.py(); + let chars: Vec = name.chars().collect(); + let len = chars.len(); + let attr_err = || { + PyAttributeError::new_err(format!( + "'{}' object has no attribute '{}'", + $py_name, name + )) + }; + if !(2..=4).contains(&len) { + return Err(attr_err()); + } + let mut vals = [0.0f32; 4]; + for (i, c) in chars.iter().enumerate() { + let idx: usize = match c { + 'x' => 0, + 'y' => 1, + 'z' => 2, + 'w' => 3, + _ => return Err(attr_err()), + }; + if idx >= $n { + return Err(attr_err()); + } + vals[i] = slf.0[idx]; + } + Ok(match len { + 2 => PyVec2(Vec2::new(vals[0], vals[1])) + .into_pyobject(py)? + .into_any(), + 3 => PyVec3(Vec3::new(vals[0], vals[1], vals[2])) + .into_pyobject(py)? + .into_any(), + 4 => PyVec4(Vec4::new(vals[0], vals[1], vals[2], vals[3])) + .into_pyobject(py)? + .into_any(), + _ => unreachable!(), + }) + } + fn length(&self) -> f32 { self.0.length() } fn length_squared(&self) -> f32 { self.0.length_squared() } fn normalize(&self) -> Self { Self(self.0.normalize()) } @@ -312,6 +360,144 @@ macro_rules! impl_py_vec { PyTuple::new(py, self.0.to_array()).unwrap() } + fn mag(&self) -> f32 { self.0.length() } + fn mag_sq(&self) -> f32 { self.0.length_squared() } + fn dist(&self, other: &Self) -> f32 { self.0.distance(other.0) } + + fn copy(&self) -> Self { Self(self.0) } + + fn equals(&self, other: &Self) -> bool { self.0 == other.0 } + + fn hash_code(&self) -> u64 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + for &c in self.0.to_array().iter() { + hash_f32(c, &mut hasher); + } + std::hash::Hasher::finish(&hasher) + } + + #[pyo3(signature = (*args))] + fn set(&mut self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + match args.len() { + 1 => { + let first = args.get_item(0)?; + if let Ok(s) = first.extract::() { + self.0 = <$glam_ty>::splat(s); + return Ok(()); + } + if let Ok(s) = first.extract::() { + self.0 = <$glam_ty>::splat(s as f32); + return Ok(()); + } + if let Ok(v) = first.extract::>() { + self.0 = v.0; + return Ok(()); + } + if let Ok(arr) = first.extract::<[f32; $n]>() { + self.0 = <$glam_ty>::from_array(arr); + return Ok(()); + } + Err(PyTypeError::new_err(concat!( + "expected scalar, ", $py_name, ", or sequence of ", + stringify!($n), " floats" + ))) + } + $n => { + let mut arr = [0.0f32; $n]; + $(arr[$idx] = args.get_item($idx)?.extract::()?;)+ + self.0 = <$glam_ty>::from_array(arr); + Ok(()) + } + _ => Err(PyTypeError::new_err(concat!( + $py_name, ".set takes 1 or ", stringify!($n), " arguments" + ))), + } + } + + #[pyo3(signature = (*args))] + fn add(&mut self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + match args.len() { + 1 => { + let first = args.get_item(0)?; + if let Ok(v) = first.extract::>() { + self.0 += v.0; + return Ok(()); + } + if let Ok(arr) = first.extract::<[f32; $n]>() { + self.0 += <$glam_ty>::from_array(arr); + return Ok(()); + } + Err(PyTypeError::new_err(concat!( + "expected ", $py_name, " or sequence of ", + stringify!($n), " floats" + ))) + } + $n => { + let mut arr = [0.0f32; $n]; + $(arr[$idx] = args.get_item($idx)?.extract::()?;)+ + self.0 += <$glam_ty>::from_array(arr); + Ok(()) + } + _ => Err(PyTypeError::new_err(concat!( + $py_name, ".add takes 1 or ", stringify!($n), " arguments" + ))), + } + } + + #[pyo3(signature = (*args))] + fn sub(&mut self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + match args.len() { + 1 => { + let first = args.get_item(0)?; + if let Ok(v) = first.extract::>() { + self.0 -= v.0; + return Ok(()); + } + if let Ok(arr) = first.extract::<[f32; $n]>() { + self.0 -= <$glam_ty>::from_array(arr); + return Ok(()); + } + Err(PyTypeError::new_err(concat!( + "expected ", $py_name, " or sequence of ", + stringify!($n), " floats" + ))) + } + $n => { + let mut arr = [0.0f32; $n]; + $(arr[$idx] = args.get_item($idx)?.extract::()?;)+ + self.0 -= <$glam_ty>::from_array(arr); + Ok(()) + } + _ => Err(PyTypeError::new_err(concat!( + $py_name, ".sub takes 1 or ", stringify!($n), " arguments" + ))), + } + } + + fn mult(&mut self, n: f32) { self.0 *= n; } + fn div(&mut self, n: f32) { self.0 /= n; } + + fn set_mag(&mut self, len: f32) { + self.0 = self.0.normalize_or_zero() * len; + } + + fn limit(&mut self, max: f32) { + if self.0.length_squared() > max * max { + self.0 = self.0.normalize_or_zero() * max; + } + } + + #[staticmethod] + fn angle_between(v1: &Self, v2: &Self) -> f32 { + if v1.0.length_squared() == 0.0 || v2.0.length_squared() == 0.0 { + return 0.0; + } + let cos_angle = (v1.0.dot(v2.0) + / (v1.0.length() * v2.0.length())) + .clamp(-1.0, 1.0); + cos_angle.acos() + } + $($($extra)*)? } }; @@ -322,6 +508,10 @@ impl_py_vec!(PyVec2, "Vec2", 2, [(x, set_x, 0), (y, set_y, 1)], Vec2, extra { self.0.y.atan2(self.0.x) } + fn heading(&self) -> f32 { + self.0.y.atan2(self.0.x) + } + fn rotate(&self, angle: f32) -> Self { Self(Vec2::from_angle(angle).rotate(self.0)) } @@ -329,12 +519,48 @@ impl_py_vec!(PyVec2, "Vec2", 2, [(x, set_x, 0), (y, set_y, 1)], Vec2, extra { fn perpendicular(&self) -> Self { Self(self.0.perp()) } + + fn set_heading(&mut self, angle: f32) { + let mag = self.0.length(); + self.0 = Vec2::new(angle.cos() * mag, angle.sin() * mag); + } + + #[staticmethod] + fn from_angle(angle: f32) -> Self { + Self(Vec2::from_angle(angle)) + } + + #[staticmethod] + fn random() -> Self { + use rand_distr::{Distribution, UnitCircle}; + let [x, y]: [f32; 2] = UnitCircle.sample(&mut rand::rng()); + Self(Vec2::new(x, y)) + } + + fn extend(&self, z: f32) -> PyVec3 { + PyVec3(self.0.extend(z)) + } }); impl_py_vec!(PyVec3, "Vec3", 3, [(x, set_x, 0), (y, set_y, 1), (z, set_z, 2)], Vec3, extra { fn cross(&self, other: &Self) -> Self { Self(self.0.cross(other.0)) } + + #[staticmethod] + fn random() -> Self { + use rand_distr::{Distribution, UnitSphere}; + let [x, y, z]: [f32; 3] = UnitSphere.sample(&mut rand::rng()); + Self(Vec3::new(x, y, z)) + } + + fn extend(&self, w: f32) -> PyVec4 { + PyVec4(self.0.extend(w)) + } + + fn truncate(&self) -> PyVec2 { + PyVec2(self.0.truncate()) + } }); impl_py_vec!( @@ -342,7 +568,12 @@ impl_py_vec!( "Vec4", 4, [(x, set_x, 0), (y, set_y, 1), (z, set_z, 2), (w, set_w, 3)], - Vec4 + Vec4, + extra { + fn truncate(&self) -> PyVec3 { + PyVec3(self.0.truncate()) + } + } ); #[pyclass(name = "Quat", from_py_object)] @@ -644,4 +875,140 @@ mod tests { let b = PyVec3(Vec3::new(3.0, 4.0, 0.0)); assert!((a.distance(&b) - 5.0).abs() < 1e-6); } + + #[test] + fn test_vec3_pvector_aliases() { + let a = PyVec3(Vec3::new(3.0, 4.0, 0.0)); + let b = PyVec3(Vec3::new(0.0, 0.0, 0.0)); + assert_eq!(a.mag(), 5.0); + assert_eq!(a.mag_sq(), 25.0); + assert_eq!(a.dist(&b), 5.0); + } + + #[test] + fn test_vec3_copy_and_equals() { + let a = PyVec3(Vec3::new(1.0, 2.0, 3.0)); + let b = a.copy(); + assert!(a.equals(&b)); + assert_eq!(a.hash_code(), b.hash_code()); + } + + #[test] + fn test_vec3_mut_add_sub() { + let mut v = PyVec3(Vec3::new(1.0, 2.0, 3.0)); + v.0 += Vec3::new(1.0, 1.0, 1.0); + assert_eq!(v.0, Vec3::new(2.0, 3.0, 4.0)); + v.0 -= Vec3::new(2.0, 0.0, 4.0); + assert_eq!(v.0, Vec3::new(0.0, 3.0, 0.0)); + } + + #[test] + fn test_vec3_mult_div() { + let mut v = PyVec3(Vec3::new(1.0, 2.0, 3.0)); + v.mult(2.0); + assert_eq!(v.0, Vec3::new(2.0, 4.0, 6.0)); + v.div(2.0); + assert_eq!(v.0, Vec3::new(1.0, 2.0, 3.0)); + } + + #[test] + fn test_vec3_set_mag() { + let mut v = PyVec3(Vec3::new(3.0, 4.0, 0.0)); + v.set_mag(10.0); + assert!((v.0.length() - 10.0).abs() < 1e-5); + assert!((v.0.x - 6.0).abs() < 1e-5); + assert!((v.0.y - 8.0).abs() < 1e-5); + } + + #[test] + fn test_vec3_set_mag_zero_vec_stays_zero() { + let mut v = PyVec3(Vec3::ZERO); + v.set_mag(5.0); + assert_eq!(v.0, Vec3::ZERO); + } + + #[test] + fn test_vec3_limit() { + let mut v = PyVec3(Vec3::new(3.0, 4.0, 0.0)); + v.limit(10.0); + assert_eq!(v.0, Vec3::new(3.0, 4.0, 0.0)); + v.limit(2.5); + assert!((v.0.length() - 2.5).abs() < 1e-5); + } + + #[test] + fn test_angle_between() { + let a = PyVec3(Vec3::X); + let b = PyVec3(Vec3::Y); + assert!((PyVec3::angle_between(&a, &b) - FRAC_PI_2).abs() < 1e-5); + + let z = PyVec3(Vec3::ZERO); + assert_eq!(PyVec3::angle_between(&a, &z), 0.0); + } + + #[test] + fn test_vec2_heading_alias() { + let v = PyVec2(Vec2::Y); + assert!((v.heading() - FRAC_PI_2).abs() < 1e-6); + } + + #[test] + fn test_vec2_set_heading_preserves_mag() { + let mut v = PyVec2(Vec2::new(3.0, 4.0)); + let mag_before = v.0.length(); + v.set_heading(0.0); + assert!((v.0.length() - mag_before).abs() < 1e-5); + assert!((v.0.x - mag_before).abs() < 1e-5); + assert!(v.0.y.abs() < 1e-5); + } + + #[test] + fn test_vec2_from_angle() { + let v = PyVec2::from_angle(0.0); + assert!((v.0.x - 1.0).abs() < 1e-6); + assert!(v.0.y.abs() < 1e-6); + + let v = PyVec2::from_angle(FRAC_PI_2); + assert!(v.0.x.abs() < 1e-6); + assert!((v.0.y - 1.0).abs() < 1e-6); + } + + #[test] + fn test_vec2_random_is_unit() { + for _ in 0..32 { + let v = PyVec2::random(); + assert!((v.0.length() - 1.0).abs() < 1e-5); + } + } + + #[test] + fn test_vec3_random_is_unit() { + for _ in 0..32 { + let v = PyVec3::random(); + assert!((v.0.length() - 1.0).abs() < 1e-5); + } + } + + #[test] + fn test_vec2_extend() { + let v = PyVec2(Vec2::new(1.0, 2.0)); + let v3 = v.extend(3.0); + assert_eq!(v3.0, Vec3::new(1.0, 2.0, 3.0)); + } + + #[test] + fn test_vec3_extend_truncate() { + let v = PyVec3(Vec3::new(1.0, 2.0, 3.0)); + let v4 = v.extend(4.0); + assert_eq!(v4.0, Vec4::new(1.0, 2.0, 3.0, 4.0)); + let v2 = v.truncate(); + assert_eq!(v2.0, Vec2::new(1.0, 2.0)); + } + + #[test] + fn test_vec4_truncate() { + let v = PyVec4(Vec4::new(1.0, 2.0, 3.0, 4.0)); + let v3 = v.truncate(); + assert_eq!(v3.0, Vec3::new(1.0, 2.0, 3.0)); + } } diff --git a/crates/processing_pyo3/src/monitor.rs b/crates/processing_pyo3/src/monitor.rs index 6c72f862..e30686e7 100644 --- a/crates/processing_pyo3/src/monitor.rs +++ b/crates/processing_pyo3/src/monitor.rs @@ -34,6 +34,20 @@ impl Monitor { pub fn name(&self) -> PyResult> { monitor_name(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + + #[getter] + pub fn position(&self) -> PyResult<(i32, i32)> { + let p = + monitor_position(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok((p.x, p.y)) + } + + #[getter] + pub fn workarea(&self) -> PyResult<(i32, i32, i32, i32)> { + let r = + monitor_workarea(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok((r.min.x, r.min.y, r.width(), r.height())) + } } pub fn primary() -> PyResult> { diff --git a/crates/processing_pyo3/src/surface.rs b/crates/processing_pyo3/src/surface.rs index 98877f8f..81f00932 100644 --- a/crates/processing_pyo3/src/surface.rs +++ b/crates/processing_pyo3/src/surface.rs @@ -1,9 +1,10 @@ use bevy::prelude::Entity; +use bevy::window::{MonitorSelection, WindowLevel, WindowMode}; use processing::prelude::*; use pyo3::{exceptions::PyRuntimeError, prelude::*}; use crate::glfw::GlfwContext; -use crate::monitor; +use crate::monitor::{self, Monitor}; use crate::set_tracked; #[pyclass(unsendable)] @@ -53,6 +54,96 @@ impl Surface { surface_set_pixel_density(self.entity, density) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + + pub fn set_title(&self, title: &str) -> PyResult<()> { + surface_set_title(self.entity, title.to_string()) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + #[getter] + pub fn position(&self) -> PyResult<(i32, i32)> { + let p = + surface_position(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok((p.x, p.y)) + } + + pub fn set_position(&self, x: i32, y: i32) -> PyResult<()> { + surface_set_position(self.entity, x, y).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn set_visible(&self, visible: bool) -> PyResult<()> { + surface_set_visible(self.entity, visible) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn show(&self) -> PyResult<()> { + self.set_visible(true) + } + + pub fn hide(&self) -> PyResult<()> { + self.set_visible(false) + } + + pub fn set_resizable(&self, resizable: bool) -> PyResult<()> { + surface_set_resizable(self.entity, resizable) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn set_decorated(&self, decorated: bool) -> PyResult<()> { + surface_set_decorated(self.entity, decorated) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn set_always_on_top(&self, on_top: bool) -> PyResult<()> { + let level = if on_top { + WindowLevel::AlwaysOnTop + } else { + WindowLevel::Normal + }; + surface_set_window_level(self.entity, level) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn set_opacity(&self, opacity: f32) -> PyResult<()> { + surface_set_opacity(self.entity, opacity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn iconify(&self) -> PyResult<()> { + surface_iconify(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn restore(&self) -> PyResult<()> { + surface_restore(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn maximize(&self) -> PyResult<()> { + surface_maximize(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn focus(&self) -> PyResult<()> { + surface_focus(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + #[pyo3(signature = (monitor=None))] + pub fn set_fullscreen(&self, monitor: Option<&Monitor>) -> PyResult<()> { + let mode = match monitor { + Some(m) => WindowMode::BorderlessFullscreen(MonitorSelection::Entity(m.entity)), + None => WindowMode::Windowed, + }; + surface_set_window_mode(self.entity, mode) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn position_on(&self, monitor: &Monitor, x: i32, y: i32) -> PyResult<()> { + surface_position_on_monitor(self.entity, monitor.entity, x, y) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn center_on(&self, monitor: &Monitor) -> PyResult<()> { + surface_center_on_monitor(self.entity, monitor.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } } impl Drop for Surface { @@ -74,6 +165,10 @@ pub fn sync_globals( set_tracked(globals, "pixel_width", surface.pixel_width()?)?; set_tracked(globals, "pixel_height", surface.pixel_height()?)?; + let (wx, wy) = surface.position().unwrap_or((0, 0)); + set_tracked(globals, "window_x", wx)?; + set_tracked(globals, "window_y", wy)?; + let (dw, dh) = match monitor::primary()? { Some(m) => (m.width()?, m.height()?), None => (0, 0), diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 27636dd6..a1626b19 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -11,7 +11,7 @@ pub mod material; pub mod monitor; pub mod render; pub mod sketch; -pub(crate) mod surface; +pub mod surface; pub mod time; pub mod transform; @@ -247,6 +247,138 @@ pub fn surface_set_pixel_density(entity: Entity, density: f32) -> error::Result< }) } +pub fn surface_set_title(entity: Entity, title: String) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_title, (entity, title)) + .unwrap() + }) +} + +pub fn surface_position(entity: Entity) -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(surface::position, entity) + .unwrap()) + }) +} + +pub fn surface_set_position(entity: Entity, x: i32, y: i32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_position, (entity, x, y)) + .unwrap() + }) +} + +pub fn surface_set_visible(entity: Entity, visible: bool) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_visible, (entity, visible)) + .unwrap() + }) +} + +pub fn surface_set_resizable(entity: Entity, resizable: bool) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_resizable, (entity, resizable)) + .unwrap() + }) +} + +pub fn surface_set_decorated(entity: Entity, decorated: bool) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_decorated, (entity, decorated)) + .unwrap() + }) +} + +pub fn surface_set_window_level( + entity: Entity, + level: bevy::window::WindowLevel, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_window_level, (entity, level)) + .unwrap() + }) +} + +pub fn surface_set_window_mode( + entity: Entity, + mode: bevy::window::WindowMode, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_window_mode, (entity, mode)) + .unwrap() + }) +} + +pub fn surface_set_opacity(entity: Entity, opacity: f32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::set_opacity, (entity, opacity)) + .unwrap() + }) +} + +pub fn surface_iconify(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::iconify, entity) + .unwrap() + }) +} + +pub fn surface_restore(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::restore, entity) + .unwrap() + }) +} + +pub fn surface_maximize(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::maximize, entity) + .unwrap() + }) +} + +pub fn surface_focus(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::focus, entity) + .unwrap() + }) +} + +pub fn surface_position_on_monitor( + surface: Entity, + monitor: Entity, + x: i32, + y: i32, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::position_on_monitor, (surface, monitor, x, y)) + .unwrap() + }) +} + +pub fn surface_center_on_monitor(surface: Entity, monitor: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::center_on_monitor, (surface, monitor)) + .unwrap() + }) +} + /// Create a new graphics surface for rendering. pub fn graphics_create( surface_entity: Entity, @@ -1386,6 +1518,24 @@ pub fn monitor_name(entity: Entity) -> error::Result> { }) } +pub fn monitor_position(entity: Entity) -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(surface::monitor_position, entity) + .unwrap()) + }) +} + +pub fn monitor_workarea(entity: Entity) -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(surface::monitor_workarea, entity) + .unwrap()) + }) +} + pub fn frame_count() -> error::Result { app_mut(|app| { Ok(app diff --git a/crates/processing_render/src/surface.rs b/crates/processing_render/src/surface.rs index ad4c9c5e..2395aaec 100644 --- a/crates/processing_render/src/surface.rs +++ b/crates/processing_render/src/surface.rs @@ -22,9 +22,13 @@ use bevy::{ app::{App, Plugin}, asset::Assets, ecs::query::QueryEntityError, + math::{IRect, IVec2}, prelude::{Commands, Component, Entity, In, Query, ResMut, Window, With, default}, render::render_resource::{Extent3d, TextureFormat}, - window::{RawHandleWrapper, WindowResolution, WindowWrapper}, + window::{ + CompositeAlphaMode, Monitor, RawHandleWrapper, WindowLevel, WindowMode, WindowPosition, + WindowResolution, WindowWrapper, + }, }; use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, @@ -40,6 +44,22 @@ use crate::image::Image; #[derive(Component, Debug, Clone)] pub struct Surface; +/// Window properties Bevy's [`Window`] doesn't model. Backends drain the pending fields +/// each tick. +#[derive(Component, Debug, Default, Clone)] +pub struct WindowControls { + pub opacity: Option, + pub pending_iconify: bool, + pub pending_restore: bool, + pub pending_maximize: bool, + pub pending_focus: bool, +} + +/// Usable region of a monitor (excluding taskbars / menu bars). Populated by the active +/// windowing backend. +#[derive(Component, Debug, Clone, Copy)] +pub struct MonitorWorkarea(pub IRect); + pub struct SurfacePlugin; impl Plugin for SurfacePlugin { @@ -102,10 +122,13 @@ fn spawn_surface( Window { resolution: WindowResolution::new(physical_width, physical_height) .with_scale_factor_override(scale_factor), + transparent: true, + composite_alpha_mode: CompositeAlphaMode::PostMultiplied, ..default() }, handle_wrapper, Surface, + WindowControls::default(), )) .id()) } @@ -371,10 +394,8 @@ pub fn resize( window .resolution .set_physical_resolution(physical_w, physical_h); - Ok(()) - } else { - Err(error::ProcessingError::SurfaceNotFound) } + Ok(()) } pub fn set_pixel_density( @@ -418,3 +439,188 @@ pub fn physical_height(In(entity): In, query: Query<&Window>) -> u32 { .map(|w| w.resolution.physical_height()) .unwrap_or(0) } + +pub fn set_title( + In((entity, title)): In<(Entity, String)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(entity) { + window.title = title; + } + Ok(()) +} + +pub fn position(In(entity): In, windows: Query<&Window>) -> IVec2 { + match windows.get(entity).map(|w| w.position) { + Ok(WindowPosition::At(p)) => p, + _ => IVec2::ZERO, + } +} + +pub fn set_position( + In((entity, x, y)): In<(Entity, i32, i32)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(entity) { + window.position = WindowPosition::At(IVec2::new(x, y)); + } + Ok(()) +} + +pub fn set_visible( + In((entity, visible)): In<(Entity, bool)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(entity) { + window.visible = visible; + } + Ok(()) +} + +pub fn set_resizable( + In((entity, resizable)): In<(Entity, bool)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(entity) { + window.resizable = resizable; + } + Ok(()) +} + +pub fn set_decorated( + In((entity, decorated)): In<(Entity, bool)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(entity) { + window.decorations = decorated; + } + Ok(()) +} + +pub fn set_window_level( + In((entity, level)): In<(Entity, WindowLevel)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(entity) { + window.window_level = level; + } + Ok(()) +} + +pub fn set_window_mode( + In((entity, mode)): In<(Entity, WindowMode)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(entity) { + window.mode = mode; + } + Ok(()) +} + +pub fn set_opacity( + In((entity, opacity)): In<(Entity, f32)>, + mut controls: Query<&mut WindowControls>, +) -> Result<()> { + if let Ok(mut controls) = controls.get_mut(entity) { + controls.opacity = Some(opacity.clamp(0.0, 1.0)); + } + Ok(()) +} + +pub fn iconify(In(entity): In, mut controls: Query<&mut WindowControls>) -> Result<()> { + if let Ok(mut controls) = controls.get_mut(entity) { + controls.pending_iconify = true; + } + Ok(()) +} + +pub fn restore(In(entity): In, mut controls: Query<&mut WindowControls>) -> Result<()> { + if let Ok(mut controls) = controls.get_mut(entity) { + controls.pending_restore = true; + } + Ok(()) +} + +pub fn maximize(In(entity): In, mut controls: Query<&mut WindowControls>) -> Result<()> { + if let Ok(mut controls) = controls.get_mut(entity) { + controls.pending_maximize = true; + } + Ok(()) +} + +pub fn focus(In(entity): In, mut controls: Query<&mut WindowControls>) -> Result<()> { + if let Ok(mut controls) = controls.get_mut(entity) { + controls.pending_focus = true; + } + Ok(()) +} + +pub fn monitor_position(In(entity): In, monitors: Query<&Monitor>) -> IVec2 { + monitors + .get(entity) + .map(|m| m.physical_position) + .unwrap_or(IVec2::ZERO) +} + +pub fn monitor_workarea( + In(entity): In, + monitors: Query<(&Monitor, Option<&MonitorWorkarea>)>, +) -> IRect { + match monitors.get(entity) { + Ok((_, Some(workarea))) => workarea.0, + Ok((monitor, None)) => IRect::from_corners( + monitor.physical_position, + monitor.physical_position + + IVec2::new( + monitor.physical_width as i32, + monitor.physical_height as i32, + ), + ), + Err(_) => IRect::from_corners(IVec2::ZERO, IVec2::ZERO), + } +} + +pub fn position_on_monitor( + In((surface, monitor, x, y)): In<(Entity, Entity, i32, i32)>, + mut windows: Query<&mut Window>, + monitors: Query<(&Monitor, Option<&MonitorWorkarea>)>, +) -> Result<()> { + let Ok(mut window) = windows.get_mut(surface) else { + return Ok(()); + }; + let origin = match monitors.get(monitor) { + Ok((_, Some(workarea))) => workarea.0.min, + Ok((monitor, None)) => monitor.physical_position, + Err(_) => return Ok(()), + }; + window.position = WindowPosition::At(origin + IVec2::new(x, y)); + Ok(()) +} + +pub fn center_on_monitor( + In((surface, monitor)): In<(Entity, Entity)>, + mut windows: Query<&mut Window>, + monitors: Query<(&Monitor, Option<&MonitorWorkarea>)>, +) -> Result<()> { + let Ok(mut window) = windows.get_mut(surface) else { + return Ok(()); + }; + let area = match monitors.get(monitor) { + Ok((_, Some(workarea))) => workarea.0, + Ok((monitor, None)) => IRect::from_corners( + monitor.physical_position, + monitor.physical_position + + IVec2::new( + monitor.physical_width as i32, + monitor.physical_height as i32, + ), + ), + Err(_) => return Ok(()), + }; + let size = IVec2::new( + window.resolution.physical_width() as i32, + window.resolution.physical_height() as i32, + ); + window.position = WindowPosition::At(area.min + (area.size() - size) / 2); + Ok(()) +} diff --git a/tools/generate_stubs/src/main.rs b/tools/generate_stubs/src/main.rs index b80cbed6..e16b159d 100644 --- a/tools/generate_stubs/src/main.rs +++ b/tools/generate_stubs/src/main.rs @@ -1,7 +1,72 @@ +use pyo3_introspection::model::{Argument, Arguments, Expr, Function, Module}; use pyo3_introspection::{introspect_cdylib, module_stub_files}; use std::path::{Path, PathBuf}; use std::{env, fs}; +const SWIZZLE_CHARS: [char; 4] = ['x', 'y', 'z', 'w']; + +fn swizzle_props(dim: usize) -> Vec { + let chars = &SWIZZLE_CHARS[..dim]; + let mut out = Vec::new(); + for length in 2..=4 { + let count = chars.len().pow(length as u32); + for n in 0..count { + let mut name = String::with_capacity(length); + let mut idx = n; + for _ in 0..length { + name.push(chars[idx % chars.len()]); + idx /= chars.len(); + } + out.push(Function { + name, + decorators: vec![Expr::Name { + id: "property".into(), + }], + arguments: Arguments { + positional_only_arguments: vec![Argument { + name: "self".into(), + default_value: None, + annotation: None, + }], + arguments: vec![], + vararg: None, + keyword_only_arguments: vec![], + kwarg: None, + }, + returns: Some(Expr::Attribute { + value: Box::new(Expr::Name { + id: "mewnala.math".into(), + }), + attr: format!("Vec{length}"), + }), + is_async: false, + docstring: None, + }); + } + } + out +} + +fn inject_swizzles(module: &mut Module) { + for math in module.modules.iter_mut().filter(|m| m.name == "math") { + for cls in math.classes.iter_mut() { + let dim = match cls.name.as_str() { + "Vec2" => 2, + "Vec3" => 3, + "Vec4" => 4, + _ => continue, + }; + let existing: std::collections::HashSet = + cls.methods.iter().map(|m| m.name.clone()).collect(); + cls.methods.extend( + swizzle_props(dim) + .into_iter() + .filter(|m| !existing.contains(&m.name)), + ); + } + } +} + fn workspace_root() -> &'static Path { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() @@ -45,8 +110,34 @@ fn main() { introspect_cdylib(&cdylib_path, "mewnala").expect("Failed to introspect cdylib"); module.incomplete = false; + inject_swizzles(&mut module); + + let mut stubs = module_stub_files(&module); - let stubs = module_stub_files(&module); + // join in extras + + let extras_dir = workspace_root() + .join("crates") + .join("processing_pyo3") + .join("stubs"); + if extras_dir.is_dir() { + for entry in fs::read_dir(&extras_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) != Some("pyi") { + continue; + } + let filename = path.file_name().unwrap().to_owned(); + let extra = fs::read_to_string(&path).unwrap(); + let target = stubs.entry(PathBuf::from(&filename)).or_default(); + if !target.is_empty() && !target.ends_with('\n') { + target.push('\n'); + } + target.push('\n'); + target.push_str(&extra); + eprintln!("Appended extras: {}", path.display()); + } + } let output_dir = workspace_root() .join("crates")