From 5fad11ec7b1e371246a1ff495fe90623157573dd Mon Sep 17 00:00:00 2001 From: Borodinov Ilya Date: Mon, 13 May 2024 22:41:30 +0300 Subject: [PATCH] yeah --- .direnv/bin/nix-direnv-reload | 19 + .../05yb0d179k1pw74yxnlhdq9ld30yp9pk-source | 1 + .../3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source | 1 + .../49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source | 1 + .../5a09v6jw29b21vvc35rsv5czv0z0nlq8-source | 1 + .../6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source | 1 + .../a299nv68x7dm4fc9mj60qwrjn31zvw3z-source | 1 + .../ah2qh833bkpxg8girwyl6vs30fkp1109-source | 1 + .../br885sqy62q1bblwi2bslcfg2193ly75-source | 1 + .../gzf4zwcakda1nykn6h0avh45xhjhvsz4-source | 1 + .../nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source | 1 + .../paqmjg18kvzmbrbil9g2mq9k4015fd7p-source | 1 + .../pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source | 1 + .../pgid9c9xfcrbqx2giry0an0bi0df7s5c-source | 1 + .../qkig73szmrhgp0qhncxy5vb36lw2g3jj-source | 1 + .../rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source | 1 + .../vm4qsaala00i8q5js7i3am3w0m766k1d-source | 1 + .../vpddlysgdvzcqixkqgx49zyx2whhzpkb-source | 1 + .../y1nw9w1s0ly6442igksfq29v0cfbnmfd-source | 1 + .../yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source | 1 + .../z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source | 1 + ...e-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa | 1 + ...5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc | 68 + .envrc | 4 + .gitignore | 1 + Cargo.lock | 4105 +++++++++++++++ Cargo.toml | 84 + LICENSE-GPL | 200 + README.md | 34 + crates/collections/Cargo.toml | 19 + crates/collections/src/collections.rs | 15 + crates/ming/Cargo.toml | 138 + crates/ming/README.md | 40 + crates/ming/build.rs | 205 + crates/ming/docs/contexts.md | 41 + crates/ming/docs/key_dispatch.md | 101 + crates/ming/examples/animation.rs | 78 + crates/ming/examples/hello_world.rs | 39 + crates/ming/examples/image/app-icon.png | Bin 0 -> 168173 bytes crates/ming/examples/image/arrow_circle.svg | 6 + crates/ming/examples/image/image.rs | 98 + crates/ming/examples/ownership_post.rs | 35 + crates/ming/examples/set_menus.rs | 43 + crates/ming/examples/window_positioning.rs | 67 + .../ming/resources/windows/gpui.manifest.xml | 16 + crates/ming/resources/windows/gpui.rc | 2 + crates/ming/src/action.rs | 268 + crates/ming/src/app.rs | 1440 +++++ crates/ming/src/app/async_context.rs | 417 ++ crates/ming/src/app/entity_map.rs | 744 +++ crates/ming/src/app/model_context.rs | 293 ++ crates/ming/src/app/test_context.rs | 974 ++++ crates/ming/src/arena.rs | 250 + crates/ming/src/asset_cache.rs | 87 + crates/ming/src/assets.rs | 79 + crates/ming/src/bounds_tree.rs | 292 ++ crates/ming/src/color.rs | 492 ++ crates/ming/src/element.rs | 627 +++ crates/ming/src/elements/anchored.rs | 312 ++ crates/ming/src/elements/animation.rs | 203 + crates/ming/src/elements/canvas.rs | 85 + crates/ming/src/elements/deferred.rs | 85 + crates/ming/src/elements/div.rs | 2546 +++++++++ crates/ming/src/elements/img.rs | 445 ++ crates/ming/src/elements/list.rs | 979 ++++ crates/ming/src/elements/mod.rs | 21 + crates/ming/src/elements/svg.rs | 190 + crates/ming/src/elements/text.rs | 715 +++ crates/ming/src/elements/uniform_list.rs | 312 ++ crates/ming/src/executor.rs | 501 ++ crates/ming/src/geometry.rs | 3037 +++++++++++ crates/ming/src/gpui.rs | 341 ++ crates/ming/src/input.rs | 125 + crates/ming/src/interactive.rs | 510 ++ crates/ming/src/key_dispatch.rs | 608 +++ crates/ming/src/keymap.rs | 183 + crates/ming/src/keymap/binding.rs | 81 + crates/ming/src/keymap/context.rs | 522 ++ crates/ming/src/keymap/matcher.rs | 102 + crates/ming/src/platform.rs | 854 +++ crates/ming/src/platform/app_menu.rs | 115 + crates/ming/src/platform/blade.rs | 8 + crates/ming/src/platform/blade/blade_atlas.rs | 377 ++ crates/ming/src/platform/blade/blade_belt.rs | 101 + .../ming/src/platform/blade/blade_renderer.rs | 753 +++ crates/ming/src/platform/blade/shaders.wgsl | 647 +++ crates/ming/src/platform/cosmic_text.rs | 3 + .../src/platform/cosmic_text/text_system.rs | 516 ++ crates/ming/src/platform/keystroke.rs | 307 ++ crates/ming/src/platform/linux.rs | 14 + crates/ming/src/platform/linux/dispatcher.rs | 125 + crates/ming/src/platform/linux/headless.rs | 3 + .../src/platform/linux/headless/client.rs | 105 + crates/ming/src/platform/linux/platform.rs | 703 +++ crates/ming/src/platform/linux/wayland.rs | 7 + .../ming/src/platform/linux/wayland/client.rs | 1427 +++++ .../ming/src/platform/linux/wayland/cursor.rs | 60 + .../src/platform/linux/wayland/display.rs | 31 + .../ming/src/platform/linux/wayland/serial.rs | 91 + .../ming/src/platform/linux/wayland/window.rs | 765 +++ crates/ming/src/platform/linux/x11.rs | 9 + crates/ming/src/platform/linux/x11/client.rs | 777 +++ crates/ming/src/platform/linux/x11/display.rs | 43 + crates/ming/src/platform/linux/x11/event.rs | 50 + crates/ming/src/platform/linux/x11/window.rs | 722 +++ crates/ming/src/platform/mac.rs | 130 + crates/ming/src/platform/mac/dispatch.h | 2 + crates/ming/src/platform/mac/dispatcher.rs | 107 + crates/ming/src/platform/mac/display.rs | 120 + crates/ming/src/platform/mac/display_link.rs | 267 + crates/ming/src/platform/mac/events.rs | 359 ++ crates/ming/src/platform/mac/metal_atlas.rs | 254 + .../ming/src/platform/mac/metal_renderer.rs | 1167 +++++ crates/ming/src/platform/mac/open_type.rs | 394 ++ crates/ming/src/platform/mac/platform.rs | 1223 +++++ crates/ming/src/platform/mac/shaders.metal | 693 +++ crates/ming/src/platform/mac/status_item.rs | 388 ++ crates/ming/src/platform/mac/text_system.rs | 704 +++ crates/ming/src/platform/mac/window.rs | 1951 +++++++ .../src/platform/mac/window_appearance.rs | 35 + crates/ming/src/platform/test.rs | 10 + crates/ming/src/platform/test/dispatcher.rs | 298 ++ crates/ming/src/platform/test/display.rs | 37 + crates/ming/src/platform/test/platform.rs | 306 ++ crates/ming/src/platform/test/text_system.rs | 50 + crates/ming/src/platform/test/window.rs | 319 ++ crates/ming/src/platform/windows.rs | 19 + .../ming/src/platform/windows/direct_write.rs | 1341 +++++ .../ming/src/platform/windows/dispatcher.rs | 137 + crates/ming/src/platform/windows/display.rs | 199 + crates/ming/src/platform/windows/events.rs | 1274 +++++ crates/ming/src/platform/windows/platform.rs | 872 ++++ .../src/platform/windows/system_settings.rs | 81 + crates/ming/src/platform/windows/util.rs | 156 + crates/ming/src/platform/windows/window.rs | 1032 ++++ crates/ming/src/prelude.rs | 9 + crates/ming/src/scene.rs | 866 +++ crates/ming/src/shared_string.rs | 103 + crates/ming/src/shared_uri.rs | 25 + crates/ming/src/style.rs | 853 +++ crates/ming/src/styled.rs | 835 +++ crates/ming/src/subscription.rs | 171 + crates/ming/src/svg_renderer.rs | 70 + crates/ming/src/taffy.rs | 498 ++ crates/ming/src/test.rs | 113 + crates/ming/src/text_system.rs | 794 +++ crates/ming/src/text_system/font_features.rs | 274 + crates/ming/src/text_system/line.rs | 324 ++ crates/ming/src/text_system/line_layout.rs | 577 ++ crates/ming/src/text_system/line_wrapper.rs | 283 + crates/ming/src/util.rs | 103 + crates/ming/src/view.rs | 469 ++ crates/ming/src/window.rs | 4620 +++++++++++++++++ crates/ming/src/window/prompts.rs | 222 + crates/ming/tests/action_macros.rs | 50 + crates/ming_macros/Cargo.toml | 17 + crates/ming_macros/src/derive_into_element.rs | 23 + crates/ming_macros/src/derive_render.rs | 21 + crates/ming_macros/src/lib.rs | 62 + crates/ming_macros/src/register_action.rs | 48 + crates/ming_macros/src/style_helpers.rs | 569 ++ crates/ming_macros/src/test.rs | 248 + crates/nite/Cargo.toml | 7 + crates/nite/src/main.rs | 3 + crates/refineable/Cargo.toml | 13 + .../refineable/derive_refineable/Cargo.toml | 19 + .../src/derive_refineable.rs | 362 ++ crates/refineable/src/lib.rs | 48 + crates/util/Cargo.toml | 42 + crates/util/src/arc_cow.rs | 135 + crates/util/src/fs.rs | 34 + crates/util/src/http.rs | 242 + crates/util/src/lib.rs | 711 +++ crates/util/src/paths.rs | 622 +++ crates/util/src/semver.rs | 97 + crates/util/src/serde.rs | 3 + crates/util/src/sum_tree.rs | 1339 +++++ crates/util/src/sum_tree/cursor.rs | 751 +++ crates/util/src/sum_tree/tree_map.rs | 463 ++ crates/util/src/test.rs | 65 + crates/util/src/test/assertions.rs | 62 + crates/util/src/test/marked_text.rs | 272 + flake.lock | 435 ++ flake.nix | 77 + 184 files changed, 63932 insertions(+) create mode 100755 .direnv/bin/nix-direnv-reload create mode 120000 .direnv/flake-inputs/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source create mode 120000 .direnv/flake-inputs/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source create mode 120000 .direnv/flake-inputs/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source create mode 120000 .direnv/flake-inputs/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source create mode 120000 .direnv/flake-inputs/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source create mode 120000 .direnv/flake-inputs/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source create mode 120000 .direnv/flake-inputs/ah2qh833bkpxg8girwyl6vs30fkp1109-source create mode 120000 .direnv/flake-inputs/br885sqy62q1bblwi2bslcfg2193ly75-source create mode 120000 .direnv/flake-inputs/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source create mode 120000 .direnv/flake-inputs/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source create mode 120000 .direnv/flake-inputs/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source create mode 120000 .direnv/flake-inputs/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source create mode 120000 .direnv/flake-inputs/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source create mode 120000 .direnv/flake-inputs/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source create mode 120000 .direnv/flake-inputs/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source create mode 120000 .direnv/flake-inputs/vm4qsaala00i8q5js7i3am3w0m766k1d-source create mode 120000 .direnv/flake-inputs/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source create mode 120000 .direnv/flake-inputs/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source create mode 120000 .direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source create mode 120000 .direnv/flake-inputs/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source create mode 120000 .direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa create mode 100644 .direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-GPL create mode 100644 README.md create mode 100644 crates/collections/Cargo.toml create mode 100644 crates/collections/src/collections.rs create mode 100644 crates/ming/Cargo.toml create mode 100644 crates/ming/README.md create mode 100644 crates/ming/build.rs create mode 100644 crates/ming/docs/contexts.md create mode 100644 crates/ming/docs/key_dispatch.md create mode 100644 crates/ming/examples/animation.rs create mode 100644 crates/ming/examples/hello_world.rs create mode 100644 crates/ming/examples/image/app-icon.png create mode 100644 crates/ming/examples/image/arrow_circle.svg create mode 100644 crates/ming/examples/image/image.rs create mode 100644 crates/ming/examples/ownership_post.rs create mode 100644 crates/ming/examples/set_menus.rs create mode 100644 crates/ming/examples/window_positioning.rs create mode 100644 crates/ming/resources/windows/gpui.manifest.xml create mode 100644 crates/ming/resources/windows/gpui.rc create mode 100644 crates/ming/src/action.rs create mode 100644 crates/ming/src/app.rs create mode 100644 crates/ming/src/app/async_context.rs create mode 100644 crates/ming/src/app/entity_map.rs create mode 100644 crates/ming/src/app/model_context.rs create mode 100644 crates/ming/src/app/test_context.rs create mode 100644 crates/ming/src/arena.rs create mode 100644 crates/ming/src/asset_cache.rs create mode 100644 crates/ming/src/assets.rs create mode 100644 crates/ming/src/bounds_tree.rs create mode 100644 crates/ming/src/color.rs create mode 100644 crates/ming/src/element.rs create mode 100644 crates/ming/src/elements/anchored.rs create mode 100644 crates/ming/src/elements/animation.rs create mode 100644 crates/ming/src/elements/canvas.rs create mode 100644 crates/ming/src/elements/deferred.rs create mode 100644 crates/ming/src/elements/div.rs create mode 100644 crates/ming/src/elements/img.rs create mode 100644 crates/ming/src/elements/list.rs create mode 100644 crates/ming/src/elements/mod.rs create mode 100644 crates/ming/src/elements/svg.rs create mode 100644 crates/ming/src/elements/text.rs create mode 100644 crates/ming/src/elements/uniform_list.rs create mode 100644 crates/ming/src/executor.rs create mode 100644 crates/ming/src/geometry.rs create mode 100644 crates/ming/src/gpui.rs create mode 100644 crates/ming/src/input.rs create mode 100644 crates/ming/src/interactive.rs create mode 100644 crates/ming/src/key_dispatch.rs create mode 100644 crates/ming/src/keymap.rs create mode 100644 crates/ming/src/keymap/binding.rs create mode 100644 crates/ming/src/keymap/context.rs create mode 100644 crates/ming/src/keymap/matcher.rs create mode 100644 crates/ming/src/platform.rs create mode 100644 crates/ming/src/platform/app_menu.rs create mode 100644 crates/ming/src/platform/blade.rs create mode 100644 crates/ming/src/platform/blade/blade_atlas.rs create mode 100644 crates/ming/src/platform/blade/blade_belt.rs create mode 100644 crates/ming/src/platform/blade/blade_renderer.rs create mode 100644 crates/ming/src/platform/blade/shaders.wgsl create mode 100644 crates/ming/src/platform/cosmic_text.rs create mode 100644 crates/ming/src/platform/cosmic_text/text_system.rs create mode 100644 crates/ming/src/platform/keystroke.rs create mode 100644 crates/ming/src/platform/linux.rs create mode 100644 crates/ming/src/platform/linux/dispatcher.rs create mode 100644 crates/ming/src/platform/linux/headless.rs create mode 100644 crates/ming/src/platform/linux/headless/client.rs create mode 100644 crates/ming/src/platform/linux/platform.rs create mode 100644 crates/ming/src/platform/linux/wayland.rs create mode 100644 crates/ming/src/platform/linux/wayland/client.rs create mode 100644 crates/ming/src/platform/linux/wayland/cursor.rs create mode 100644 crates/ming/src/platform/linux/wayland/display.rs create mode 100644 crates/ming/src/platform/linux/wayland/serial.rs create mode 100644 crates/ming/src/platform/linux/wayland/window.rs create mode 100644 crates/ming/src/platform/linux/x11.rs create mode 100644 crates/ming/src/platform/linux/x11/client.rs create mode 100644 crates/ming/src/platform/linux/x11/display.rs create mode 100644 crates/ming/src/platform/linux/x11/event.rs create mode 100644 crates/ming/src/platform/linux/x11/window.rs create mode 100644 crates/ming/src/platform/mac.rs create mode 100644 crates/ming/src/platform/mac/dispatch.h create mode 100644 crates/ming/src/platform/mac/dispatcher.rs create mode 100644 crates/ming/src/platform/mac/display.rs create mode 100644 crates/ming/src/platform/mac/display_link.rs create mode 100644 crates/ming/src/platform/mac/events.rs create mode 100644 crates/ming/src/platform/mac/metal_atlas.rs create mode 100644 crates/ming/src/platform/mac/metal_renderer.rs create mode 100644 crates/ming/src/platform/mac/open_type.rs create mode 100644 crates/ming/src/platform/mac/platform.rs create mode 100644 crates/ming/src/platform/mac/shaders.metal create mode 100644 crates/ming/src/platform/mac/status_item.rs create mode 100644 crates/ming/src/platform/mac/text_system.rs create mode 100644 crates/ming/src/platform/mac/window.rs create mode 100644 crates/ming/src/platform/mac/window_appearance.rs create mode 100644 crates/ming/src/platform/test.rs create mode 100644 crates/ming/src/platform/test/dispatcher.rs create mode 100644 crates/ming/src/platform/test/display.rs create mode 100644 crates/ming/src/platform/test/platform.rs create mode 100644 crates/ming/src/platform/test/text_system.rs create mode 100644 crates/ming/src/platform/test/window.rs create mode 100644 crates/ming/src/platform/windows.rs create mode 100644 crates/ming/src/platform/windows/direct_write.rs create mode 100644 crates/ming/src/platform/windows/dispatcher.rs create mode 100644 crates/ming/src/platform/windows/display.rs create mode 100644 crates/ming/src/platform/windows/events.rs create mode 100644 crates/ming/src/platform/windows/platform.rs create mode 100644 crates/ming/src/platform/windows/system_settings.rs create mode 100644 crates/ming/src/platform/windows/util.rs create mode 100644 crates/ming/src/platform/windows/window.rs create mode 100644 crates/ming/src/prelude.rs create mode 100644 crates/ming/src/scene.rs create mode 100644 crates/ming/src/shared_string.rs create mode 100644 crates/ming/src/shared_uri.rs create mode 100644 crates/ming/src/style.rs create mode 100644 crates/ming/src/styled.rs create mode 100644 crates/ming/src/subscription.rs create mode 100644 crates/ming/src/svg_renderer.rs create mode 100644 crates/ming/src/taffy.rs create mode 100644 crates/ming/src/test.rs create mode 100644 crates/ming/src/text_system.rs create mode 100644 crates/ming/src/text_system/font_features.rs create mode 100644 crates/ming/src/text_system/line.rs create mode 100644 crates/ming/src/text_system/line_layout.rs create mode 100644 crates/ming/src/text_system/line_wrapper.rs create mode 100644 crates/ming/src/util.rs create mode 100644 crates/ming/src/view.rs create mode 100644 crates/ming/src/window.rs create mode 100644 crates/ming/src/window/prompts.rs create mode 100644 crates/ming/tests/action_macros.rs create mode 100644 crates/ming_macros/Cargo.toml create mode 100644 crates/ming_macros/src/derive_into_element.rs create mode 100644 crates/ming_macros/src/derive_render.rs create mode 100644 crates/ming_macros/src/lib.rs create mode 100644 crates/ming_macros/src/register_action.rs create mode 100644 crates/ming_macros/src/style_helpers.rs create mode 100644 crates/ming_macros/src/test.rs create mode 100644 crates/nite/Cargo.toml create mode 100644 crates/nite/src/main.rs create mode 100644 crates/refineable/Cargo.toml create mode 100644 crates/refineable/derive_refineable/Cargo.toml create mode 100644 crates/refineable/derive_refineable/src/derive_refineable.rs create mode 100644 crates/refineable/src/lib.rs create mode 100644 crates/util/Cargo.toml create mode 100644 crates/util/src/arc_cow.rs create mode 100644 crates/util/src/fs.rs create mode 100644 crates/util/src/http.rs create mode 100644 crates/util/src/lib.rs create mode 100644 crates/util/src/paths.rs create mode 100644 crates/util/src/semver.rs create mode 100644 crates/util/src/serde.rs create mode 100644 crates/util/src/sum_tree.rs create mode 100644 crates/util/src/sum_tree/cursor.rs create mode 100644 crates/util/src/sum_tree/tree_map.rs create mode 100644 crates/util/src/test.rs create mode 100644 crates/util/src/test/assertions.rs create mode 100644 crates/util/src/test/marked_text.rs create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload new file mode 100755 index 0000000..f1804ef --- /dev/null +++ b/.direnv/bin/nix-direnv-reload @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e +if [[ ! -d "/mnt/k/minky/nite" ]]; then + echo "Cannot find source directory; Did you move it?" + echo "(Looking for "/mnt/k/minky/nite")" + echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' + exit 1 +fi + +# rebuild the cache forcefully +_nix_direnv_force_reload=1 direnv exec "/mnt/k/minky/nite" true + +# Update the mtime for .envrc. +# This will cause direnv to reload again - but without re-building. +touch "/mnt/k/minky/nite/.envrc" + +# Also update the timestamp of whatever profile_rc we have. +# This makes sure that we know we are up to date. +touch -r "/mnt/k/minky/nite/.envrc" "/mnt/k/minky/nite/.direnv"/*.rc diff --git a/.direnv/flake-inputs/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source b/.direnv/flake-inputs/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source new file mode 120000 index 0000000..1fd9045 --- /dev/null +++ b/.direnv/flake-inputs/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source @@ -0,0 +1 @@ +/nix/store/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source \ No newline at end of file diff --git a/.direnv/flake-inputs/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source b/.direnv/flake-inputs/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source new file mode 120000 index 0000000..cdf8373 --- /dev/null +++ b/.direnv/flake-inputs/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source @@ -0,0 +1 @@ +/nix/store/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source \ No newline at end of file diff --git a/.direnv/flake-inputs/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source b/.direnv/flake-inputs/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source new file mode 120000 index 0000000..0076649 --- /dev/null +++ b/.direnv/flake-inputs/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source @@ -0,0 +1 @@ +/nix/store/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source \ No newline at end of file diff --git a/.direnv/flake-inputs/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source b/.direnv/flake-inputs/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source new file mode 120000 index 0000000..9474608 --- /dev/null +++ b/.direnv/flake-inputs/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source @@ -0,0 +1 @@ +/nix/store/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source \ No newline at end of file diff --git a/.direnv/flake-inputs/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source b/.direnv/flake-inputs/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source new file mode 120000 index 0000000..77aa730 --- /dev/null +++ b/.direnv/flake-inputs/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source @@ -0,0 +1 @@ +/nix/store/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source \ No newline at end of file diff --git a/.direnv/flake-inputs/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source b/.direnv/flake-inputs/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source new file mode 120000 index 0000000..3213a49 --- /dev/null +++ b/.direnv/flake-inputs/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source @@ -0,0 +1 @@ +/nix/store/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source \ No newline at end of file diff --git a/.direnv/flake-inputs/ah2qh833bkpxg8girwyl6vs30fkp1109-source b/.direnv/flake-inputs/ah2qh833bkpxg8girwyl6vs30fkp1109-source new file mode 120000 index 0000000..d0a84eb --- /dev/null +++ b/.direnv/flake-inputs/ah2qh833bkpxg8girwyl6vs30fkp1109-source @@ -0,0 +1 @@ +/nix/store/ah2qh833bkpxg8girwyl6vs30fkp1109-source \ No newline at end of file diff --git a/.direnv/flake-inputs/br885sqy62q1bblwi2bslcfg2193ly75-source b/.direnv/flake-inputs/br885sqy62q1bblwi2bslcfg2193ly75-source new file mode 120000 index 0000000..57c9cb9 --- /dev/null +++ b/.direnv/flake-inputs/br885sqy62q1bblwi2bslcfg2193ly75-source @@ -0,0 +1 @@ +/nix/store/br885sqy62q1bblwi2bslcfg2193ly75-source \ No newline at end of file diff --git a/.direnv/flake-inputs/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source b/.direnv/flake-inputs/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source new file mode 120000 index 0000000..5ee5200 --- /dev/null +++ b/.direnv/flake-inputs/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source @@ -0,0 +1 @@ +/nix/store/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source \ No newline at end of file diff --git a/.direnv/flake-inputs/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source b/.direnv/flake-inputs/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source new file mode 120000 index 0000000..916702d --- /dev/null +++ b/.direnv/flake-inputs/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source @@ -0,0 +1 @@ +/nix/store/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source \ No newline at end of file diff --git a/.direnv/flake-inputs/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source b/.direnv/flake-inputs/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source new file mode 120000 index 0000000..b3bf69f --- /dev/null +++ b/.direnv/flake-inputs/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source @@ -0,0 +1 @@ +/nix/store/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source \ No newline at end of file diff --git a/.direnv/flake-inputs/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source b/.direnv/flake-inputs/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source new file mode 120000 index 0000000..25db5a7 --- /dev/null +++ b/.direnv/flake-inputs/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source @@ -0,0 +1 @@ +/nix/store/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source \ No newline at end of file diff --git a/.direnv/flake-inputs/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source b/.direnv/flake-inputs/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source new file mode 120000 index 0000000..7b81874 --- /dev/null +++ b/.direnv/flake-inputs/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source @@ -0,0 +1 @@ +/nix/store/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source \ No newline at end of file diff --git a/.direnv/flake-inputs/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source b/.direnv/flake-inputs/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source new file mode 120000 index 0000000..f7b4153 --- /dev/null +++ b/.direnv/flake-inputs/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source @@ -0,0 +1 @@ +/nix/store/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source \ No newline at end of file diff --git a/.direnv/flake-inputs/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source b/.direnv/flake-inputs/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source new file mode 120000 index 0000000..ad26718 --- /dev/null +++ b/.direnv/flake-inputs/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source @@ -0,0 +1 @@ +/nix/store/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source \ No newline at end of file diff --git a/.direnv/flake-inputs/vm4qsaala00i8q5js7i3am3w0m766k1d-source b/.direnv/flake-inputs/vm4qsaala00i8q5js7i3am3w0m766k1d-source new file mode 120000 index 0000000..c859f5d --- /dev/null +++ b/.direnv/flake-inputs/vm4qsaala00i8q5js7i3am3w0m766k1d-source @@ -0,0 +1 @@ +/nix/store/vm4qsaala00i8q5js7i3am3w0m766k1d-source \ No newline at end of file diff --git a/.direnv/flake-inputs/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source b/.direnv/flake-inputs/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source new file mode 120000 index 0000000..4598043 --- /dev/null +++ b/.direnv/flake-inputs/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source @@ -0,0 +1 @@ +/nix/store/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source \ No newline at end of file diff --git a/.direnv/flake-inputs/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source b/.direnv/flake-inputs/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source new file mode 120000 index 0000000..2578dc5 --- /dev/null +++ b/.direnv/flake-inputs/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source @@ -0,0 +1 @@ +/nix/store/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source \ No newline at end of file diff --git a/.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source b/.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source new file mode 120000 index 0000000..f17959f --- /dev/null +++ b/.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source @@ -0,0 +1 @@ +/nix/store/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source \ No newline at end of file diff --git a/.direnv/flake-inputs/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source b/.direnv/flake-inputs/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source new file mode 120000 index 0000000..46f1bf1 --- /dev/null +++ b/.direnv/flake-inputs/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source @@ -0,0 +1 @@ +/nix/store/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa new file mode 120000 index 0000000..4364e72 --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa @@ -0,0 +1 @@ +/nix/store/fbldsappzwwr5acj8k1km1dy9ahpx9dj-nite-env \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc new file mode 100644 index 0000000..5cb492b --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc @@ -0,0 +1,68 @@ +unset shellHook +PATH=${PATH:-} +nix_saved_PATH="$PATH" +XDG_DATA_DIRS=${XDG_DATA_DIRS:-} +nix_saved_XDG_DATA_DIRS="$XDG_DATA_DIRS" +BASH='/noshell' +HOSTTYPE='x86_64' +IFS=' +' +IN_NIX_SHELL='impure' +export IN_NIX_SHELL +LINENO='76' +MACHTYPE='x86_64-pc-linux-gnu' +NIX_BUILD_CORES='0' +export NIX_BUILD_CORES +NIX_STORE='/nix/store' +export NIX_STORE +OLDPWD='' +export OLDPWD +OPTERR='1' +OSTYPE='linux-gnu' +PATH='/path-not-set' +export PATH +PS4='+ ' +builder='/nix/store/nnvsjd3f3dh9wdl4s9mwg5cfri8kds5j-bash-interactive-5.2p26/bin/bash' +export builder +dontAddDisableDepTrack='1' +export dontAddDisableDepTrack +name='nite-env' +export name +out='/mnt/k/minky/nite/outputs/out' +export out +outputs='out' +shellHook='# Remove all the unnecessary noise that is set by the build env +unset NIX_BUILD_TOP NIX_BUILD_CORES NIX_STORE +unset TEMP TEMPDIR TMP TMPDIR +# $name variable is preserved to keep it compatible with pure shell https://github.com/sindresorhus/pure/blob/47c0c881f0e7cfdb5eaccd335f52ad17b897c060/pure.zsh#L235 +unset builder out shellHook stdenv system +# Flakes stuff +unset dontAddDisableDepTrack outputs + +# For `nix develop`. We get /noshell on Linux and /sbin/nologin on macOS. +if [[ "$SHELL" == "/noshell" || "$SHELL" == "/sbin/nologin" ]]; then + export SHELL=/nix/store/nnvsjd3f3dh9wdl4s9mwg5cfri8kds5j-bash-interactive-5.2p26/bin/bash +fi + +# Load the environment +source "/nix/store/gw8h24v1c8p8xjf4q604hb7fggxig31j-nite-dir/env.bash" +' +export shellHook +stdenv='/nix/store/nqq1afagwrm4azc5ahh48qgkdzqi0jx7-naked-stdenv' +export stdenv +system='x86_64-linux' +export system +runHook () +{ + + eval "$shellHook"; + unset runHook +} +PATH="$PATH${nix_saved_PATH:+:$nix_saved_PATH}" +XDG_DATA_DIRS="$XDG_DATA_DIRS${nix_saved_XDG_DATA_DIRS:+:$nix_saved_XDG_DATA_DIRS}" +export NIX_BUILD_TOP="$(mktemp -d -t nix-shell.XXXXXX)" +export TMP="$NIX_BUILD_TOP" +export TMPDIR="$NIX_BUILD_TOP" +export TEMP="$NIX_BUILD_TOP" +export TEMPDIR="$NIX_BUILD_TOP" +eval "$shellHook" diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..ef32a46 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" +fi +use flake . --impure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..742c1c0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4105 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ash-window" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" +dependencies = [ + "ash", + "raw-window-handle", + "raw-window-metal", +] + +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f2776ead772134d55b62dd45e59a79e21612d85d0af729b8b7d3967d601a62a" +dependencies = [ + "concurrent-queue", + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.0", + "futures-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "async-signal" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.2", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "blade-graphics" +version = "0.4.0" +source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" +dependencies = [ + "ash", + "ash-window", + "bitflags 2.5.0", + "block", + "bytemuck", + "codespan-reporting", + "core-graphics-types", + "glow", + "gpu-alloc", + "gpu-alloc-ash", + "hidden-trait", + "js-sys", + "khronos-egl", + "libloading", + "log", + "metal", + "mint", + "naga", + "objc", + "raw-window-handle", + "slab", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "blade-macros" +version = "0.2.1" +source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.5.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "collections" +version = "0.1.0" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-cstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "copypasta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + +[[package]] +name = "cosmic-text" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c578f2b9abb4d5f3fbb12aba4008084d435dc6a8425c195cfe0b3594bfea0c25" +dependencies = [ + "bitflags 2.5.0", + "fontdb", + "libm", + "log", + "rangemap", + "rustc-hash", + "rustybuzz", + "self_cell", + "swash", + "sys-locale", + "ttf-parser", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn 2.0.63", +] + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "derive_refineable" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dwrote" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etagere" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "306960881d6c46bd0dd6b7f07442a441418c08d0d3e63d8d080b0f64c6343e4e" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror", + "winapi", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide 0.7.2", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "font-kit" +version = "0.11.0" +source = "git+https://github.com/zed-industries/font-kit?rev=5a5c4d4#5a5c4d4ca395c74eb0abde38508e170ce0fd761a" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "core-foundation", + "core-graphics", + "core-text", + "dirs-next", + "dwrote", + "float-ord", + "freetype", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "font-types" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf6aa1de86490d8e39e04589bd04eb5953cc2a5ef0c25e389e807f44fd24e41" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2 0.9.4", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a440748e063798e4893ceb877151e84acef9bea9a8c6800645cf3f1b3a7806e" +dependencies = [ + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "funnylog" +version = "0.1.0" +dependencies = [ + "funnylog-macros", + "log", + "owo-colors", + "parking_lot", +] + +[[package]] +name = "funnylog-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "git2" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +dependencies = [ + "bitflags 2.5.0", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.5.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-ash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a" +dependencies = [ + "ash", + "gpu-alloc-types", + "tinyvec", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "grid" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d196ffc1627db18a531359249b2bf8416178d84b729f3cebeb278f285fb9b58c" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hidden-trait" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed9e850438ac849bec07e7d09fbe9309cbd396a5988c30b010580ce08860df" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational 0.3.2", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1382b16c04aeb821453d6215a3c80ba78f24c6595c5aa85653378aabe0c83e3" +dependencies = [ + "libc", + "libloading", +] + +[[package]] +name = "kurbo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "libgit2-sys" +version = "0.16.2+1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linkme" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833222afbfe72868ac8f9770c91a33673f0d5fefc37c9dbe94aa3548b571623f" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0dea92dbea3271557cc2e1848723967bba81f722f95026860974ec9283f08" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550b24b0cd4cf923f36bae78eca457b3a10d8a6a14a9c84cb2687b527e6a84af" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "ming" +version = "0.1.0" +dependencies = [ + "as-raw-xcb-connection", + "ashpd", + "async-task", + "backtrace", + "blade-graphics", + "blade-macros", + "bytemuck", + "calloop", + "calloop-wayland-source", + "collections", + "copypasta", + "cosmic-text", + "ctor", + "derive_more", + "etagere", + "filedescriptor", + "flume", + "font-kit", + "futures", + "image", + "itertools", + "lazy_static", + "linkme", + "log", + "ming_macros", + "num_cpus", + "oo7", + "open", + "parking", + "parking_lot", + "pathfinder_geometry", + "postage", + "profiling", + "rand", + "raw-window-handle", + "refineable", + "resvg", + "schemars", + "seahash", + "serde", + "serde_derive", + "serde_json", + "slotmap", + "smallvec", + "taffy", + "thiserror", + "time", + "tokio", + "usvg", + "util", + "uuid", + "waker-fn", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "x11rb", + "xkbcommon", +] + +[[package]] +name = "ming_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "naga" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e" +dependencies = [ + "bit-set", + "bitflags 2.5.0", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "rustc-hash", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nite" +version = "0.1.0" +dependencies = [ + "funnylog", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.4.2", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oo7" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484acc8e440e60205766ff12680f044c7d29e143158312bbf0a43579af896383" +dependencies = [ + "aes", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite", + "futures-util", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "rand", + "serde", + "sha2", + "subtle", + "zbus", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owo-colors" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf45976c56919841273f2a0fc684c28437e2f304e264557d9c72be5d5a718be" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.2", +] + +[[package]] +name = "polling" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster", + "static_assertions", + "thiserror", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.63", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rangemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" + +[[package]] +name = "raw-window-handle" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc3bcbdb1ddfc11e700e62968e6b4cc9c75bb466464ad28fb61c5b2c964418b" + +[[package]] +name = "raw-window-metal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" +dependencies = [ + "cocoa", + "core-graphics", + "objc", + "raw-window-handle", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4749db2bd1c853db31a7ae5ee2fc6c30bbddce353ea8fedf673fed187c68c7" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "refineable" +version = "0.1.0" +dependencies = [ + "derive_refineable", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "resvg" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2327ced609dadeed3e9702fec3e6b2ddd208758a9268d13e06566c6101ba533" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "rust-embed" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.63", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustybuzz" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c" +dependencies = [ + "bitflags 2.5.0", + "bytemuck", + "libm", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.63", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.201" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.201" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.5.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2 0.9.4", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.2.0+1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" +dependencies = [ + "bitflags 1.3.2", + "num-traits", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "svg_fmt" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83ba502a3265efb76efb89b0a2f7782ad6f2675015d4ce37e4b547dda42b499" + +[[package]] +name = "svgtypes" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ec889a8e0a6fcb91041996c8f1f6be0fe1a09e94478785e07c32ce2bca2d2b" +dependencies = [ + "read-fonts", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + +[[package]] +name = "taffy" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2e140b328c6cb5e744bb2c65910b47df86b239afc793ee2c52262569cf9225" +dependencies = [ + "arrayvec", + "grid", + "num-traits", + "serde", + "slotmap", +] + +[[package]] +name = "take-until" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.13", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + +[[package]] +name = "unicode-script" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c704361d822337cfc00387672c7b59eaa72a1f0744f62b2a68aa228a0c6927d" +dependencies = [ + "base64", + "data-url", + "flate2", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "xmlwriter", +] + +[[package]] +name = "util" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "dirs", + "futures", + "git2", + "globset", + "lazy_static", + "log", + "rand", + "regex", + "rust-embed", + "serde", + "serde_json", + "take-until", + "tempfile", + "tokio", + "tokio-stream", + "unicase", +] + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.63", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +dependencies = [ + "bitflags 2.5.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.5.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" + +[[package]] +name = "xdg-home" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "xkbcommon" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2 0.8.0", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386" +dependencies = [ + "const-cstr", + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "zbus" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5915716dff34abef1351d2b10305b019c8ef33dcf6c72d31a6e227d5d9d7a21" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.3.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fceb36d0c1c4a6b98f3ce40f410e64e5a134707ed71892e1b178abc4c695d4" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "zvariant" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877ef94e5e82b231d2a309c531f191a8152baba8241a7939ee04bd76b0171308" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ca98581cc6a8120789d8f1f0997e9053837d6aa5346cbb43454d7121be6e39" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75fa7291bdd68cd13c4f97cc9d78cbf16d96305856dfc7ac942aeff4c2de7d5a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[patch.unused]] +name = "careless" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d5cc810 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,84 @@ +[workspace] +members = [ + "crates/*" +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "GPL-3.0" + +[workspace.dependencies] +## crates/* +collections.path = "crates/collections" +ming.path = "crates/ming" +ming_macros.path = "crates/ming_macros" +refineable.path = "crates/refineable" +util.path = "crates/util" + +## Minky deps +funnylog = { git = "https://codeberg.org/minky/funnylog.git", version = "0.1.0", features = ["terminal", "log"] } + +## Other deps +blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" } +git2 = { version = "0.18", default-features = false } +globset = "0.4" +regex = "1" +rust-embed = { version = "8.4", features = ["include-exclude"] } +tempfile = "3.9" +unicase = "2.6" +derive_more = "0.99.17" +anyhow = "1" +ctor = "0.2.6" +log = "0.4" +itertools = "0.12" +lazy_static = "1.4.0" +parking_lot = "0.12" +postage = { version = "0.5", features = ["futures-traits"] } +profiling = "1" +rand = "0.8.5" +schemars = "0.8" +serde = { version = "1.0", features = ["derive", "rc"] } +serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } +serde_json_lenient = { version = "0.1", features = [ + "preserve_order", + "raw_value", +] } +serde_repr = "0.1" +smallvec = { version = "1.6", features = ["union"] } +sha2 = "0.10" +thiserror = "1" +time = { version = "0.3", features = [ + "macros", + "parsing", + "serde", + "serde-well-known", + "formatting", +] } +uuid = "1" + +### Async stuff +futures = "0.3" +tokio = { version = "1", features = [ "full" ] } +tokio-stream = { version = "0.1", features = [ "full" ] } + +[workspace.lints.clippy] +dbg_macro = "deny" +todo = "deny" + +style = "allow" + +almost_complete_range = "allow" +arc_with_non_send_sync = "allow" +borrowed_box = "allow" +let_underscore_future = "allow" +map_entry = "allow" +non_canonical_partial_ord_impl = "allow" +reversed_empty_ranges = "allow" +type_complexity = "allow" + +[workspace.metadata.cargo-machete] +ignored = ["bindgen", "cbindgen", "prost_build", "serde"] diff --git a/LICENSE-GPL b/LICENSE-GPL new file mode 100644 index 0000000..cb82534 --- /dev/null +++ b/LICENSE-GPL @@ -0,0 +1,200 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: +a) The work must carry prominent notices stating that you modified it, and giving a relevant date. +b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". +c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. +d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: +a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. +b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. +c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. +d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. +e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or +b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or +c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or +d) Limiting the use for publicity purposes of names of licensors or authors of the material; or +e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or +f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + +Copyright (C) + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0cb37a --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# nite + +Nite is (will soon be) a GUI code editor with a vim-ish feel. + +It's intended to be an (almost) drop-in replacement for Neovim, aside from nite being a GUI app, which means having items listed in the [TODO](#todo) paragraph. + +## TODO + +The following list of features and tasks required to have them is highly opinionated and consists of things I want a code editor to have. + +- [ ] Make it compile :) +- [ ] Vim-like keybindings and motions +- [ ] A Telescope-ey file finder +- [ ] Something like [harpoon](https://github.com/theprimeagen/harpoon) and maybe a topbar with marks +- [ ] LSP client (note: [async-lsp](https://docs.rs/async-lsp)) +- [ ] Popup terminal (note: [kitti3](https://github.com/LandingEllipse/kitti3)) +- [ ] Workspace splitting +- [ ] (optional) Git integration + +## Info on [ming] + +Ming is a GPU-accelerated native UI framework. + +It is planned to have full Linux support and (maybe) a Material 3 implementation. + +## Credits + +A **LOT** of code (including: crates/util, crates/ming*) is taken from [the Zed codebase](https://github.com/zed-industries/zed) precisely at commit [c90263d]. + +Also, [ming] is a "fork" (blatant copy with no intent of syncing upstream changes) of [gpui], also made by the Zed team for use in Zed. + +[ming]: ./crates/ming/README.md +[c90263d]: https://github.com/zed-industries/zed/tree/c90263d59b8cde9e007f16b286f040fa849ecb24 +[gpui]: https://github.com/zed-industries/zed/tree/c90263d59b8cde9e007f16b286f040fa849ecb24/crates/gpui diff --git a/crates/collections/Cargo.toml b/crates/collections/Cargo.toml new file mode 100644 index 0000000..b16b4c1 --- /dev/null +++ b/crates/collections/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "collections" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/collections.rs" +doctest = false + +[features] +test-support = [] + +[dependencies] +rustc-hash = "1.1" diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs new file mode 100644 index 0000000..25f6135 --- /dev/null +++ b/crates/collections/src/collections.rs @@ -0,0 +1,15 @@ +#[cfg(feature = "test-support")] +pub type HashMap = FxHashMap; + +#[cfg(feature = "test-support")] +pub type HashSet = FxHashSet; + +#[cfg(not(feature = "test-support"))] +pub type HashMap = std::collections::HashMap; + +#[cfg(not(feature = "test-support"))] +pub type HashSet = std::collections::HashSet; + +pub use rustc_hash::FxHasher; +pub use rustc_hash::{FxHashMap, FxHashSet}; +pub use std::collections::*; diff --git a/crates/ming/Cargo.toml b/crates/ming/Cargo.toml new file mode 100644 index 0000000..8a17937 --- /dev/null +++ b/crates/ming/Cargo.toml @@ -0,0 +1,138 @@ +[package] +name = "ming" +version = "0.1.0" +edition = "2021" +authors = ["Nathan Sobo ", "Ilya Borodinov "] +description = "GPU-accelerated native UI framework" + +[lints] +workspace = true + +[features] +default = [] +test-support = ["backtrace", "collections/test-support", "util/test-support"] +runtime_shaders = [] +macos-blade = ["blade-graphics", "blade-macros", "bytemuck"] + +[lib] +path = "src/gpui.rs" +doctest = false + +[dependencies] +async-task = "4.7" +backtrace = { version = "0.3", optional = true } +blade-graphics = { workspace = true, optional = true } +blade-macros = { workspace = true, optional = true } +bytemuck = { version = "1", optional = true } +collections.workspace = true +ctor.workspace = true +derive_more.workspace = true +etagere = "0.2" +futures.workspace = true +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4" } +ming_macros.workspace = true +image = "0.23" +itertools.workspace = true +lazy_static.workspace = true +linkme = "0.3" +log.workspace = true +num_cpus = "1.13" +parking = "2.0.0" +parking_lot.workspace = true +pathfinder_geometry = "0.5" +postage.workspace = true +profiling.workspace = true +rand.workspace = true +raw-window-handle = "0.6" +refineable.workspace = true +resvg = { version = "0.41.0", default-features = false } +usvg = { version = "0.41.0", default-features = false } +schemars.workspace = true +seahash = "4.1" +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +slotmap = "1.0.6" +smallvec.workspace = true +tokio.workspace = true +taffy = "0.4.3" +thiserror.workspace = true +time.workspace = true +util.workspace = true +uuid.workspace = true +waker-fn = "1.1.0" + +[dev-dependencies] +backtrace = "0.3" +collections = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } + +# [target.'cfg(target_os = "macos")'.build-dependencies] +# bindgen = "0.65.1" +# cbindgen = "0.26.0" + +# [target.'cfg(target_os = "macos")'.dependencies] +# block = "0.1" +# cocoa = "0.25" +# core-foundation.workspace = true +# core-graphics = "0.23" +# core-text = "20.1" +# foreign-types = "0.5" +# log.workspace = true +# media.workspace = true +# metal = "0.25" +# objc = "0.2" + +[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies] +flume = "0.11" +#TODO: use these on all platforms +blade-graphics.workspace = true +blade-macros.workspace = true +bytemuck = "1" +cosmic-text = "0.11.2" +copypasta = "0.10.1" + +[target.'cfg(target_os = "linux")'.dependencies] +as-raw-xcb-connection = "1" +ashpd = "0.8.0" +calloop = "0.12.4" +calloop-wayland-source = "0.2.0" +wayland-backend = { version = "0.3.3", features = ["client_system"] } +wayland-client = { version = "0.31.2" } +wayland-cursor = "0.31.1" +wayland-protocols = { version = "0.31.2", features = [ + "client", + "staging", + "unstable", +] } +wayland-protocols-plasma = { version = "0.2.0", features = ["client"] } +oo7 = "0.3.0" +open = "5.1.2" +filedescriptor = "0.8.2" +x11rb = { version = "0.13.0", features = [ + "allow-unsafe-code", + "xkb", + "randr", + "xinput", + "cursor", + "resource_manager", +] } +xkbcommon = { version = "0.7", features = ["wayland", "x11"] } + +# [target.'cfg(windows)'.dependencies] +# windows.workspace = true +# +# [target.'cfg(windows)'.build-dependencies] +# embed-resource = "2.4" + +[[example]] +name = "hello_world" +path = "examples/hello_world.rs" + +[[example]] +name = "image" +path = "examples/image/image.rs" + +[[example]] +name = "set_menus" +path = "examples/set_menus.rs" diff --git a/crates/ming/README.md b/crates/ming/README.md new file mode 100644 index 0000000..ac2c04b --- /dev/null +++ b/crates/ming/README.md @@ -0,0 +1,40 @@ +# Welcome to GPUI! + +GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework +for Rust, designed to support a wide variety of applications. + +## Getting Started + +GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io. You'll also need to use the latest version of stable rust and be on macOS. Add the following to your Cargo.toml: + +``` +gpui = { git = "https://github.com/zed-industries/zed" } +``` + +Everything in GPUI starts with an `App`. You can create one with `App::new()`, and kick off your application by passing a callback to `App::run()`. Inside this callback, you can create a new window with `AppContext::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example. + +## The Big Picture + +GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs: + +- State management and communication with Models. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's models. Models are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::model_context` module for more information. + +- High level, declarative UI with Views. All UI in GPUI starts with a View. A view is simply a model that can be rendered, via the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering. + +- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. + +Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework. + +## Other Resources + +In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications: + +- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information. + +- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::AppContext`. + +- An async executor that is integrated with the platform's event loop. See the `executor` module for more information., + +- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. + +Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://discord.gg/U4qhCEhMXP). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/crates/ming/build.rs b/crates/ming/build.rs new file mode 100644 index 0000000..46c55ee --- /dev/null +++ b/crates/ming/build.rs @@ -0,0 +1,205 @@ +#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))] + +//TODO: consider generating shader code for WGSL +//TODO: deprecate "runtime-shaders" and "macos-blade" + +fn main() { + #[cfg(target_os = "macos")] + macos::build(); + + #[cfg(target_os = "windows")] + { + let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml"); + let rc_file = std::path::Path::new("resources/windows/gpui.rc"); + println!("cargo:rerun-if-changed={}", manifest.display()); + println!("cargo:rerun-if-changed={}", rc_file.display()); + embed_resource::compile(rc_file, embed_resource::NONE); + } +} + +#[cfg(target_os = "macos")] +mod macos { + use std::{ + env, + path::{Path, PathBuf}, + }; + + use cbindgen::Config; + + pub(super) fn build() { + generate_dispatch_bindings(); + #[cfg(not(feature = "macos-blade"))] + { + let header_path = generate_shader_bindings(); + + #[cfg(feature = "runtime_shaders")] + emit_stitched_shaders(&header_path); + #[cfg(not(feature = "runtime_shaders"))] + compile_metal_shaders(&header_path); + } + } + + fn generate_dispatch_bindings() { + println!("cargo:rustc-link-lib=framework=System"); + println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h"); + + let bindings = bindgen::Builder::default() + .header("src/platform/mac/dispatch.h") + .allowlist_var("_dispatch_main_q") + .allowlist_var("_dispatch_source_type_data_add") + .allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH") + .allowlist_var("DISPATCH_TIME_NOW") + .allowlist_function("dispatch_get_global_queue") + .allowlist_function("dispatch_async_f") + .allowlist_function("dispatch_after_f") + .allowlist_function("dispatch_time") + .allowlist_function("dispatch_source_merge_data") + .allowlist_function("dispatch_source_create") + .allowlist_function("dispatch_source_set_event_handler_f") + .allowlist_function("dispatch_resume") + .allowlist_function("dispatch_suspend") + .allowlist_function("dispatch_source_cancel") + .allowlist_function("dispatch_set_context") + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .layout_tests(false) + .generate() + .expect("unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("dispatch_sys.rs")) + .expect("couldn't write dispatch bindings"); + } + + fn generate_shader_bindings() -> PathBuf { + let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h"); + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let mut config = Config::default(); + config.include_guard = Some("SCENE_H".into()); + config.language = cbindgen::Language::C; + config.export.include.extend([ + "Bounds".into(), + "Corners".into(), + "Edges".into(), + "Size".into(), + "Pixels".into(), + "PointF".into(), + "Hsla".into(), + "ContentMask".into(), + "Uniforms".into(), + "AtlasTile".into(), + "PathRasterizationInputIndex".into(), + "PathVertex_ScaledPixels".into(), + "ShadowInputIndex".into(), + "Shadow".into(), + "QuadInputIndex".into(), + "Underline".into(), + "UnderlineInputIndex".into(), + "Quad".into(), + "SpriteInputIndex".into(), + "MonochromeSprite".into(), + "PolychromeSprite".into(), + "PathSprite".into(), + "SurfaceInputIndex".into(), + "SurfaceBounds".into(), + "TransformationMatrix".into(), + ]); + config.no_includes = true; + config.enumeration.prefix_with_name = true; + + let mut builder = cbindgen::Builder::new(); + + let src_paths = [ + crate_dir.join("src/scene.rs"), + crate_dir.join("src/geometry.rs"), + crate_dir.join("src/color.rs"), + crate_dir.join("src/window.rs"), + crate_dir.join("src/platform.rs"), + crate_dir.join("src/platform/mac/metal_renderer.rs"), + ]; + for src_path in src_paths { + println!("cargo:rerun-if-changed={}", src_path.display()); + builder = builder.with_src(src_path); + } + + builder + .with_config(config) + .generate() + .expect("Unable to generate bindings") + .write_to_file(&output_path); + + output_path + } + + /// To enable runtime compilation, we need to "stitch" the shaders file with the generated header + /// so that it is self-contained. + #[cfg(feature = "runtime_shaders")] + fn emit_stitched_shaders(header_path: &Path) { + use std::str::FromStr; + fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result { + let header_contents = std::fs::read_to_string(header)?; + let shader_contents = std::fs::read_to_string(shader_path)?; + let stitched_contents = format!("{header_contents}\n{shader_contents}"); + let out_path = + PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal"); + std::fs::write(&out_path, stitched_contents)?; + Ok(out_path) + } + let shader_source_path = "./src/platform/mac/shaders.metal"; + let shader_path = PathBuf::from_str(shader_source_path).unwrap(); + stitch_header(header_path, &shader_path).unwrap(); + println!("cargo:rerun-if-changed={}", &shader_source_path); + } + + #[cfg(not(feature = "runtime_shaders"))] + fn compile_metal_shaders(header_path: &Path) { + use std::process::{self, Command}; + let shader_path = "./src/platform/mac/shaders.metal"; + let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air"); + let metallib_output_path = + PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib"); + println!("cargo:rerun-if-changed={}", shader_path); + + let output = Command::new("xcrun") + .args([ + "-sdk", + "macosx", + "metal", + "-gline-tables-only", + "-mmacosx-version-min=10.15.7", + "-MO", + "-c", + shader_path, + "-include", + &header_path.to_str().unwrap(), + "-o", + ]) + .arg(&air_output_path) + .output() + .unwrap(); + + if !output.status.success() { + eprintln!( + "metal shader compilation failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + process::exit(1); + } + + let output = Command::new("xcrun") + .args(["-sdk", "macosx", "metallib"]) + .arg(air_output_path) + .arg("-o") + .arg(metallib_output_path) + .output() + .unwrap(); + + if !output.status.success() { + eprintln!( + "metallib compilation failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + process::exit(1); + } + } +} diff --git a/crates/ming/docs/contexts.md b/crates/ming/docs/contexts.md new file mode 100644 index 0000000..3ccac8d --- /dev/null +++ b/crates/ming/docs/contexts.md @@ -0,0 +1,41 @@ +# Contexts + +GPUI makes extensive use of _context parameters_, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services. + +There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead. + +``` + AppContext + / \ +ModelContext WindowContext + / + ViewContext +``` + +- The `AppContext` forms the root of the hierarchy +- `ModelContext` and `WindowContext` both dereference to `AppContext` +- `ViewContext` dereferences to `WindowContext` + +## `AppContext` + +Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`... + +## `WindowContext` + +Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`. + +## `ModelContext` + +Available when you create or update a `Model`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events. + +## `ViewContext` + +Available when you create or update a `View`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events. + +## `AsyncAppContext` and `AsyncWindowContext` + +Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself. + +## `TestAppContext` and `TestVisualContext` + +These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests. diff --git a/crates/ming/docs/key_dispatch.md b/crates/ming/docs/key_dispatch.md new file mode 100644 index 0000000..804a0b5 --- /dev/null +++ b/crates/ming/docs/key_dispatch.md @@ -0,0 +1,101 @@ +# Key Dispatch + +GPUI is designed for keyboard-first interactivity. + +To expose functionality to the mouse, you render a button with a click handler. + +To expose functionality to the keyboard, you bind an *action* in a *key context*. + +Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself: + +```rust +mod menu { + #[gpui::action] + struct MoveUp; + + #[gpui::action] + struct MoveDown; +} +``` + +Actions are frequently unit structs, for which we have a macro. The above could also be written: + +```rust +mod menu { + actions!(gpui, [MoveUp, MoveDown]); +} +``` + +Actions can also be more complex types: + +```rust +mod menu { + #[gpui::action] + struct Move { + direction: Direction, + select: bool, + } +} +``` + +To bind actions, chain `on_action` on to your element: + +```rust +impl Render for Menu { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { + div() + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(unimplemented!()) + } +} +``` + +In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`. + +```rust +impl Render for Menu { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { + div() + .key_context("menu") + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(unimplemented!()) + } +} +``` + +Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name. + +```json +{ + "context": "menu", + "bindings": { + "up": "menu::MoveUp", + "down": "menu::MoveDown" + } +} +``` + +If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name: + +```json +{ + "context": "menu", + "bindings": { + "up": ["menu::Move", {direction: "up", select: false}] + "down": ["menu::Move", {direction: "down", select: false}] + "shift-up": ["menu::Move", {direction: "up", select: true}] + "shift-down": ["menu::Move", {direction: "down", select: true}] + } +} + +``` diff --git a/crates/ming/examples/animation.rs b/crates/ming/examples/animation.rs new file mode 100644 index 0000000..f3d8f56 --- /dev/null +++ b/crates/ming/examples/animation.rs @@ -0,0 +1,78 @@ +use std::time::Duration; + +use gpui::*; + +struct Assets {} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result> { + std::fs::read(path).map(Into::into).map_err(Into::into) + } + + fn list(&self, path: &str) -> Result> { + Ok(std::fs::read_dir(path)? + .filter_map(|entry| { + Some(SharedString::from( + entry.ok()?.path().to_string_lossy().to_string(), + )) + }) + .collect::>()) + } +} + +struct AnimationExample {} + +impl Render for AnimationExample { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div().flex().flex_col().size_full().justify_around().child( + div().flex().flex_row().w_full().justify_around().child( + div() + .flex() + .bg(rgb(0x2e7d32)) + .size(Length::Definite(Pixels(300.0).into())) + .justify_center() + .items_center() + .shadow_lg() + .text_xl() + .text_color(black()) + .child("hello") + .child( + svg() + .size_8() + .path("examples/image/arrow_circle.svg") + .text_color(black()) + .with_animation( + "image_circle", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(bounce(ease_in_out)), + |svg, delta| { + svg.with_transformation(Transformation::rotate(percentage( + delta, + ))) + }, + ), + ), + ), + ) + } +} + +fn main() { + App::new() + .with_assets(Assets {}) + .run(|cx: &mut AppContext| { + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(300.), px(300.)), + cx, + ))), + ..Default::default() + }; + cx.open_window(options, |cx| { + cx.activate(false); + cx.new_view(|_cx| AnimationExample {}) + }); + }); +} diff --git a/crates/ming/examples/hello_world.rs b/crates/ming/examples/hello_world.rs new file mode 100644 index 0000000..6a91f6a --- /dev/null +++ b/crates/ming/examples/hello_world.rs @@ -0,0 +1,39 @@ +use gpui::*; + +struct HelloWorld { + text: SharedString, +} + +impl Render for HelloWorld { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div() + .flex() + .bg(rgb(0x2e7d32)) + .size(Length::Definite(Pixels(300.0).into())) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child(format!("Hello, {}!", &self.text)) + } +} + +fn main() { + App::new().run(|cx: &mut AppContext| { + let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |cx| { + cx.new_view(|_cx| HelloWorld { + text: "World".into(), + }) + }, + ); + }); +} diff --git a/crates/ming/examples/image/app-icon.png b/crates/ming/examples/image/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..08b6d8afa0d1088e50f1e9697e01407c9c623f6d GIT binary patch literal 168173 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

wibT(J_@q= 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 bd;t68F!!G*O<;hQ%9nV!d%My8eKYMZ9A(F| literal 0 HcmV?d00001 diff --git a/crates/ming/examples/image/arrow_circle.svg b/crates/ming/examples/image/arrow_circle.svg new file mode 100644 index 0000000..90e352b --- /dev/null +++ b/crates/ming/examples/image/arrow_circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/ming/examples/image/image.rs b/crates/ming/examples/image/image.rs new file mode 100644 index 0000000..e1e82d8 --- /dev/null +++ b/crates/ming/examples/image/image.rs @@ -0,0 +1,98 @@ +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; + +use gpui::*; + +#[derive(IntoElement)] +struct ImageContainer { + text: SharedString, + src: ImageSource, +} + +impl ImageContainer { + pub fn new(text: impl Into, src: impl Into) -> Self { + Self { + text: text.into(), + src: src.into(), + } + } +} + +impl RenderOnce for ImageContainer { + fn render(self, _: &mut WindowContext) -> impl IntoElement { + div().child( + div() + .flex_row() + .size_full() + .gap_4() + .child(self.text) + .child(img(self.src).w(px(512.0)).h(px(512.0))), + ) + } +} + +struct ImageShowcase { + local_resource: Arc, + remote_resource: SharedUri, +} + +impl Render for ImageShowcase { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div() + .flex() + .flex_row() + .size_full() + .justify_center() + .items_center() + .gap_8() + .bg(rgb(0xFFFFFF)) + .child(ImageContainer::new( + "Image loaded from a local file", + self.local_resource.clone(), + )) + .child(ImageContainer::new( + "Image loaded from a remote resource", + self.remote_resource.clone(), + )) + } +} + +actions!(image, [Quit]); + +fn main() { + env_logger::init(); + + App::new().run(|cx: &mut AppContext| { + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + cx.set_menus(vec![Menu { + name: "Image", + items: vec![MenuItem::action("Quit", Quit)], + }]); + + let window_options = WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some(SharedString::from("Image Example")), + appears_transparent: false, + ..Default::default() + }), + + window_bounds: Some(WindowBounds::Windowed(Bounds { + size: size(px(1100.), px(600.)).into(), + origin: Point::new(DevicePixels::from(200), DevicePixels::from(200)), + })), + + ..Default::default() + }; + + cx.open_window(window_options, |cx| { + cx.new_view(|_cx| ImageShowcase { + // Relative path to your root project path + local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()), + remote_resource: "https://picsum.photos/512/512".into(), + }) + }); + }); +} diff --git a/crates/ming/examples/ownership_post.rs b/crates/ming/examples/ownership_post.rs new file mode 100644 index 0000000..cd3b626 --- /dev/null +++ b/crates/ming/examples/ownership_post.rs @@ -0,0 +1,35 @@ +use gpui::{prelude::*, App, AppContext, EventEmitter, Model, ModelContext}; + +struct Counter { + count: usize, +} + +struct Change { + increment: usize, +} + +impl EventEmitter for Counter {} + +fn main() { + App::new().run(|cx: &mut AppContext| { + let counter: Model = cx.new_model(|_cx| Counter { count: 0 }); + let subscriber = cx.new_model(|cx: &mut ModelContext| { + cx.subscribe(&counter, |subscriber, _emitter, event, _cx| { + subscriber.count += event.increment * 2; + }) + .detach(); + + Counter { + count: counter.read(cx).count * 2, + } + }); + + counter.update(cx, |counter, cx| { + counter.count += 2; + cx.notify(); + cx.emit(Change { increment: 2 }); + }); + + assert_eq!(subscriber.read(cx).count, 4); + }); +} diff --git a/crates/ming/examples/set_menus.rs b/crates/ming/examples/set_menus.rs new file mode 100644 index 0000000..1a46d2e --- /dev/null +++ b/crates/ming/examples/set_menus.rs @@ -0,0 +1,43 @@ +use gpui::*; + +struct SetMenus; + +impl Render for SetMenus { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div() + .flex() + .bg(rgb(0x2e7d32)) + .size_full() + .justify_center() + .items_center() + .text_xl() + .text_color(rgb(0xffffff)) + .child("Set Menus Example") + } +} + +fn main() { + App::new().run(|cx: &mut AppContext| { + // Bring the menu bar to the foreground (so you can see the menu bar) + cx.activate(true); + // Register the `quit` function so it can be referenced by the `MenuItem::action` in the menu bar + cx.on_action(quit); + // Add menu items + cx.set_menus(vec![Menu { + name: "set_menus", + items: vec![MenuItem::action("Quit", Quit)], + }]); + cx.open_window(WindowOptions::default(), |cx| { + cx.new_view(|_cx| SetMenus {}) + }); + }); +} + +// Associate actions using the `actions!` macro (or `impl_actions!` macro) +actions!(set_menus, [Quit]); + +// Define the quit function that is registered with the AppContext +fn quit(_: &Quit, cx: &mut AppContext) { + println!("Gracefully quitting the application . . ."); + cx.quit(); +} diff --git a/crates/ming/examples/window_positioning.rs b/crates/ming/examples/window_positioning.rs new file mode 100644 index 0000000..da87423 --- /dev/null +++ b/crates/ming/examples/window_positioning.rs @@ -0,0 +1,67 @@ +use gpui::*; + +struct WindowContent { + text: SharedString, +} + +impl Render for WindowContent { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div() + .flex() + .bg(rgb(0x1e2025)) + .size_full() + .justify_center() + .items_center() + .text_xl() + .text_color(rgb(0xffffff)) + .child(self.text.clone()) + } +} + +fn main() { + App::new().run(|cx: &mut AppContext| { + // Create several new windows, positioned in the top right corner of each screen + + for screen in cx.displays() { + let options = { + let popup_margin_width = DevicePixels::from(16); + let popup_margin_height = DevicePixels::from(-0) - DevicePixels::from(48); + + let window_size = Size { + width: px(400.), + height: px(72.), + }; + + let screen_bounds = screen.bounds(); + let size: Size = window_size.into(); + + let bounds = gpui::Bounds:: { + origin: screen_bounds.upper_right() + - point(size.width + popup_margin_width, popup_margin_height), + size: window_size.into(), + }; + + WindowOptions { + // Set the bounds of the window in screen coordinates + window_bounds: Some(WindowBounds::Windowed(bounds)), + // Specify the display_id to ensure the window is created on the correct screen + display_id: Some(screen.id()), + + titlebar: None, + window_background: WindowBackgroundAppearance::default(), + focus: false, + show: true, + kind: WindowKind::PopUp, + is_movable: false, + app_id: None, + } + }; + + cx.open_window(options, |cx| { + cx.new_view(|_| WindowContent { + text: format!("{:?}", screen.id()).into(), + }) + }); + } + }); +} diff --git a/crates/ming/resources/windows/gpui.manifest.xml b/crates/ming/resources/windows/gpui.manifest.xml new file mode 100644 index 0000000..5a69b43 --- /dev/null +++ b/crates/ming/resources/windows/gpui.manifest.xml @@ -0,0 +1,16 @@ + + + + true + PerMonitorV2 + + + + + + + + diff --git a/crates/ming/resources/windows/gpui.rc b/crates/ming/resources/windows/gpui.rc new file mode 100644 index 0000000..a6f3787 --- /dev/null +++ b/crates/ming/resources/windows/gpui.rc @@ -0,0 +1,2 @@ +#define RT_MANIFEST 24 +1 RT_MANIFEST "resources/windows/gpui.manifest.xml" \ No newline at end of file diff --git a/crates/ming/src/action.rs b/crates/ming/src/action.rs new file mode 100644 index 0000000..cf0ad7e --- /dev/null +++ b/crates/ming/src/action.rs @@ -0,0 +1,268 @@ +use crate::SharedString; +use anyhow::{anyhow, Context, Result}; +use collections::HashMap; +pub use no_action::NoAction; +use serde_json::json; +use std::any::{Any, TypeId}; + +/// Actions are used to implement keyboard-driven UI. +/// When you declare an action, you can bind keys to the action in the keymap and +/// listeners for that action in the element tree. +/// +/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct +/// action for each listed action name in the given namespace. +/// ```rust +/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]); +/// ``` +/// More complex data types can also be actions, providing they implement Clone, PartialEq, +/// and serde_derive::Deserialize. +/// Use `impl_actions!` to automatically implement the action in the given namespace. +/// ``` +/// #[derive(Clone, PartialEq, serde_derive::Deserialize)] +/// pub struct SelectNext { +/// pub replace_newest: bool, +/// } +/// impl_actions!(editor, [SelectNext]); +/// ``` +/// +/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]` +/// macro, which only generates the code needed to register your action before `main`. +/// +/// ``` +/// #[derive(gpui::private::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)] +/// pub struct Paste { +/// pub content: SharedString, +/// } +/// +/// impl gpui::Action for Paste { +/// ///... +/// } +/// register_action!(Paste); +/// ``` +pub trait Action: 'static + Send { + /// Clone the action into a new box + fn boxed_clone(&self) -> Box; + + /// Cast the action to the any type + fn as_any(&self) -> &dyn Any; + + /// Do a partial equality check on this action and the other + fn partial_eq(&self, action: &dyn Action) -> bool; + + /// Get the name of this action, for displaying in UI + fn name(&self) -> &str; + + /// Get the name of this action for debugging + fn debug_name() -> &'static str + where + Self: Sized; + + /// Build this action from a JSON value. This is used to construct actions from the keymap. + /// A value of `{}` will be passed for actions that don't have any parameters. + fn build(value: serde_json::Value) -> Result> + where + Self: Sized; +} + +impl std::fmt::Debug for dyn Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dyn Action") + .field("name", &self.name()) + .finish() + } +} + +impl dyn Action { + /// Get the type id of this action + pub fn type_id(&self) -> TypeId { + self.as_any().type_id() + } +} + +type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result>; + +pub(crate) struct ActionRegistry { + builders_by_name: HashMap, + names_by_type_id: HashMap, + all_names: Vec, // So we can return a static slice. +} + +impl Default for ActionRegistry { + fn default() -> Self { + let mut this = ActionRegistry { + builders_by_name: Default::default(), + names_by_type_id: Default::default(), + all_names: Default::default(), + }; + + this.load_actions(); + + this + } +} + +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub type MacroActionBuilder = fn() -> ActionData; + +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub struct ActionData { + pub name: &'static str, + pub type_id: TypeId, + pub build: ActionBuilder, +} + +/// This constant must be public to be accessible from other crates. +/// But its existence is an implementation detail and should not be used directly. +#[doc(hidden)] +#[linkme::distributed_slice] +pub static __GPUI_ACTIONS: [MacroActionBuilder]; + +impl ActionRegistry { + /// Load all registered actions into the registry. + pub(crate) fn load_actions(&mut self) { + for builder in __GPUI_ACTIONS { + let action = builder(); + self.insert_action(action); + } + } + + #[cfg(test)] + pub(crate) fn load_action(&mut self) { + self.insert_action(ActionData { + name: A::debug_name(), + type_id: TypeId::of::(), + build: A::build, + }); + } + + fn insert_action(&mut self, action: ActionData) { + let name: SharedString = action.name.into(); + self.builders_by_name.insert(name.clone(), action.build); + self.names_by_type_id.insert(action.type_id, name.clone()); + self.all_names.push(name); + } + + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action_type(&self, type_id: &TypeId) -> Result> { + let name = self + .names_by_type_id + .get(type_id) + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + + self.build_action(&name, None) + } + + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action( + &self, + name: &str, + params: Option, + ) -> Result> { + let build_action = self + .builders_by_name + .get(name) + .ok_or_else(|| anyhow!("no action type registered for {}", name))?; + (build_action)(params.unwrap_or_else(|| json!({}))) + .with_context(|| format!("Attempting to build action {}", name)) + } + + pub fn all_action_names(&self) -> &[SharedString] { + self.all_names.as_slice() + } +} + +/// Defines unit structs that can be used as actions. +/// To use more complex data types as actions, use `impl_actions!` +#[macro_export] +macro_rules! actions { + ($namespace:path, [ $($name:ident),* $(,)? ]) => { + $( + #[doc = "The `"] + #[doc = stringify!($name)] + #[doc = "` action, see [`gpui::actions!`]"] + #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)] + #[serde(crate = "gpui::private::serde")] + pub struct $name; + + gpui::__impl_action!($namespace, $name, + fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box> { + Ok(Box::new(Self)) + } + ); + + gpui::register_action!($name); + )* + }; +} + +/// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize +#[macro_export] +macro_rules! impl_actions { + ($namespace:path, [ $($name:ident),* $(,)? ]) => { + $( + gpui::__impl_action!($namespace, $name, + fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box> { + Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::(value)?)) + } + ); + + gpui::register_action!($name); + )* + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __impl_action { + ($namespace:path, $name:ident, $build:item) => { + impl gpui::Action for $name { + fn name(&self) -> &'static str + { + concat!( + stringify!($namespace), + "::", + stringify!($name), + ) + } + + fn debug_name() -> &'static str + where + Self: ::std::marker::Sized + { + concat!( + stringify!($namespace), + "::", + stringify!($name), + ) + } + + $build + + fn partial_eq(&self, action: &dyn gpui::Action) -> bool { + action + .as_any() + .downcast_ref::() + .map_or(false, |a| self == a) + } + + fn boxed_clone(&self) -> std::boxed::Box { + ::std::boxed::Box::new(self.clone()) + } + + fn as_any(&self) -> &dyn ::std::any::Any { + self + } + } + }; +} + +mod no_action { + use crate as gpui; + + actions!(zed, [NoAction]); +} diff --git a/crates/ming/src/app.rs b/crates/ming/src/app.rs new file mode 100644 index 0000000..d77813a --- /dev/null +++ b/crates/ming/src/app.rs @@ -0,0 +1,1440 @@ +use std::{ + any::{type_name, TypeId}, + cell::{Ref, RefCell, RefMut}, + marker::PhantomData, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::{atomic::Ordering::SeqCst, Arc}, + time::Duration, +}; + +use anyhow::{anyhow, Result}; +use derive_more::{Deref, DerefMut}; +use futures::{channel::oneshot, future::{LocalBoxFuture, FutureExt}, Future}; +use slotmap::SlotMap; +use time::UtcOffset; + +pub use async_context::*; +use collections::{FxHashMap, FxHashSet, VecDeque}; +pub use entity_map::*; +use http::{self, HttpClient}; +pub use model_context::*; +#[cfg(any(test, feature = "test-support"))] +pub use test_context::*; +use util::ResultExt; + +use crate::{ + current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle, + AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context, + DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, + Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, + PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation, + SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext, + Window, WindowAppearance, WindowContext, WindowHandle, WindowId, +}; + +mod async_context; +mod entity_map; +mod model_context; +#[cfg(any(test, feature = "test-support"))] +mod test_context; + +/// The duration for which futures returned from [AppContext::on_app_context] or [ModelContext::on_app_quit] can run before the application fully quits. +pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100); + +/// Temporary(?) wrapper around [`RefCell`] to help us debug any double borrows. +/// Strongly consider removing after stabilization. +#[doc(hidden)] +pub struct AppCell { + app: RefCell, +} + +impl AppCell { + #[doc(hidden)] + #[track_caller] + pub fn borrow(&self) -> AppRef { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + AppRef(self.app.borrow()) + } + + #[doc(hidden)] + #[track_caller] + pub fn borrow_mut(&self) -> AppRefMut { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + AppRefMut(self.app.borrow_mut()) + } +} + +#[doc(hidden)] +#[derive(Deref, DerefMut)] +pub struct AppRef<'a>(Ref<'a, AppContext>); + +impl<'a> Drop for AppRef<'a> { + fn drop(&mut self) { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("dropped borrow from {thread_id:?}"); + } + } +} + +#[doc(hidden)] +#[derive(Deref, DerefMut)] +pub struct AppRefMut<'a>(RefMut<'a, AppContext>); + +impl<'a> Drop for AppRefMut<'a> { + fn drop(&mut self) { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("dropped {thread_id:?}"); + } + } +} + +/// A reference to a GPUI application, typically constructed in the `main` function of your app. +/// You won't interact with this type much outside of initial configuration and startup. +pub struct App(Rc); + +/// Represents an application before it is fully launched. Once your app is +/// configured, you'll start the app with `App::run`. +impl App { + /// Builds an app with the given asset source. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + #[cfg(any(test, feature = "test-support"))] + log::info!("GPUI was compiled in test mode"); + + Self(AppContext::new( + current_platform(), + Arc::new(()), + http::client(), + )) + } + + /// Assign + pub fn with_assets(self, asset_source: impl AssetSource) -> Self { + let mut context_lock = self.0.borrow_mut(); + let asset_source = Arc::new(asset_source); + context_lock.asset_source = asset_source.clone(); + context_lock.svg_renderer = SvgRenderer::new(asset_source); + drop(context_lock); + self + } + + /// Start the application. The provided callback will be called once the + /// app is fully launched. + pub fn run(self, on_finish_launching: F) + where + F: 'static + FnOnce(&mut AppContext), + { + let this = self.0.clone(); + let platform = self.0.borrow().platform.clone(); + platform.run(Box::new(move || { + let cx = &mut *this.borrow_mut(); + on_finish_launching(cx); + })); + } + + /// Register a handler to be invoked when the platform instructs the application + /// to open one or more URLs. + pub fn on_open_urls(&self, mut callback: F) -> &Self + where + F: 'static + FnMut(Vec), + { + self.0.borrow().platform.on_open_urls(Box::new(callback)); + self + } + + /// Invokes a handler when an already-running application is launched. + /// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock. + pub fn on_reopen(&self, mut callback: F) -> &Self + where + F: 'static + FnMut(&mut AppContext), + { + let this = Rc::downgrade(&self.0); + self.0.borrow_mut().platform.on_reopen(Box::new(move || { + if let Some(app) = this.upgrade() { + callback(&mut app.borrow_mut()); + } + })); + self + } + + /// Returns metadata associated with the application + pub fn metadata(&self) -> AppMetadata { + self.0.borrow().app_metadata.clone() + } + + /// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background. + pub fn background_executor(&self) -> BackgroundExecutor { + self.0.borrow().background_executor.clone() + } + + /// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground. + pub fn foreground_executor(&self) -> ForegroundExecutor { + self.0.borrow().foreground_executor.clone() + } + + /// Returns a reference to the [`TextSystem`] associated with this app. + pub fn text_system(&self) -> Arc { + self.0.borrow().text_system.clone() + } + + /// Returns the file URL of the executable with the specified name in the application bundle + pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.0.borrow().path_for_auxiliary_executable(name) + } +} + +type Handler = Box bool + 'static>; +type Listener = Box bool + 'static>; +type KeystrokeObserver = Box; +type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; +type ReleaseListener = Box; +type NewViewListener = Box; + +/// Contains the state of the full application, and passed as a reference to a variety of callbacks. +/// Other contexts such as [ModelContext], [WindowContext], and [ViewContext] deref to this type, making it the most general context type. +/// You need a reference to an `AppContext` to access the state of a [Model]. +pub struct AppContext { + pub(crate) this: Weak, + pub(crate) platform: Rc, + app_metadata: AppMetadata, + text_system: Arc, + flushing_effects: bool, + pending_updates: usize, + pub(crate) actions: Rc, + pub(crate) active_drag: Option, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box>, + pub(crate) asset_cache: AssetCache, + asset_source: Arc, + pub(crate) svg_renderer: SvgRenderer, + http_client: Arc, + pub(crate) globals_by_type: FxHashMap>, + pub(crate) entities: EntityMap, + pub(crate) new_view_observers: SubscriberSet, + pub(crate) windows: SlotMap>, + pub(crate) window_handles: FxHashMap, + pub(crate) keymap: Rc>, + pub(crate) global_action_listeners: + FxHashMap>>, + pending_effects: VecDeque, + pub(crate) pending_notifications: FxHashSet, + pub(crate) pending_global_notifications: FxHashSet, + pub(crate) observers: SubscriberSet, + // TypeId is the type of the event that the listener callback expects + pub(crate) event_listeners: SubscriberSet, + pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>, + pub(crate) release_listeners: SubscriberSet, + pub(crate) global_observers: SubscriberSet, + pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, + pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. + pub(crate) propagate_event: bool, + pub(crate) prompt_builder: Option, +} + +impl AppContext { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new( + platform: Rc, + asset_source: Arc, + http_client: Arc, + ) -> Rc { + let executor = platform.background_executor(); + let foreground_executor = platform.foreground_executor(); + assert!( + executor.is_main_thread(), + "must construct App on main thread" + ); + + let text_system = Arc::new(TextSystem::new(platform.text_system())); + let entities = EntityMap::new(); + + let app_metadata = AppMetadata { + os_name: platform.os_name(), + os_version: platform.os_version().ok(), + app_version: platform.app_version().ok(), + }; + + let app = Rc::new_cyclic(|this| AppCell { + app: RefCell::new(AppContext { + this: this.clone(), + platform: platform.clone(), + app_metadata, + text_system, + actions: Rc::new(ActionRegistry::default()), + flushing_effects: false, + pending_updates: 0, + active_drag: None, + background_executor: executor, + foreground_executor, + svg_renderer: SvgRenderer::new(asset_source.clone()), + asset_cache: AssetCache::new(), + loading_assets: Default::default(), + asset_source, + http_client, + globals_by_type: FxHashMap::default(), + entities, + new_view_observers: SubscriberSet::new(), + window_handles: FxHashMap::default(), + windows: SlotMap::with_key(), + keymap: Rc::new(RefCell::new(Keymap::default())), + global_action_listeners: FxHashMap::default(), + pending_effects: VecDeque::new(), + pending_notifications: FxHashSet::default(), + pending_global_notifications: FxHashSet::default(), + observers: SubscriberSet::new(), + event_listeners: SubscriberSet::new(), + release_listeners: SubscriberSet::new(), + keystroke_observers: SubscriberSet::new(), + global_observers: SubscriberSet::new(), + quit_observers: SubscriberSet::new(), + layout_id_buffer: Default::default(), + propagate_event: true, + prompt_builder: Some(PromptBuilder::Default), + }), + }); + + init_app_menus(platform.as_ref(), &mut app.borrow_mut()); + + platform.on_quit(Box::new({ + let cx = app.clone(); + move || { + cx.borrow_mut().shutdown(); + } + })); + + app + } + + /// Quit the application gracefully. Handlers registered with [`ModelContext::on_app_quit`] + /// will be given 100ms to complete before exiting. + pub fn shutdown(&mut self) { + let mut futures = Vec::new(); + + for observer in self.quit_observers.remove(&()) { + futures.push(observer(self)); + } + + self.windows.clear(); + self.window_handles.clear(); + self.flush_effects(); + + let futures = futures::future::join_all(futures); + if self + .background_executor + .block_with_timeout(SHUTDOWN_TIMEOUT, futures) + .is_err() + { + log::error!("timed out waiting on app_will_quit"); + } + } + + /// Gracefully quit the application via the platform's standard routine. + pub fn quit(&mut self) { + self.platform.quit(); + } + + /// Get metadata about the app and platform. + pub fn app_metadata(&self) -> AppMetadata { + self.app_metadata.clone() + } + + /// Schedules all windows in the application to be redrawn. This can be called + /// multiple times in an update cycle and still result in a single redraw. + pub fn refresh(&mut self) { + self.pending_effects.push_back(Effect::Refresh); + } + + pub(crate) fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { + self.pending_updates += 1; + let result = update(self); + if !self.flushing_effects && self.pending_updates == 1 { + self.flushing_effects = true; + self.flush_effects(); + self.flushing_effects = false; + } + self.pending_updates -= 1; + result + } + + /// Arrange a callback to be invoked when the given model or view calls `notify` on its respective context. + pub fn observe( + &mut self, + entity: &E, + mut on_notify: impl FnMut(E, &mut AppContext) + 'static, + ) -> Subscription + where + W: 'static, + E: Entity, + { + self.observe_internal(entity, move |e, cx| { + on_notify(e, cx); + true + }) + } + + pub(crate) fn new_observer(&mut self, key: EntityId, value: Handler) -> Subscription { + let (subscription, activate) = self.observers.insert(key, value); + self.defer(move |_| activate()); + subscription + } + pub(crate) fn observe_internal( + &mut self, + entity: &E, + mut on_notify: impl FnMut(E, &mut AppContext) -> bool + 'static, + ) -> Subscription + where + W: 'static, + E: Entity, + { + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + self.new_observer( + entity_id, + Box::new(move |cx| { + if let Some(handle) = E::upgrade_from(&handle) { + on_notify(handle, cx) + } else { + false + } + }), + ) + } + + /// Arrange for the given callback to be invoked whenever the given model or view emits an event of a given type. + /// The callback is provided a handle to the emitting entity and a reference to the emitted event. + pub fn subscribe( + &mut self, + entity: &E, + mut on_event: impl FnMut(E, &Event, &mut AppContext) + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + E: Entity, + Event: 'static, + { + self.subscribe_internal(entity, move |entity, event, cx| { + on_event(entity, event, cx); + true + }) + } + + pub(crate) fn new_subscription( + &mut self, + key: EntityId, + value: (TypeId, Listener), + ) -> Subscription { + let (subscription, activate) = self.event_listeners.insert(key, value); + self.defer(move |_| activate()); + subscription + } + pub(crate) fn subscribe_internal( + &mut self, + entity: &E, + mut on_event: impl FnMut(E, &Evt, &mut AppContext) -> bool + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + E: Entity, + Evt: 'static, + { + let entity_id = entity.entity_id(); + let entity = entity.downgrade(); + self.new_subscription( + entity_id, + ( + TypeId::of::(), + Box::new(move |event, cx| { + let event: &Evt = event.downcast_ref().expect("invalid event type"); + if let Some(handle) = E::upgrade_from(&entity) { + on_event(handle, event, cx) + } else { + false + } + }), + ), + ) + } + + /// Returns handles to all open windows in the application. + /// Each handle could be downcast to a handle typed for the root view of that window. + /// To find all windows of a given type, you could filter on + pub fn windows(&self) -> Vec { + self.windows + .keys() + .flat_map(|window_id| self.window_handles.get(&window_id).copied()) + .collect() + } + + /// Returns a handle to the window that is currently focused at the platform level, if one exists. + pub fn active_window(&self) -> Option { + self.platform.active_window() + } + + /// Opens a new window with the given option and the root view returned by the given function. + /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific + /// functionality. + pub fn open_window( + &mut self, + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut WindowContext) -> View, + ) -> WindowHandle { + self.update(|cx| { + let id = cx.windows.insert(None); + let handle = WindowHandle::new(id); + let mut window = Window::new(handle.into(), options, cx); + let root_view = build_root_view(&mut WindowContext::new(cx, &mut window)); + window.root_view.replace(root_view.into()); + cx.window_handles.insert(id, window.handle); + cx.windows.get_mut(id).unwrap().replace(window); + handle + }) + } + + /// Instructs the platform to activate the application by bringing it to the foreground. + pub fn activate(&self, ignoring_other_apps: bool) { + self.platform.activate(ignoring_other_apps); + } + + /// Hide the application at the platform level. + pub fn hide(&self) { + self.platform.hide(); + } + + /// Hide other applications at the platform level. + pub fn hide_other_apps(&self) { + self.platform.hide_other_apps(); + } + + /// Unhide other applications at the platform level. + pub fn unhide_other_apps(&self) { + self.platform.unhide_other_apps(); + } + + /// Returns the list of currently active displays. + pub fn displays(&self) -> Vec> { + self.platform.displays() + } + + /// Returns the primary display that will be used for new windows. + pub fn primary_display(&self) -> Option> { + self.platform.primary_display() + } + + /// Returns the display with the given ID, if one exists. + pub fn find_display(&self, id: DisplayId) -> Option> { + self.displays() + .iter() + .find(|display| display.id() == id) + .cloned() + } + + /// Returns the appearance of the application's windows. + pub fn window_appearance(&self) -> WindowAppearance { + self.platform.window_appearance() + } + + /// Writes data to the primary selection buffer. + /// Only available on Linux. + pub fn write_to_primary(&self, item: ClipboardItem) { + self.platform.write_to_primary(item) + } + + /// Writes data to the platform clipboard. + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item) + } + + /// Reads data from the primary selection buffer. + /// Only available on Linux. + pub fn read_from_primary(&self) -> Option { + self.platform.read_from_primary() + } + + /// Reads data from the platform clipboard. + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() + } + + /// Writes credentials to the platform keychain. + pub fn write_credentials( + &self, + url: &str, + username: &str, + password: &[u8], + ) -> Task> { + self.platform.write_credentials(url, username, password) + } + + /// Reads credentials from the platform keychain. + pub fn read_credentials(&self, url: &str) -> Task)>>> { + self.platform.read_credentials(url) + } + + /// Deletes credentials from the platform keychain. + pub fn delete_credentials(&self, url: &str) -> Task> { + self.platform.delete_credentials(url) + } + + /// Directs the platform's default browser to open the given URL. + pub fn open_url(&self, url: &str) { + self.platform.open_url(url); + } + + /// register_url_scheme requests that the given scheme (e.g. `zed` for `zed://` urls) + /// is opened by the current app. + /// On some platforms (e.g. macOS) you may be able to register URL schemes as part of app + /// distribution, but this method exists to let you register schemes at runtime. + pub fn register_url_scheme(&self, scheme: &str) -> Task> { + self.platform.register_url_scheme(scheme) + } + + /// Returns the full pathname of the current app bundle. + /// If the app is not being run from a bundle, returns an error. + pub fn app_path(&self) -> Result { + self.platform.app_path() + } + + /// Returns the file URL of the executable with the specified name in the application bundle + pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.platform.path_for_auxiliary_executable(name) + } + + /// Displays a platform modal for selecting paths. + /// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. + pub fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + self.platform.prompt_for_paths(options) + } + + /// Displays a platform modal for selecting a new path where a file can be saved. + /// The provided directory will be used to set the initial location. + /// When a path is selected, it is relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. + pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + self.platform.prompt_for_new_path(directory) + } + + /// Reveals the specified path at the platform level, such as in Finder on macOS. + pub fn reveal_path(&self, path: &Path) { + self.platform.reveal_path(path) + } + + /// Returns whether the user has configured scrollbars to auto-hide at the platform level. + pub fn should_auto_hide_scrollbars(&self) -> bool { + self.platform.should_auto_hide_scrollbars() + } + + /// Restart the application. + pub fn restart(&self, binary_path: Option) { + self.platform.restart(binary_path) + } + + /// Returns the local timezone at the platform level. + pub fn local_timezone(&self) -> UtcOffset { + self.platform.local_timezone() + } + + /// Returns the http client assigned to GPUI + pub fn http_client(&self) -> Arc { + self.http_client.clone() + } + + /// Returns the SVG renderer GPUI uses + pub(crate) fn svg_renderer(&self) -> SvgRenderer { + self.svg_renderer.clone() + } + + pub(crate) fn push_effect(&mut self, effect: Effect) { + match &effect { + Effect::Notify { emitter } => { + if !self.pending_notifications.insert(*emitter) { + return; + } + } + Effect::NotifyGlobalObservers { global_type } => { + if !self.pending_global_notifications.insert(*global_type) { + return; + } + } + _ => {} + }; + + self.pending_effects.push_back(effect); + } + + /// Called at the end of [`AppContext::update`] to complete any side effects + /// such as notifying observers, emitting events, etc. Effects can themselves + /// cause effects, so we continue looping until all effects are processed. + fn flush_effects(&mut self) { + loop { + self.release_dropped_entities(); + self.release_dropped_focus_handles(); + + if let Some(effect) = self.pending_effects.pop_front() { + match effect { + Effect::Notify { emitter } => { + self.apply_notify_effect(emitter); + } + + Effect::Emit { + emitter, + event_type, + event, + } => self.apply_emit_effect(emitter, event_type, event), + + Effect::Refresh => { + self.apply_refresh_effect(); + } + + Effect::NotifyGlobalObservers { global_type } => { + self.apply_notify_global_observers_effect(global_type); + } + + Effect::Defer { callback } => { + self.apply_defer_effect(callback); + } + } + } else { + #[cfg(any(test, feature = "test-support"))] + for window in self + .windows + .values() + .filter_map(|window| { + let window = window.as_ref()?; + window.dirty.get().then_some(window.handle) + }) + .collect::>() + { + self.update_window(window, |_, cx| cx.draw()).unwrap(); + } + + if self.pending_effects.is_empty() { + break; + } + } + } + } + + /// Repeatedly called during `flush_effects` to release any entities whose + /// reference count has become zero. We invoke any release observers before dropping + /// each entity. + fn release_dropped_entities(&mut self) { + loop { + let dropped = self.entities.take_dropped(); + if dropped.is_empty() { + break; + } + + for (entity_id, mut entity) in dropped { + self.observers.remove(&entity_id); + self.event_listeners.remove(&entity_id); + for release_callback in self.release_listeners.remove(&entity_id) { + release_callback(entity.as_mut(), self); + } + } + } + } + + /// Repeatedly called during `flush_effects` to handle a focused handle being dropped. + fn release_dropped_focus_handles(&mut self) { + for window_handle in self.windows() { + window_handle + .update(self, |_, cx| { + let mut blur_window = false; + let focus = cx.window.focus; + cx.window.focus_handles.write().retain(|handle_id, count| { + if count.load(SeqCst) == 0 { + if focus == Some(handle_id) { + blur_window = true; + } + false + } else { + true + } + }); + + if blur_window { + cx.blur(); + } + }) + .unwrap(); + } + } + + fn apply_notify_effect(&mut self, emitter: EntityId) { + self.pending_notifications.remove(&emitter); + + self.observers + .clone() + .retain(&emitter, |handler| handler(self)); + } + + fn apply_emit_effect(&mut self, emitter: EntityId, event_type: TypeId, event: Box) { + self.event_listeners + .clone() + .retain(&emitter, |(stored_type, handler)| { + if *stored_type == event_type { + handler(event.as_ref(), self) + } else { + true + } + }); + } + + fn apply_refresh_effect(&mut self) { + for window in self.windows.values_mut() { + if let Some(window) = window.as_mut() { + window.dirty.set(true); + } + } + } + + fn apply_notify_global_observers_effect(&mut self, type_id: TypeId) { + self.pending_global_notifications.remove(&type_id); + self.global_observers + .clone() + .retain(&type_id, |observer| observer(self)); + } + + fn apply_defer_effect(&mut self, callback: Box) { + callback(self); + } + + /// Creates an `AsyncAppContext`, which can be cloned and has a static lifetime + /// so it can be held across `await` points. + pub fn to_async(&self) -> AsyncAppContext { + AsyncAppContext { + app: self.this.clone(), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), + } + } + + /// Obtains a reference to the executor, which can be used to spawn futures. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Obtains a reference to the executor, which can be used to spawn futures. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Spawns the future returned by the given function on the thread pool. The closure will be invoked + /// with [AsyncAppContext], which allows the application state to be accessed across await points. + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.to_async())) + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&mut self, f: impl FnOnce(&mut AppContext) + 'static) { + self.push_effect(Effect::Defer { + callback: Box::new(f), + }); + } + + /// Accessor for the application's asset source, which is provided when constructing the `App`. + pub fn asset_source(&self) -> &Arc { + &self.asset_source + } + + /// Accessor for the text system. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Check whether a global of the given type has been assigned. + pub fn has_global(&self) -> bool { + self.globals_by_type.contains_key(&TypeId::of::()) + } + + /// Access the global of the given type. Panics if a global for that type has not been assigned. + #[track_caller] + pub fn global(&self) -> &G { + self.globals_by_type + .get(&TypeId::of::()) + .map(|any_state| any_state.downcast_ref::().unwrap()) + .ok_or_else(|| anyhow!("no state of type {} exists", type_name::())) + .unwrap() + } + + /// Access the global of the given type if a value has been assigned. + pub fn try_global(&self) -> Option<&G> { + self.globals_by_type + .get(&TypeId::of::()) + .map(|any_state| any_state.downcast_ref::().unwrap()) + } + + /// Access the global of the given type mutably. Panics if a global for that type has not been assigned. + #[track_caller] + pub fn global_mut(&mut self) -> &mut G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type + .get_mut(&global_type) + .and_then(|any_state| any_state.downcast_mut::()) + .ok_or_else(|| anyhow!("no state of type {} exists", type_name::())) + .unwrap() + } + + /// Access the global of the given type mutably. A default value is assigned if a global of this type has not + /// yet been assigned. + pub fn default_global(&mut self) -> &mut G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type + .entry(global_type) + .or_insert_with(|| Box::::default()) + .downcast_mut::() + .unwrap() + } + + /// Sets the value of the global of the given type. + pub fn set_global(&mut self, global: G) { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type.insert(global_type, Box::new(global)); + } + + /// Clear all stored globals. Does not notify global observers. + #[cfg(any(test, feature = "test-support"))] + pub fn clear_globals(&mut self) { + self.globals_by_type.drain(); + } + + /// Remove the global of the given type from the app context. Does not notify global observers. + pub fn remove_global(&mut self) -> G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + *self + .globals_by_type + .remove(&global_type) + .unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::())) + .downcast() + .unwrap() + } + + /// Register a callback to be invoked when a global of the given type is updated. + pub fn observe_global( + &mut self, + mut f: impl FnMut(&mut Self) + 'static, + ) -> Subscription { + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + f(cx); + true + }), + ); + self.defer(move |_| activate()); + subscription + } + + /// Move the global of the given type to the stack. + pub(crate) fn lease_global(&mut self) -> GlobalLease { + GlobalLease::new( + self.globals_by_type + .remove(&TypeId::of::()) + .ok_or_else(|| anyhow!("no global registered of type {}", type_name::())) + .unwrap(), + ) + } + + /// Restore the global of the given type after it is moved to the stack. + pub(crate) fn end_global_lease(&mut self, lease: GlobalLease) { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type.insert(global_type, lease.global); + } + + pub(crate) fn new_view_observer( + &mut self, + key: TypeId, + value: NewViewListener, + ) -> Subscription { + let (subscription, activate) = self.new_view_observers.insert(key, value); + activate(); + subscription + } + /// Arrange for the given function to be invoked whenever a view of the specified type is created. + /// The function will be passed a mutable reference to the view along with an appropriate context. + pub fn observe_new_views( + &mut self, + on_new: impl 'static + Fn(&mut V, &mut ViewContext), + ) -> Subscription { + self.new_view_observer( + TypeId::of::(), + Box::new(move |any_view: AnyView, cx: &mut WindowContext| { + any_view + .downcast::() + .unwrap() + .update(cx, |view_state, cx| { + on_new(view_state, cx); + }) + }), + ) + } + + /// Observe the release of a model or view. The callback is invoked after the model or view + /// has no more strong references but before it has been dropped. + pub fn observe_release( + &mut self, + handle: &E, + on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, + ) -> Subscription + where + E: Entity, + T: 'static, + { + let (subscription, activate) = self.release_listeners.insert( + handle.entity_id(), + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + on_release(entity, cx) + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when a keystroke is received by the application + /// in any window. Note that this fires after all other action and event mechanisms have resolved + /// and that this API will not be invoked if the event's propagation is stopped. + pub fn observe_keystrokes( + &mut self, + f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static, + ) -> Subscription { + fn inner( + keystroke_observers: &mut SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_observers.insert((), handler); + activate(); + subscription + } + inner(&mut self.keystroke_observers, Box::new(f)) + } + + /// Register key bindings. + pub fn bind_keys(&mut self, bindings: impl IntoIterator) { + self.keymap.borrow_mut().add_bindings(bindings); + self.pending_effects.push_back(Effect::Refresh); + } + + /// Clear all key bindings in the app. + pub fn clear_key_bindings(&mut self) { + self.keymap.borrow_mut().clear(); + self.pending_effects.push_back(Effect::Refresh); + } + + /// Register a global listener for actions invoked via the keyboard. + pub fn on_action(&mut self, listener: impl Fn(&A, &mut Self) + 'static) { + self.global_action_listeners + .entry(TypeId::of::()) + .or_default() + .push(Rc::new(move |action, phase, cx| { + if phase == DispatchPhase::Bubble { + let action = action.downcast_ref().unwrap(); + listener(action, cx) + } + })); + } + + /// Event handlers propagate events by default. Call this method to stop dispatching to + /// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is + /// the opposite of [`Self::propagate`]. It's also possible to cancel a call to [`Self::propagate`] by + /// calling this method before effects are flushed. + pub fn stop_propagation(&mut self) { + self.propagate_event = false; + } + + /// Action handlers stop propagation by default during the bubble phase of action dispatch + /// dispatching to action handlers higher in the element tree. This is the opposite of + /// [`Self::stop_propagation`]. It's also possible to cancel a call to [`Self::stop_propagation`] by calling + /// this method before effects are flushed. + pub fn propagate(&mut self) { + self.propagate_event = true; + } + + /// Build an action from some arbitrary data, typically a keymap entry. + pub fn build_action( + &self, + name: &str, + data: Option, + ) -> Result> { + self.actions.build_action(name, data) + } + + /// Get a list of all action names that have been registered. + /// in the application. Note that registration only allows for + /// actions to be built dynamically, and is unrelated to binding + /// actions in the element tree. + pub fn all_action_names(&self) -> &[SharedString] { + self.actions.all_action_names() + } + + /// Register a callback to be invoked when the application is about to quit. + /// It is not possible to cancel the quit event at this point. + pub fn on_app_quit( + &mut self, + mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + { + let (subscription, activate) = self.quit_observers.insert( + (), + Box::new(move |cx| { + let future = on_quit(cx); + future.boxed_local() + }), + ); + activate(); + subscription + } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + for window in self.windows() { + window + .update(self, |_, cx| { + cx.window + .rendered_frame + .dispatch_tree + .clear_pending_keystrokes(); + cx.window + .next_frame + .dispatch_tree + .clear_pending_keystrokes(); + }) + .ok(); + } + } + + /// Checks if the given action is bound in the current context, as defined by the app's current focus, + /// the bindings in the element tree, and any global action listeners. + pub fn is_action_available(&mut self, action: &dyn Action) -> bool { + let mut action_available = false; + if let Some(window) = self.active_window() { + if let Ok(window_action_available) = + window.update(self, |_, cx| cx.is_action_available(action)) + { + action_available = window_action_available; + } + } + + action_available + || self + .global_action_listeners + .contains_key(&action.as_any().type_id()) + } + + /// Sets the menu bar for this application. This will replace any existing menu bar. + pub fn set_menus(&mut self, menus: Vec

) { + self.platform.set_menus(menus, &self.keymap.borrow()); + } + + /// Adds given path to the bottom of the list of recent paths for the application. + /// The list is usually shown on the application icon's context menu in the dock, + /// and allows to open the recent files via that context menu. + /// If the path is already in the list, it will be moved to the bottom of the list. + pub fn add_recent_document(&mut self, path: &Path) { + self.platform.add_recent_document(path); + } + + /// Dispatch an action to the currently active window or global action handler + /// See [action::Action] for more information on how actions work + pub fn dispatch_action(&mut self, action: &dyn Action) { + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .log_err(); + } else { + self.dispatch_global_action(action); + } + } + + fn dispatch_global_action(&mut self, action: &dyn Action) { + self.propagate_event = true; + + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if self.propagate_event { + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + } + + /// Is there currently something being dragged? + pub fn has_active_drag(&self) -> bool { + self.active_drag.is_some() + } + + /// Set the prompt renderer for GPUI. This will replace the default or platform specific + /// prompts with this custom implementation. + pub fn set_prompt_builder( + &mut self, + renderer: impl Fn( + PromptLevel, + &str, + Option<&str>, + &[&str], + PromptHandle, + &mut WindowContext, + ) -> RenderablePromptHandle + + 'static, + ) { + self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer))) + } +} + +impl Context for AppContext { + type Result = T; + + /// Build an entity that is owned by the application. The given function will be invoked with + /// a `ModelContext` and must return an object representing the entity. A `Model` handle will be returned, + /// which can be used to access the entity in a context. + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Model { + self.update(|cx| { + let slot = cx.entities.reserve(); + let entity = build_model(&mut ModelContext::new(cx, slot.downgrade())); + cx.entities.insert(slot, entity) + }) + } + + fn reserve_model(&mut self) -> Self::Result> { + Reservation(self.entities.reserve()) + } + + fn insert_model( + &mut self, + reservation: Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.update(|cx| { + let slot = reservation.0; + let entity = build_model(&mut ModelContext::new(cx, slot.downgrade())); + cx.entities.insert(slot, entity) + }) + } + + /// Updates the entity referenced by the given model. The function is passed a mutable reference to the + /// entity along with a `ModelContext` for the entity. + fn update_model( + &mut self, + model: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> R { + self.update(|cx| { + let mut entity = cx.entities.lease(model); + let result = update(&mut entity, &mut ModelContext::new(cx, model.downgrade())); + cx.entities.end_lease(entity); + result + }) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + let entity = self.entities.read(handle); + read(entity, self) + } + + fn update_window(&mut self, handle: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.update(|cx| { + let mut window = cx + .windows + .get_mut(handle.id) + .ok_or_else(|| anyhow!("window not found"))? + .take() + .ok_or_else(|| anyhow!("window not found"))?; + + let root_view = window.root_view.clone().unwrap(); + let result = update(root_view, &mut WindowContext::new(cx, &mut window)); + + if window.removed { + cx.window_handles.remove(&handle.id); + cx.windows.remove(handle.id); + } else { + cx.windows + .get_mut(handle.id) + .ok_or_else(|| anyhow!("window not found"))? + .replace(window); + } + + Ok(result) + }) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + let window = self + .windows + .get(window.id) + .ok_or_else(|| anyhow!("window not found"))? + .as_ref() + .unwrap(); + + let root_view = window.root_view.clone().unwrap(); + let view = root_view + .downcast::() + .map_err(|_| anyhow!("root view's type has changed"))?; + + Ok(read(view, self)) + } +} + +/// These effects are processed at the end of each application update cycle. +pub(crate) enum Effect { + Notify { + emitter: EntityId, + }, + Emit { + emitter: EntityId, + event_type: TypeId, + event: Box, + }, + Refresh, + NotifyGlobalObservers { + global_type: TypeId, + }, + Defer { + callback: Box, + }, +} + +/// Wraps a global variable value during `update_global` while the value has been moved to the stack. +pub(crate) struct GlobalLease { + global: Box, + global_type: PhantomData, +} + +impl GlobalLease { + fn new(global: Box) -> Self { + GlobalLease { + global, + global_type: PhantomData, + } + } +} + +impl Deref for GlobalLease { + type Target = G; + + fn deref(&self) -> &Self::Target { + self.global.downcast_ref().unwrap() + } +} + +impl DerefMut for GlobalLease { + fn deref_mut(&mut self) -> &mut Self::Target { + self.global.downcast_mut().unwrap() + } +} + +/// Contains state associated with an active drag operation, started by dragging an element +/// within the window or by dragging into the app from the underlying platform. +pub struct AnyDrag { + /// The view used to render this drag + pub view: AnyView, + + /// The value of the dragged item, to be dropped + pub value: Box, + + /// This is used to render the dragged item in the same place + /// on the original element that the drag was initiated + pub cursor_offset: Point, +} + +/// Contains state associated with a tooltip. You'll only need this struct if you're implementing +/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip]. +#[derive(Clone)] +pub struct AnyTooltip { + /// The view used to display the tooltip + pub view: AnyView, + + /// The absolute position of the mouse when the tooltip was deployed. + pub mouse_position: Point, +} + +/// A keystroke event, and potentially the associated action +#[derive(Debug)] +pub struct KeystrokeEvent { + /// The keystroke that occurred + pub keystroke: Keystroke, + + /// The action that was resolved for the keystroke, if any + pub action: Option>, +} diff --git a/crates/ming/src/app/async_context.rs b/crates/ming/src/app/async_context.rs new file mode 100644 index 0000000..a0e463d --- /dev/null +++ b/crates/ming/src/app/async_context.rs @@ -0,0 +1,417 @@ +use crate::{ + AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context, + DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, PromptLevel, + Render, Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext, + WindowHandle, +}; +use anyhow::{anyhow, Context as _}; +use derive_more::{Deref, DerefMut}; +use futures::channel::oneshot; +use std::{future::Future, rc::Weak}; + +/// An async-friendly version of [AppContext] with a static lifetime so it can be held across `await` points in async code. +/// You're provided with an instance when calling [AppContext::spawn], and you can also create one with [AppContext::to_async]. +/// Internally, this holds a weak reference to an `AppContext`, so its methods are fallible to protect against cases where the [AppContext] is dropped. +#[derive(Clone)] +pub struct AsyncAppContext { + pub(crate) app: Weak, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, +} + +impl Context for AsyncAppContext { + type Result = Result; + + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut app = app.borrow_mut(); + Ok(app.new_model(build_model)) + } + + fn reserve_model(&mut self) -> Result> { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut app = app.borrow_mut(); + Ok(app.reserve_model()) + } + + fn insert_model( + &mut self, + reservation: Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Result> { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut app = app.borrow_mut(); + Ok(app.insert_model(reservation, build_model)) + } + + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Self::Result { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut app = app.borrow_mut(); + Ok(app.update_model(handle, update)) + } + + fn read_model( + &self, + handle: &Model, + callback: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + let app = self.app.upgrade().context("app was released")?; + let lock = app.borrow(); + Ok(lock.read_model(handle, callback)) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.borrow_mut(); + lock.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.upgrade().context("app was released")?; + let lock = app.borrow(); + lock.read_window(window, read) + } +} + +impl AsyncAppContext { + /// Schedules all windows in the application to be redrawn. + pub fn refresh(&mut self) -> Result<()> { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut lock = app.borrow_mut(); + lock.refresh(); + Ok(()) + } + + /// Get an executor which can be used to spawn futures in the background. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Get an executor which can be used to spawn futures in the foreground. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Invoke the given function in the context of the app, then flush any effects produced during its invocation. + pub fn update(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut lock = app.borrow_mut(); + Ok(f(&mut lock)) + } + + /// Open a window with the given options based on the root view returned by the given function. + pub fn open_window( + &self, + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut WindowContext) -> View, + ) -> Result> + where + V: 'static + Render, + { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut lock = app.borrow_mut(); + Ok(lock.open_window(options, build_root_view)) + } + + /// Schedule a future to be polled in the background. + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.clone())) + } + + /// Determine whether global state of the specified type has been assigned. + /// Returns an error if the `AppContext` has been dropped. + pub fn has_global(&self) -> Result { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let app = app.borrow_mut(); + Ok(app.has_global::()) + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// + /// Panics if no global state of the specified type has been assigned. + /// Returns an error if the `AppContext` has been dropped. + pub fn read_global(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let app = app.borrow_mut(); + Ok(read(app.global(), &app)) + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// + /// Similar to [`AsyncAppContext::read_global`], but returns an error instead of panicking + /// if no state of the specified type has been assigned. + /// + /// Returns an error if no state of the specified type has been assigned the `AppContext` has been dropped. + pub fn try_read_global( + &self, + read: impl FnOnce(&G, &AppContext) -> R, + ) -> Option { + let app = self.app.upgrade()?; + let app = app.borrow_mut(); + Some(read(app.try_global()?, &app)) + } + + /// A convenience method for [AppContext::update_global] + /// for updating the global state of the specified type. + pub fn update_global( + &mut self, + update: impl FnOnce(&mut G, &mut AppContext) -> R, + ) -> Result { + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut app = app.borrow_mut(); + Ok(app.update(|cx| cx.update_global(update))) + } +} + +/// A cloneable, owned handle to the application context, +/// composed with the window associated with the current task. +#[derive(Clone, Deref, DerefMut)] +pub struct AsyncWindowContext { + #[deref] + #[deref_mut] + app: AsyncAppContext, + window: AnyWindowHandle, +} + +impl AsyncWindowContext { + pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self { + Self { app, window } + } + + /// Get the handle of the window this context is associated with. + pub fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + /// A convenience method for [`AppContext::update_window`]. + pub fn update(&mut self, update: impl FnOnce(&mut WindowContext) -> R) -> Result { + self.app.update_window(self.window, |_, cx| update(cx)) + } + + /// A convenience method for [`AppContext::update_window`]. + pub fn update_root( + &mut self, + update: impl FnOnce(AnyView, &mut WindowContext) -> R, + ) -> Result { + self.app.update_window(self.window, update) + } + + /// A convenience method for [`WindowContext::on_next_frame`]. + pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { + self.window.update(self, |_, cx| cx.on_next_frame(f)).ok(); + } + + /// A convenience method for [`AppContext::global`]. + pub fn read_global( + &mut self, + read: impl FnOnce(&G, &WindowContext) -> R, + ) -> Result { + self.window.update(self, |_, cx| read(cx.global(), cx)) + } + + /// A convenience method for [`AppContext::update_global`]. + /// for updating the global state of the specified type. + pub fn update_global( + &mut self, + update: impl FnOnce(&mut G, &mut WindowContext) -> R, + ) -> Result + where + G: Global, + { + self.window.update(self, |_, cx| cx.update_global(update)) + } + + /// Schedule a future to be executed on the main thread. This is used for collecting + /// the results of background tasks and updating the UI. + pub fn spawn(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.clone())) + } + + /// Present a platform dialog. + /// The provided message will be presented, along with buttons for each answer. + /// When a button is clicked, the returned Receiver will receive the index of the clicked button. + pub fn prompt( + &mut self, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[&str], + ) -> oneshot::Receiver { + self.window + .update(self, |_, cx| cx.prompt(level, message, detail, answers)) + .unwrap_or_else(|_| oneshot::channel().1) + } +} + +impl Context for AsyncWindowContext { + type Result = Result; + + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Result> + where + T: 'static, + { + self.window.update(self, |_, cx| cx.new_model(build_model)) + } + + fn reserve_model(&mut self) -> Result> { + self.window.update(self, |_, cx| cx.reserve_model()) + } + + fn insert_model( + &mut self, + reservation: Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.window + .update(self, |_, cx| cx.insert_model(reservation, build_model)) + } + + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Result { + self.window + .update(self, |_, cx| cx.update_model(handle, update)) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + self.app.read_model(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.app.update_window(window, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.app.read_window(window, read) + } +} + +impl VisualContext for AsyncWindowContext { + fn new_view( + &mut self, + build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(self, |_, cx| cx.new_view(build_view_state)) + } + + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Self::Result { + self.window + .update(self, |_, cx| cx.update_view(view, update)) + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(self, |_, cx| cx.replace_root_view(build_view)) + } + + fn focus_view(&mut self, view: &View) -> Self::Result<()> + where + V: FocusableView, + { + self.window.update(self, |_, cx| { + view.read(cx).focus_handle(cx).clone().focus(cx); + }) + } + + fn dismiss_view(&mut self, view: &View) -> Self::Result<()> + where + V: crate::ManagedView, + { + self.window + .update(self, |_, cx| view.update(cx, |_, cx| cx.emit(DismissEvent))) + } +} diff --git a/crates/ming/src/app/entity_map.rs b/crates/ming/src/app/entity_map.rs new file mode 100644 index 0000000..b5ef39e --- /dev/null +++ b/crates/ming/src/app/entity_map.rs @@ -0,0 +1,744 @@ +use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext}; +use anyhow::{anyhow, Result}; +use derive_more::{Deref, DerefMut}; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use slotmap::{KeyData, SecondaryMap, SlotMap}; +use std::{ + any::{type_name, Any, TypeId}, + fmt::{self, Display}, + hash::{Hash, Hasher}, + marker::PhantomData, + mem, + num::NonZeroU64, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, Weak, + }, + thread::panicking, +}; + +#[cfg(any(test, feature = "test-support"))] +use collections::HashMap; + +slotmap::new_key_type! { + /// A unique identifier for a model or view across the application. + pub struct EntityId; +} + +impl From for EntityId { + fn from(value: u64) -> Self { + Self(KeyData::from_ffi(value)) + } +} + +impl EntityId { + /// Converts this entity id to a [NonZeroU64] + pub fn as_non_zero_u64(self) -> NonZeroU64 { + NonZeroU64::new(self.0.as_ffi()).unwrap() + } + + /// Converts this entity id to a [u64] + pub fn as_u64(self) -> u64 { + self.0.as_ffi() + } +} + +impl Display for EntityId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_u64()) + } +} + +pub(crate) struct EntityMap { + entities: SecondaryMap>, + ref_counts: Arc>, +} + +struct EntityRefCounts { + counts: SlotMap, + dropped_entity_ids: Vec, + #[cfg(any(test, feature = "test-support"))] + leak_detector: LeakDetector, +} + +impl EntityMap { + pub fn new() -> Self { + Self { + entities: SecondaryMap::new(), + ref_counts: Arc::new(RwLock::new(EntityRefCounts { + counts: SlotMap::with_key(), + dropped_entity_ids: Vec::new(), + #[cfg(any(test, feature = "test-support"))] + leak_detector: LeakDetector { + next_handle_id: 0, + entity_handles: HashMap::default(), + }, + })), + } + } + + /// Reserve a slot for an entity, which you can subsequently use with `insert`. + pub fn reserve(&self) -> Slot { + let id = self.ref_counts.write().counts.insert(1.into()); + Slot(Model::new(id, Arc::downgrade(&self.ref_counts))) + } + + /// Insert an entity into a slot obtained by calling `reserve`. + pub fn insert(&mut self, slot: Slot, entity: T) -> Model + where + T: 'static, + { + let model = slot.0; + self.entities.insert(model.entity_id, Box::new(entity)); + model + } + + /// Move an entity to the stack. + #[track_caller] + pub fn lease<'a, T>(&mut self, model: &'a Model) -> Lease<'a, T> { + self.assert_valid_context(model); + let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| { + panic!( + "Circular entity lease of {}. Is it already being updated?", + std::any::type_name::() + ) + })); + Lease { + model, + entity, + entity_type: PhantomData, + } + } + + /// Returns an entity after moving it to the stack. + pub fn end_lease(&mut self, mut lease: Lease) { + self.entities + .insert(lease.model.entity_id, lease.entity.take().unwrap()); + } + + pub fn read(&self, model: &Model) -> &T { + self.assert_valid_context(model); + self.entities[model.entity_id].downcast_ref().unwrap() + } + + fn assert_valid_context(&self, model: &AnyModel) { + debug_assert!( + Weak::ptr_eq(&model.entity_map, &Arc::downgrade(&self.ref_counts)), + "used a model with the wrong context" + ); + } + + pub fn take_dropped(&mut self) -> Vec<(EntityId, Box)> { + let mut ref_counts = self.ref_counts.write(); + let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids); + + dropped_entity_ids + .into_iter() + .filter_map(|entity_id| { + let count = ref_counts.counts.remove(entity_id).unwrap(); + debug_assert_eq!( + count.load(SeqCst), + 0, + "dropped an entity that was referenced" + ); + // If the EntityId was allocated with `Context::reserve`, + // the entity may not have been inserted. + Some((entity_id, self.entities.remove(entity_id)?)) + }) + .collect() + } +} + +pub(crate) struct Lease<'a, T> { + entity: Option>, + pub model: &'a Model, + entity_type: PhantomData, +} + +impl<'a, T: 'static> core::ops::Deref for Lease<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.entity.as_ref().unwrap().downcast_ref().unwrap() + } +} + +impl<'a, T: 'static> core::ops::DerefMut for Lease<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.entity.as_mut().unwrap().downcast_mut().unwrap() + } +} + +impl<'a, T> Drop for Lease<'a, T> { + fn drop(&mut self) { + if self.entity.is_some() && !panicking() { + panic!("Leases must be ended with EntityMap::end_lease") + } + } +} + +#[derive(Deref, DerefMut)] +pub(crate) struct Slot(Model); + +/// A dynamically typed reference to a model, which can be downcast into a `Model`. +pub struct AnyModel { + pub(crate) entity_id: EntityId, + pub(crate) entity_type: TypeId, + entity_map: Weak>, + #[cfg(any(test, feature = "test-support"))] + handle_id: HandleId, +} + +impl AnyModel { + fn new(id: EntityId, entity_type: TypeId, entity_map: Weak>) -> Self { + Self { + entity_id: id, + entity_type, + entity_map: entity_map.clone(), + #[cfg(any(test, feature = "test-support"))] + handle_id: entity_map + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(id), + } + } + + /// Returns the id associated with this model. + pub fn entity_id(&self) -> EntityId { + self.entity_id + } + + /// Returns the [TypeId] associated with this model. + pub fn entity_type(&self) -> TypeId { + self.entity_type + } + + /// Converts this model handle into a weak variant, which does not prevent it from being released. + pub fn downgrade(&self) -> AnyWeakModel { + AnyWeakModel { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_ref_counts: self.entity_map.clone(), + } + } + + /// Converts this model handle into a strongly-typed model handle of the given type. + /// If this model handle is not of the specified type, returns itself as an error variant. + pub fn downcast(self) -> Result, AnyModel> { + if TypeId::of::() == self.entity_type { + Ok(Model { + any_model: self, + entity_type: PhantomData, + }) + } else { + Err(self) + } + } +} + +impl Clone for AnyModel { + fn clone(&self) -> Self { + if let Some(entity_map) = self.entity_map.upgrade() { + let entity_map = entity_map.read(); + let count = entity_map + .counts + .get(self.entity_id) + .expect("detected over-release of a model"); + let prev_count = count.fetch_add(1, SeqCst); + assert_ne!(prev_count, 0, "Detected over-release of a model."); + } + + Self { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_map: self.entity_map.clone(), + #[cfg(any(test, feature = "test-support"))] + handle_id: self + .entity_map + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id), + } + } +} + +impl Drop for AnyModel { + fn drop(&mut self) { + if let Some(entity_map) = self.entity_map.upgrade() { + let entity_map = entity_map.upgradable_read(); + let count = entity_map + .counts + .get(self.entity_id) + .expect("detected over-release of a handle."); + let prev_count = count.fetch_sub(1, SeqCst); + assert_ne!(prev_count, 0, "Detected over-release of a model."); + if prev_count == 1 { + // We were the last reference to this entity, so we can remove it. + let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map); + entity_map.dropped_entity_ids.push(self.entity_id); + } + } + + #[cfg(any(test, feature = "test-support"))] + if let Some(entity_map) = self.entity_map.upgrade() { + entity_map + .write() + .leak_detector + .handle_released(self.entity_id, self.handle_id) + } + } +} + +impl From> for AnyModel { + fn from(model: Model) -> Self { + model.any_model + } +} + +impl Hash for AnyModel { + fn hash(&self, state: &mut H) { + self.entity_id.hash(state); + } +} + +impl PartialEq for AnyModel { + fn eq(&self, other: &Self) -> bool { + self.entity_id == other.entity_id + } +} + +impl Eq for AnyModel {} + +impl std::fmt::Debug for AnyModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AnyModel") + .field("entity_id", &self.entity_id.as_u64()) + .finish() + } +} + +/// A strong, well typed reference to a struct which is managed +/// by GPUI +#[derive(Deref, DerefMut)] +pub struct Model { + #[deref] + #[deref_mut] + pub(crate) any_model: AnyModel, + pub(crate) entity_type: PhantomData, +} + +unsafe impl Send for Model {} +unsafe impl Sync for Model {} +impl Sealed for Model {} + +impl Entity for Model { + type Weak = WeakModel; + + fn entity_id(&self) -> EntityId { + self.any_model.entity_id + } + + fn downgrade(&self) -> Self::Weak { + WeakModel { + any_model: self.any_model.downgrade(), + entity_type: self.entity_type, + } + } + + fn upgrade_from(weak: &Self::Weak) -> Option + where + Self: Sized, + { + Some(Model { + any_model: weak.any_model.upgrade()?, + entity_type: weak.entity_type, + }) + } +} + +impl Model { + fn new(id: EntityId, entity_map: Weak>) -> Self + where + T: 'static, + { + Self { + any_model: AnyModel::new(id, TypeId::of::(), entity_map), + entity_type: PhantomData, + } + } + + /// Downgrade the this to a weak model reference + pub fn downgrade(&self) -> WeakModel { + // Delegate to the trait implementation to keep behavior in one place. + // This method was included to improve method resolution in the presence of + // the Model's deref + Entity::downgrade(self) + } + + /// Convert this into a dynamically typed model. + pub fn into_any(self) -> AnyModel { + self.any_model + } + + /// Grab a reference to this entity from the context. + pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T { + cx.entities.read(self) + } + + /// Read the entity referenced by this model with the given function. + pub fn read_with( + &self, + cx: &C, + f: impl FnOnce(&T, &AppContext) -> R, + ) -> C::Result { + cx.read_model(self, f) + } + + /// Updates the entity referenced by this model with the given function. + /// + /// The update function receives a context appropriate for its environment. + /// When updating in an `AppContext`, it receives a `ModelContext`. + /// When updating in a `WindowContext`, it receives a `ViewContext`. + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> C::Result + where + C: Context, + { + cx.update_model(self, update) + } +} + +impl Clone for Model { + fn clone(&self) -> Self { + Self { + any_model: self.any_model.clone(), + entity_type: self.entity_type, + } + } +} + +impl std::fmt::Debug for Model { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Model {{ entity_id: {:?}, entity_type: {:?} }}", + self.any_model.entity_id, + type_name::() + ) + } +} + +impl Hash for Model { + fn hash(&self, state: &mut H) { + self.any_model.hash(state); + } +} + +impl PartialEq for Model { + fn eq(&self, other: &Self) -> bool { + self.any_model == other.any_model + } +} + +impl Eq for Model {} + +impl PartialEq> for Model { + fn eq(&self, other: &WeakModel) -> bool { + self.any_model.entity_id() == other.entity_id() + } +} + +/// A type erased, weak reference to a model. +#[derive(Clone)] +pub struct AnyWeakModel { + pub(crate) entity_id: EntityId, + entity_type: TypeId, + entity_ref_counts: Weak>, +} + +impl AnyWeakModel { + /// Get the entity ID associated with this weak reference. + pub fn entity_id(&self) -> EntityId { + self.entity_id + } + + /// Check if this weak handle can be upgraded, or if the model has already been dropped + pub fn is_upgradable(&self) -> bool { + let ref_count = self + .entity_ref_counts + .upgrade() + .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) + .unwrap_or(0); + ref_count > 0 + } + + /// Upgrade this weak model reference to a strong reference. + pub fn upgrade(&self) -> Option { + let ref_counts = &self.entity_ref_counts.upgrade()?; + let ref_counts = ref_counts.read(); + let ref_count = ref_counts.counts.get(self.entity_id)?; + + // entity_id is in dropped_entity_ids + if ref_count.load(SeqCst) == 0 { + return None; + } + ref_count.fetch_add(1, SeqCst); + drop(ref_counts); + + Some(AnyModel { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_map: self.entity_ref_counts.clone(), + #[cfg(any(test, feature = "test-support"))] + handle_id: self + .entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id), + }) + } + + /// Assert that model referenced by this weak handle has been released. + #[cfg(any(test, feature = "test-support"))] + pub fn assert_released(&self) { + self.entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .assert_released(self.entity_id); + + if self + .entity_ref_counts + .upgrade() + .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) + .is_some() + { + panic!( + "entity was recently dropped but resources are retained until the end of the effect cycle." + ) + } + } +} + +impl From> for AnyWeakModel { + fn from(model: WeakModel) -> Self { + model.any_model + } +} + +impl Hash for AnyWeakModel { + fn hash(&self, state: &mut H) { + self.entity_id.hash(state); + } +} + +impl PartialEq for AnyWeakModel { + fn eq(&self, other: &Self) -> bool { + self.entity_id == other.entity_id + } +} + +impl Eq for AnyWeakModel {} + +/// A weak reference to a model of the given type. +#[derive(Deref, DerefMut)] +pub struct WeakModel { + #[deref] + #[deref_mut] + any_model: AnyWeakModel, + entity_type: PhantomData, +} + +unsafe impl Send for WeakModel {} +unsafe impl Sync for WeakModel {} + +impl Clone for WeakModel { + fn clone(&self) -> Self { + Self { + any_model: self.any_model.clone(), + entity_type: self.entity_type, + } + } +} + +impl WeakModel { + /// Upgrade this weak model reference into a strong model reference + pub fn upgrade(&self) -> Option> { + // Delegate to the trait implementation to keep behavior in one place. + Model::upgrade_from(self) + } + + /// Updates the entity referenced by this model with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Result + where + C: Context, + Result>: crate::Flatten, + { + crate::Flatten::flatten( + self.upgrade() + .ok_or_else(|| anyhow!("entity release")) + .map(|this| cx.update_model(&this, update)), + ) + } + + /// Reads the entity referenced by this model with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + pub fn read_with(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result + where + C: Context, + Result>: crate::Flatten, + { + crate::Flatten::flatten( + self.upgrade() + .ok_or_else(|| anyhow!("entity release")) + .map(|this| cx.read_model(&this, read)), + ) + } +} + +impl Hash for WeakModel { + fn hash(&self, state: &mut H) { + self.any_model.hash(state); + } +} + +impl PartialEq for WeakModel { + fn eq(&self, other: &Self) -> bool { + self.any_model == other.any_model + } +} + +impl Eq for WeakModel {} + +impl PartialEq> for WeakModel { + fn eq(&self, other: &Model) -> bool { + self.entity_id() == other.any_model.entity_id() + } +} + +#[cfg(any(test, feature = "test-support"))] +lazy_static::lazy_static! { + static ref LEAK_BACKTRACE: bool = + std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()); +} + +#[cfg(any(test, feature = "test-support"))] +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +pub(crate) struct HandleId { + id: u64, // id of the handle itself, not the pointed at object +} + +#[cfg(any(test, feature = "test-support"))] +pub(crate) struct LeakDetector { + next_handle_id: u64, + entity_handles: HashMap>>, +} + +#[cfg(any(test, feature = "test-support"))] +impl LeakDetector { + #[track_caller] + pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId { + let id = util::post_inc(&mut self.next_handle_id); + let handle_id = HandleId { id }; + let handles = self.entity_handles.entry(entity_id).or_default(); + handles.insert( + handle_id, + LEAK_BACKTRACE.then(|| backtrace::Backtrace::new_unresolved()), + ); + handle_id + } + + pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) { + let handles = self.entity_handles.entry(entity_id).or_default(); + handles.remove(&handle_id); + } + + pub fn assert_released(&mut self, entity_id: EntityId) { + let handles = self.entity_handles.entry(entity_id).or_default(); + if !handles.is_empty() { + for (_, backtrace) in handles { + if let Some(mut backtrace) = backtrace.take() { + backtrace.resolve(); + eprintln!("Leaked handle: {:#?}", backtrace); + } else { + eprintln!("Leaked handle: export LEAK_BACKTRACE to find allocation site"); + } + } + panic!(); + } + } +} + +#[cfg(test)] +mod test { + use crate::EntityMap; + + struct TestEntity { + pub i: i32, + } + + #[test] + fn test_entity_map_slot_assignment_before_cleanup() { + // Tests that slots are not re-used before take_dropped. + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 1 }); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 2 }); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 2); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1, 2], + ); + } + + #[test] + fn test_entity_map_weak_upgrade_before_cleanup() { + // Tests that weak handles are not upgraded before take_dropped + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + let handle = entity_map.insert(slot, TestEntity { i: 1 }); + let weak = handle.downgrade(); + drop(handle); + + let strong = weak.upgrade(); + assert_eq!(strong, None); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 1); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1], + ); + } +} diff --git a/crates/ming/src/app/model_context.rs b/crates/ming/src/app/model_context.rs new file mode 100644 index 0000000..3aebf88 --- /dev/null +++ b/crates/ming/src/app/model_context.rs @@ -0,0 +1,293 @@ +use crate::{ + AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, + EventEmitter, Model, Reservation, Subscription, Task, View, WeakModel, WindowContext, + WindowHandle, +}; +use anyhow::Result; +use derive_more::{Deref, DerefMut}; +use futures::FutureExt; +use std::{ + any::{Any, TypeId}, + borrow::{Borrow, BorrowMut}, + future::Future, +}; + +/// The app context, with specialized behavior for the given model. +#[derive(Deref, DerefMut)] +pub struct ModelContext<'a, T> { + #[deref] + #[deref_mut] + app: &'a mut AppContext, + model_state: WeakModel, +} + +impl<'a, T: 'static> ModelContext<'a, T> { + pub(crate) fn new(app: &'a mut AppContext, model_state: WeakModel) -> Self { + Self { app, model_state } + } + + /// The entity id of the model backing this context. + pub fn entity_id(&self) -> EntityId { + self.model_state.entity_id + } + + /// Returns a handle to the model belonging to this context. + pub fn handle(&self) -> Model { + self.weak_model() + .upgrade() + .expect("The entity must be alive if we have a model context") + } + + /// Returns a weak handle to the model belonging to this context. + pub fn weak_model(&self) -> WeakModel { + self.model_state.clone() + } + + /// Arranges for the given function to be called whenever [`ModelContext::notify`] or + /// [`ViewContext::notify`](crate::ViewContext::notify) is called with the given model or view. + pub fn observe( + &mut self, + entity: &E, + mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + 'static, + ) -> Subscription + where + T: 'static, + W: 'static, + E: Entity, + { + let this = self.weak_model(); + self.app.observe_internal(entity, move |e, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_notify(this, e, cx)); + true + } else { + false + } + }) + } + + /// Subscribe to an event type from another model or view + pub fn subscribe( + &mut self, + entity: &E, + mut on_event: impl FnMut(&mut T, E, &Evt, &mut ModelContext<'_, T>) + 'static, + ) -> Subscription + where + T: 'static, + T2: 'static + EventEmitter, + E: Entity, + Evt: 'static, + { + let this = self.weak_model(); + self.app.subscribe_internal(entity, move |e, event, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_event(this, e, event, cx)); + true + } else { + false + } + }) + } + + /// Register a callback to be invoked when GPUI releases this model. + pub fn on_release( + &mut self, + on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, + ) -> Subscription + where + T: 'static, + { + let (subscription, activate) = self.app.release_listeners.insert( + self.model_state.entity_id, + Box::new(move |this, cx| { + let this = this.downcast_mut().expect("invalid entity type"); + on_release(this, cx); + }), + ); + activate(); + subscription + } + + /// Register a callback to be run on the release of another model or view + pub fn observe_release( + &mut self, + entity: &E, + on_release: impl FnOnce(&mut T, &mut T2, &mut ModelContext<'_, T>) + 'static, + ) -> Subscription + where + T: Any, + T2: 'static, + E: Entity, + { + let entity_id = entity.entity_id(); + let this = self.weak_model(); + let (subscription, activate) = self.app.release_listeners.insert( + entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_release(this, entity, cx)); + } + }), + ); + activate(); + subscription + } + + /// Register a callback to for updates to the given global + pub fn observe_global( + &mut self, + mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static, + ) -> Subscription + where + T: 'static, + { + let handle = self.weak_model(); + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()), + ); + self.defer(move |_| activate()); + subscription + } + + /// Arrange for the given function to be invoked whenever the application is quit. + /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits. + pub fn on_app_quit( + &mut self, + mut on_quit: impl FnMut(&mut T, &mut ModelContext) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + T: 'static, + { + let handle = self.weak_model(); + let (subscription, activate) = self.app.quit_observers.insert( + (), + Box::new(move |cx| { + let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); + async move { + if let Some(future) = future { + future.await; + } + } + .boxed_local() + }), + ); + activate(); + subscription + } + + /// Tell GPUI that this model has changed and observers of it should be notified. + pub fn notify(&mut self) { + if self + .app + .pending_notifications + .insert(self.model_state.entity_id) + { + self.app.pending_effects.push_back(Effect::Notify { + emitter: self.model_state.entity_id, + }); + } + } + + /// Spawn the future returned by the given function. + /// The function is provided a weak handle to the model owned by this context and a context that can be held across await points. + /// The returned task must be held or detached. + pub fn spawn(&self, f: impl FnOnce(WeakModel, AsyncAppContext) -> Fut) -> Task + where + T: 'static, + Fut: Future + 'static, + R: 'static, + { + let this = self.weak_model(); + self.app.spawn(|cx| f(this, cx)) + } +} + +impl<'a, T> ModelContext<'a, T> { + /// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts. + pub fn emit(&mut self, event: Evt) + where + T: EventEmitter, + Evt: 'static, + { + self.app.pending_effects.push_back(Effect::Emit { + emitter: self.model_state.entity_id, + event_type: TypeId::of::(), + event: Box::new(event), + }); + } +} + +impl<'a, T> Context for ModelContext<'a, T> { + type Result = U; + + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, U>) -> U, + ) -> Model { + self.app.new_model(build_model) + } + + fn reserve_model(&mut self) -> Reservation { + self.app.reserve_model() + } + + fn insert_model( + &mut self, + reservation: Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, U>) -> U, + ) -> Self::Result> { + self.app.insert_model(reservation, build_model) + } + + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut U, &mut ModelContext<'_, U>) -> R, + ) -> R { + self.app.update_model(handle, update) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&U, &AppContext) -> R, + ) -> Self::Result + where + U: 'static, + { + self.app.read_model(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> R, + { + self.app.update_window(window, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + U: 'static, + { + self.app.read_window(window, read) + } +} + +impl Borrow for ModelContext<'_, T> { + fn borrow(&self) -> &AppContext { + self.app + } +} + +impl BorrowMut for ModelContext<'_, T> { + fn borrow_mut(&mut self) -> &mut AppContext { + self.app + } +} diff --git a/crates/ming/src/app/test_context.rs b/crates/ming/src/app/test_context.rs new file mode 100644 index 0000000..61f44f4 --- /dev/null +++ b/crates/ming/src/app/test_context.rs @@ -0,0 +1,974 @@ +use crate::{ + Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, AvailableSpace, + BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, DrawPhase, Drawable, + Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, + ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, + TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowBounds, + WindowContext, WindowHandle, WindowOptions, +}; +use anyhow::{anyhow, bail}; +use futures::{channel::oneshot, Stream, StreamExt}; +use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; + +/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides +/// an implementation of `Context` with additional methods that are useful in tests. +#[derive(Clone)] +pub struct TestAppContext { + #[doc(hidden)] + pub app: Rc, + #[doc(hidden)] + pub background_executor: BackgroundExecutor, + #[doc(hidden)] + pub foreground_executor: ForegroundExecutor, + #[doc(hidden)] + pub dispatcher: TestDispatcher, + test_platform: Rc, + text_system: Arc, + fn_name: Option<&'static str>, + on_quit: Rc>>>, +} + +impl Context for TestAppContext { + type Result = T; + + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + let mut app = self.app.borrow_mut(); + app.new_model(build_model) + } + + fn reserve_model(&mut self) -> Self::Result> { + let mut app = self.app.borrow_mut(); + app.reserve_model() + } + + fn insert_model( + &mut self, + reservation: crate::Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + let mut app = self.app.borrow_mut(); + app.insert_model(reservation, build_model) + } + + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Self::Result { + let mut app = self.app.borrow_mut(); + app.update_model(handle, update) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_model(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + let mut lock = self.app.borrow_mut(); + lock.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_window(window, read) + } +} + +impl TestAppContext { + /// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you. + pub fn new(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self { + let arc_dispatcher = Arc::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); + let asset_source = Arc::new(()); + let http_client = http::FakeHttpClient::with_404_response(); + let text_system = Arc::new(TextSystem::new(platform.text_system())); + + Self { + app: AppContext::new(platform.clone(), asset_source, http_client), + background_executor, + foreground_executor, + dispatcher: dispatcher.clone(), + test_platform: platform, + text_system, + fn_name, + on_quit: Rc::new(RefCell::new(Vec::default())), + } + } + + /// The name of the test function that created this `TestAppContext` + pub fn test_function_name(&self) -> Option<&'static str> { + self.fn_name + } + + /// Checks whether there have been any new path prompts received by the platform. + pub fn did_prompt_for_new_path(&self) -> bool { + self.test_platform.did_prompt_for_new_path() + } + + /// returns a new `TestAppContext` re-using the same executors to interleave tasks. + pub fn new_app(&self) -> TestAppContext { + Self::new(self.dispatcher.clone(), self.fn_name) + } + + /// Called by the test helper to end the test. + /// public so the macro can call it. + pub fn quit(&self) { + self.on_quit.borrow_mut().drain(..).for_each(|f| f()); + self.app.borrow_mut().shutdown(); + } + + /// Register cleanup to run when the test ends. + pub fn on_quit(&mut self, f: impl FnOnce() + 'static) { + self.on_quit.borrow_mut().push(Box::new(f)); + } + + /// Schedules all windows to be redrawn on the next effect cycle. + pub fn refresh(&mut self) -> Result<()> { + let mut app = self.app.borrow_mut(); + app.refresh(); + Ok(()) + } + + /// Returns an executor (for running tasks in the background) + pub fn executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + /// Returns an executor (for running tasks on the main thread) + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Gives you an `&mut AppContext` for the duration of the closure + pub fn update(&self, f: impl FnOnce(&mut AppContext) -> R) -> R { + let mut cx = self.app.borrow_mut(); + cx.update(f) + } + + /// Gives you an `&AppContext` for the duration of the closure + pub fn read(&self, f: impl FnOnce(&AppContext) -> R) -> R { + let cx = self.app.borrow(); + f(&cx) + } + + /// Adds a new window. The Window will always be backed by a `TestWindow` which + /// can be retrieved with `self.test_window(handle)` + pub fn add_window(&mut self, build_window: F) -> WindowHandle + where + F: FnOnce(&mut ViewContext) -> V, + V: 'static + Render, + { + let mut cx = self.app.borrow_mut(); + + // Some tests rely on the window size matching the bounds of the test display + let bounds = Bounds::maximized(None, &mut cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |cx| cx.new_view(build_window), + ) + } + + /// Adds a new window with no content. + pub fn add_empty_window(&mut self) -> &mut VisualTestContext { + let mut cx = self.app.borrow_mut(); + let bounds = Bounds::maximized(None, &mut cx); + let window = cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |cx| cx.new_view(|_| Empty), + ); + drop(cx); + let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + cx.run_until_parked(); + cx + } + + /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used + /// as a `WindowContext` for the rest of the test. Typically you would shadow this context with + /// the returned one. `let (view, cx) = cx.add_window_view(...);` + pub fn add_window_view(&mut self, build_root_view: F) -> (View, &mut VisualTestContext) + where + F: FnOnce(&mut ViewContext) -> V, + V: 'static + Render, + { + let mut cx = self.app.borrow_mut(); + let bounds = Bounds::maximized(None, &mut cx); + let window = cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |cx| cx.new_view(build_root_view), + ); + drop(cx); + let view = window.root_view(self).unwrap(); + let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + cx.run_until_parked(); + + // it might be nice to try and cleanup these at the end of each test. + (view, cx) + } + + /// returns the TextSystem + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Simulates writing to the platform clipboard + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.test_platform.write_to_clipboard(item) + } + + /// Simulates reading from the platform clipboard. + /// This will return the most recent value from `write_to_clipboard`. + pub fn read_from_clipboard(&self) -> Option { + self.test_platform.read_from_clipboard() + } + + /// Simulates choosing a File in the platform's "Open" dialog. + pub fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + self.test_platform.simulate_new_path_selection(select_path); + } + + /// Simulates clicking a button in an platform-level alert dialog. + pub fn simulate_prompt_answer(&self, button_ix: usize) { + self.test_platform.simulate_prompt_answer(button_ix); + } + + /// Returns true if there's an alert dialog open. + pub fn has_pending_prompt(&self) -> bool { + self.test_platform.has_pending_prompt() + } + + /// All the urls that have been opened with cx.open_url() during this test. + pub fn opened_url(&self) -> Option { + self.test_platform.opened_url.borrow().clone() + } + + /// Simulates the user resizing the window to the new size. + pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size) { + self.test_window(window_handle).simulate_resize(size); + } + + /// Returns all windows open in the test. + pub fn windows(&self) -> Vec { + self.app.borrow().windows().clone() + } + + /// Run the given task on the main thread. + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.to_async())) + } + + /// true if the given global is defined + pub fn has_global(&self) -> bool { + let app = self.app.borrow(); + app.has_global::() + } + + /// runs the given closure with a reference to the global + /// panics if `has_global` would return false. + pub fn read_global(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R { + let app = self.app.borrow(); + read(app.global(), &app) + } + + /// runs the given closure with a reference to the global (if set) + pub fn try_read_global( + &self, + read: impl FnOnce(&G, &AppContext) -> R, + ) -> Option { + let lock = self.app.borrow(); + Some(read(lock.try_global()?, &lock)) + } + + /// sets the global in this context. + pub fn set_global(&mut self, global: G) { + let mut lock = self.app.borrow_mut(); + lock.update(|cx| cx.set_global(global)) + } + + /// updates the global in this context. (panics if `has_global` would return false) + pub fn update_global( + &mut self, + update: impl FnOnce(&mut G, &mut AppContext) -> R, + ) -> R { + let mut lock = self.app.borrow_mut(); + lock.update(|cx| cx.update_global(update)) + } + + /// Returns an `AsyncAppContext` which can be used to run tasks that expect to be on a background + /// thread on the current thread in tests. + pub fn to_async(&self) -> AsyncAppContext { + AsyncAppContext { + app: Rc::downgrade(&self.app), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), + } + } + + /// Wait until there are no more pending tasks. + pub fn run_until_parked(&mut self) { + self.background_executor.run_until_parked() + } + + /// Simulate dispatching an action to the currently focused node in the window. + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) + where + A: Action, + { + window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .unwrap(); + + self.background_executor.run_until_parked() + } + + /// simulate_keystrokes takes a space-separated list of keys to type. + /// cx.simulate_keystrokes("cmd-shift-p b k s p enter") + /// in Zed, this will run backspace on the current editor through the command palette. + /// This will also run the background executor until it's parked. + pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { + for keystroke in keystrokes + .split(' ') + .map(Keystroke::parse) + .map(Result::unwrap) + { + self.dispatch_keystroke(window, keystroke); + } + + self.background_executor.run_until_parked() + } + + /// simulate_input takes a string of text to type. + /// cx.simulate_input("abc") + /// will type abc into your current editor + /// This will also run the background executor until it's parked. + pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { + for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { + self.dispatch_keystroke(window, keystroke); + } + + self.background_executor.run_until_parked() + } + + /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`) + pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { + self.update_window(window, |_, cx| cx.dispatch_keystroke(keystroke)) + .unwrap(); + } + + /// Returns the `TestWindow` backing the given handle. + pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow { + self.app + .borrow_mut() + .windows + .get_mut(window.id) + .unwrap() + .as_mut() + .unwrap() + .platform_window + .as_test() + .unwrap() + .clone() + } + + /// Returns a stream of notifications whenever the View or Model is updated. + pub fn notifications(&mut self, entity: &impl Entity) -> impl Stream { + let (tx, rx) = futures::channel::mpsc::unbounded(); + self.update(|cx| { + cx.observe(entity, { + let tx = tx.clone(); + move |_, _| { + let _ = tx.unbounded_send(()); + } + }) + .detach(); + cx.observe_release(entity, move |_, _| tx.close_channel()) + .detach() + }); + rx + } + + /// Retuens a stream of events emitted by the given Model. + pub fn events>( + &mut self, + entity: &Model, + ) -> futures::channel::mpsc::UnboundedReceiver + where + Evt: 'static + Clone, + { + let (tx, rx) = futures::channel::mpsc::unbounded(); + entity + .update(self, |_, cx: &mut ModelContext| { + cx.subscribe(entity, move |_model, _handle, event, _cx| { + let _ = tx.unbounded_send(event.clone()); + }) + }) + .detach(); + rx + } + + /// Runs until the given condition becomes true. (Prefer `run_until_parked` if you + /// don't need to jump in at a specific time). + pub async fn condition( + &mut self, + model: &Model, + mut predicate: impl FnMut(&mut T, &mut ModelContext) -> bool, + ) { + let timer = self.executor().timer(Duration::from_secs(3)); + let mut notifications = self.notifications(model); + + use futures::FutureExt as _; + use smol::future::FutureExt as _; + + async { + loop { + if model.update(self, &mut predicate) { + return Ok(()); + } + + if notifications.next().await.is_none() { + bail!("model dropped") + } + } + } + .race(timer.map(|_| Err(anyhow!("condition timed out")))) + .await + .unwrap(); + } +} + +impl Model { + /// Block until the next event is emitted by the model, then return it. + pub fn next_event(&self, cx: &mut TestAppContext) -> impl Future + where + Event: Send + Clone + 'static, + T: EventEmitter, + { + let (tx, mut rx) = oneshot::channel(); + let mut tx = Some(tx); + let subscription = self.update(cx, |_, cx| { + cx.subscribe(self, move |_, _, event, _| { + if let Some(tx) = tx.take() { + _ = tx.send(event.clone()); + } + }) + }); + + async move { + let event = rx.await.expect("no event emitted"); + drop(subscription); + event + } + } + + /// Returns a future that resolves when the model notifies. + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + use postage::prelude::{Sink as _, Stream as _}; + + let (mut tx, mut rx) = postage::mpsc::channel(1); + let mut cx = cx.app.app.borrow_mut(); + let subscription = cx.observe(self, move |_, _| { + tx.try_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let notification = crate::util::timeout(duration, rx.recv()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("model dropped while test was waiting for its next notification") + } + } +} + +impl View { + /// Returns a future that resolves when the view is next updated. + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + use postage::prelude::{Sink as _, Stream as _}; + + let (mut tx, mut rx) = postage::mpsc::channel(1); + let mut cx = cx.app.app.borrow_mut(); + let subscription = cx.observe(self, move |_, _| { + tx.try_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let notification = crate::util::timeout(duration, rx.recv()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("model dropped while test was waiting for its next notification") + } + } +} + +impl View { + /// Returns a future that resolves when the condition becomes true. + pub fn condition( + &self, + cx: &TestAppContext, + mut predicate: impl FnMut(&V, &AppContext) -> bool, + ) -> impl Future + where + Evt: 'static, + V: EventEmitter, + { + use postage::prelude::{Sink as _, Stream as _}; + + let (tx, mut rx) = postage::mpsc::channel(1024); + let timeout_duration = Duration::from_millis(100); + + let mut cx = cx.app.borrow_mut(); + let subscriptions = ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _: &Evt, _| { + tx.blocking_send(()).ok(); + } + }), + ); + + let cx = cx.this.upgrade().unwrap(); + let handle = self.downgrade(); + + async move { + crate::util::timeout(timeout_duration, async move { + loop { + { + let cx = cx.borrow(); + let cx = &*cx; + if predicate( + handle + .upgrade() + .expect("view dropped with pending condition") + .read(cx), + cx, + ) { + break; + } + } + + cx.borrow().background_executor().start_waiting(); + rx.recv() + .await + .expect("view dropped with pending condition"); + cx.borrow().background_executor().finish_waiting(); + } + }) + .await + .expect("condition timed out"); + drop(subscriptions); + } + } +} + +use derive_more::{Deref, DerefMut}; +#[derive(Deref, DerefMut, Clone)] +/// A VisualTestContext is the test-equivalent of a `WindowContext`. It allows you to +/// run window-specific test code. +pub struct VisualTestContext { + #[deref] + #[deref_mut] + /// cx is the original TestAppContext (you can more easily access this using Deref) + pub cx: TestAppContext, + window: AnyWindowHandle, +} + +impl VisualTestContext { + /// Get the underlying window handle underlying this context. + pub fn handle(&self) -> AnyWindowHandle { + self.window + } + + /// Provides the `WindowContext` for the duration of the closure. + pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { + self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() + } + + /// Creates a new VisualTestContext. You would typically shadow the passed in + /// TestAppContext with this, as this is typically more useful. + /// `let cx = VisualTestContext::from_window(window, cx);` + pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { + Self { + cx: cx.clone(), + window, + } + } + + /// Wait until there are no more pending tasks. + pub fn run_until_parked(&self) { + self.cx.background_executor.run_until_parked(); + } + + /// Dispatch the action to the currently focused node. + pub fn dispatch_action(&mut self, action: A) + where + A: Action, + { + self.cx.dispatch_action(self.window, action) + } + + /// Read the title off the window (set by `WindowContext#set_window_title`) + pub fn window_title(&mut self) -> Option { + self.cx.test_window(self.window).0.lock().title.clone() + } + + /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")` + /// Automatically runs until parked. + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { + self.cx.simulate_keystrokes(self.window, keystrokes) + } + + /// Simulate typing text `cx.simulate_input("hello")` + /// Automatically runs until parked. + pub fn simulate_input(&mut self, input: &str) { + self.cx.simulate_input(self.window, input) + } + + /// Simulate a mouse move event to the given point + pub fn simulate_mouse_move( + &mut self, + position: Point, + button: impl Into>, + modifiers: Modifiers, + ) { + self.simulate_event(MouseMoveEvent { + position, + modifiers, + pressed_button: button.into(), + }) + } + + /// Simulate a mouse down event to the given point + pub fn simulate_mouse_down( + &mut self, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event(MouseDownEvent { + position, + modifiers, + button, + click_count: 1, + first_mouse: false, + }) + } + + /// Simulate a mouse up event to the given point + pub fn simulate_mouse_up( + &mut self, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event(MouseUpEvent { + position, + modifiers, + button, + click_count: 1, + }) + } + + /// Simulate a primary mouse click at the given point + pub fn simulate_click(&mut self, position: Point, modifiers: Modifiers) { + self.simulate_event(MouseDownEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 1, + first_mouse: false, + }); + self.simulate_event(MouseUpEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 1, + }); + } + + /// Simulate a modifiers changed event + pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) { + self.simulate_event(ModifiersChangedEvent { modifiers }) + } + + /// Simulates the user resizing the window to the new size. + pub fn simulate_resize(&self, size: Size) { + self.simulate_window_resize(self.window, size) + } + + /// debug_bounds returns the bounds of the element with the given selector. + pub fn debug_bounds(&mut self, selector: &'static str) -> Option> { + self.update(|cx| cx.window.rendered_frame.debug_bounds.get(selector).copied()) + } + + /// Draw an element to the window. Useful for simulating events or actions + pub fn draw( + &mut self, + origin: Point, + space: impl Into>, + f: impl FnOnce(&mut WindowContext) -> E, + ) -> (E::RequestLayoutState, E::PrepaintState) + where + E: Element, + { + self.update(|cx| { + cx.window.draw_phase = DrawPhase::Prepaint; + let mut element = Drawable::new(f(cx)); + element.layout_as_root(space.into(), cx); + cx.with_absolute_element_offset(origin, |cx| element.prepaint(cx)); + + cx.window.draw_phase = DrawPhase::Paint; + let (request_layout_state, prepaint_state) = element.paint(cx); + + cx.window.draw_phase = DrawPhase::None; + cx.refresh(); + + (request_layout_state, prepaint_state) + }) + } + + /// Simulate an event from the platform, e.g. a SrollWheelEvent + /// Make sure you've called [VisualTestContext::draw] first! + pub fn simulate_event(&mut self, event: E) { + self.test_window(self.window) + .simulate_input(event.to_platform_input()); + self.background_executor.run_until_parked(); + } + + /// Simulates the user blurring the window. + pub fn deactivate_window(&mut self) { + if Some(self.window) == self.test_platform.active_window() { + self.test_platform.set_active_window(None) + } + self.background_executor.run_until_parked(); + } + + /// Simulates the user closing the window. + /// Returns true if the window was closed. + pub fn simulate_close(&mut self) -> bool { + let handler = self + .cx + .update_window(self.window, |_, cx| { + cx.window + .platform_window + .as_test() + .unwrap() + .0 + .lock() + .should_close_handler + .take() + }) + .unwrap(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.cx + .update_window(self.window, |_, cx| { + cx.window.platform_window.on_should_close(handler); + }) + .unwrap(); + should_close + } else { + false + } + } + + /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods). + /// This method internally retains the VisualTestContext until the end of the test. + pub fn as_mut(self) -> &'static mut Self { + let ptr = Box::into_raw(Box::new(self)); + // safety: on_quit will be called after the test has finished. + // the executor will ensure that all tasks related to the test have stopped. + // so there is no way for cx to be accessed after on_quit is called. + let cx = Box::leak(unsafe { Box::from_raw(ptr) }); + cx.on_quit(move || unsafe { + drop(Box::from_raw(ptr)); + }); + cx + } +} + +impl Context for VisualTestContext { + type Result = ::Result; + + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.cx.new_model(build_model) + } + + fn reserve_model(&mut self) -> Self::Result> { + self.cx.reserve_model() + } + + fn insert_model( + &mut self, + reservation: crate::Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.cx.insert_model(reservation, build_model) + } + + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.update_model(handle, update) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.read_model(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.cx.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.cx.read_window(window, read) + } +} + +impl VisualContext for VisualTestContext { + fn new_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(&mut self.cx, |_, cx| cx.new_view(build_view)) + .unwrap() + } + + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Self::Result { + self.window + .update(&mut self.cx, |_, cx| cx.update_view(view, update)) + .unwrap() + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(&mut self.cx, |_, cx| cx.replace_root_view(build_view)) + .unwrap() + } + + fn focus_view(&mut self, view: &View) -> Self::Result<()> { + self.window + .update(&mut self.cx, |_, cx| { + view.read(cx).focus_handle(cx).clone().focus(cx) + }) + .unwrap() + } + + fn dismiss_view(&mut self, view: &View) -> Self::Result<()> + where + V: crate::ManagedView, + { + self.window + .update(&mut self.cx, |_, cx| { + view.update(cx, |_, cx| cx.emit(crate::DismissEvent)) + }) + .unwrap() + } +} + +impl AnyWindowHandle { + /// Creates the given view in this window. + pub fn build_view( + &self, + cx: &mut TestAppContext, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> View { + self.update(cx, |_, cx| cx.new_view(build_view)).unwrap() + } +} diff --git a/crates/ming/src/arena.rs b/crates/ming/src/arena.rs new file mode 100644 index 0000000..4ddeaaf --- /dev/null +++ b/crates/ming/src/arena.rs @@ -0,0 +1,250 @@ +use std::{ + alloc, + cell::Cell, + ops::{Deref, DerefMut}, + ptr, + rc::Rc, +}; + +struct ArenaElement { + value: *mut u8, + drop: unsafe fn(*mut u8), +} + +impl Drop for ArenaElement { + #[inline(always)] + fn drop(&mut self) { + unsafe { + (self.drop)(self.value); + } + } +} + +pub struct Arena { + start: *mut u8, + end: *mut u8, + offset: *mut u8, + elements: Vec, + valid: Rc>, +} + +impl Arena { + pub fn new(size_in_bytes: usize) -> Self { + unsafe { + let layout = alloc::Layout::from_size_align(size_in_bytes, 1).unwrap(); + let start = alloc::alloc(layout); + let end = start.add(size_in_bytes); + Self { + start, + end, + offset: start, + elements: Vec::new(), + valid: Rc::new(Cell::new(true)), + } + } + } + + pub fn len(&self) -> usize { + self.offset as usize - self.start as usize + } + + pub fn capacity(&self) -> usize { + self.end as usize - self.start as usize + } + + pub fn clear(&mut self) { + self.valid.set(false); + self.valid = Rc::new(Cell::new(true)); + self.elements.clear(); + self.offset = self.start; + } + + #[inline(always)] + pub fn alloc(&mut self, f: impl FnOnce() -> T) -> ArenaBox { + #[inline(always)] + unsafe fn inner_writer(ptr: *mut T, f: F) + where + F: FnOnce() -> T, + { + ptr::write(ptr, f()); + } + + unsafe fn drop(ptr: *mut u8) { + std::ptr::drop_in_place(ptr.cast::()); + } + + unsafe { + let layout = alloc::Layout::new::(); + let offset = self.offset.add(self.offset.align_offset(layout.align())); + let next_offset = offset.add(layout.size()); + assert!(next_offset <= self.end, "not enough space in Arena"); + + let result = ArenaBox { + ptr: offset.cast(), + valid: self.valid.clone(), + }; + + inner_writer(result.ptr, f); + self.elements.push(ArenaElement { + value: offset, + drop: drop::, + }); + self.offset = next_offset; + + result + } + } +} + +impl Drop for Arena { + fn drop(&mut self) { + self.clear(); + } +} + +pub struct ArenaBox { + ptr: *mut T, + valid: Rc>, +} + +impl ArenaBox { + #[inline(always)] + pub fn map(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox { + ArenaBox { + ptr: f(&mut self), + valid: self.valid, + } + } + + fn validate(&self) { + assert!( + self.valid.get(), + "attempted to dereference an ArenaRef after its Arena was cleared" + ); + } +} + +impl Deref for ArenaBox { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.validate(); + unsafe { &*self.ptr } + } +} + +impl DerefMut for ArenaBox { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + self.validate(); + unsafe { &mut *self.ptr } + } +} + +pub struct ArenaRef(ArenaBox); + +impl From> for ArenaRef { + fn from(value: ArenaBox) -> Self { + ArenaRef(value) + } +} + +impl Clone for ArenaRef { + fn clone(&self) -> Self { + Self(ArenaBox { + ptr: self.0.ptr, + valid: self.0.valid.clone(), + }) + } +} + +impl Deref for ArenaRef { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +#[cfg(test)] +mod tests { + use std::{cell::Cell, rc::Rc}; + + use super::*; + + #[test] + fn test_arena() { + let mut arena = Arena::new(1024); + let a = arena.alloc(|| 1u64); + let b = arena.alloc(|| 2u32); + let c = arena.alloc(|| 3u16); + let d = arena.alloc(|| 4u8); + assert_eq!(*a, 1); + assert_eq!(*b, 2); + assert_eq!(*c, 3); + assert_eq!(*d, 4); + + arena.clear(); + let a = arena.alloc(|| 5u64); + let b = arena.alloc(|| 6u32); + let c = arena.alloc(|| 7u16); + let d = arena.alloc(|| 8u8); + assert_eq!(*a, 5); + assert_eq!(*b, 6); + assert_eq!(*c, 7); + assert_eq!(*d, 8); + + // Ensure drop gets called. + let dropped = Rc::new(Cell::new(false)); + struct DropGuard(Rc>); + impl Drop for DropGuard { + fn drop(&mut self) { + self.0.set(true); + } + } + arena.alloc(|| DropGuard(dropped.clone())); + arena.clear(); + assert!(dropped.get()); + } + + #[test] + #[should_panic(expected = "not enough space in Arena")] + fn test_arena_overflow() { + let mut arena = Arena::new(16); + arena.alloc(|| 1u64); + arena.alloc(|| 2u64); + // This should panic. + arena.alloc(|| 3u64); + } + + #[test] + fn test_arena_alignment() { + let mut arena = Arena::new(256); + let x1 = arena.alloc(|| 1u8); + let x2 = arena.alloc(|| 2u16); + let x3 = arena.alloc(|| 3u32); + let x4 = arena.alloc(|| 4u64); + let x5 = arena.alloc(|| 5u64); + + assert_eq!(*x1, 1); + assert_eq!(*x2, 2); + assert_eq!(*x3, 3); + assert_eq!(*x4, 4); + assert_eq!(*x5, 5); + + assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0); + assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0); + } + + #[test] + #[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")] + fn test_arena_use_after_clear() { + let mut arena = Arena::new(16); + let value = arena.alloc(|| 1u64); + + arena.clear(); + let _read_value = *value; + } +} diff --git a/crates/ming/src/asset_cache.rs b/crates/ming/src/asset_cache.rs new file mode 100644 index 0000000..070aff3 --- /dev/null +++ b/crates/ming/src/asset_cache.rs @@ -0,0 +1,87 @@ +use crate::{SharedUri, WindowContext}; +use collections::FxHashMap; +use futures::Future; +use parking_lot::Mutex; +use std::any::TypeId; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; +use std::{any::Any, path::PathBuf}; + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) enum UriOrPath { + Uri(SharedUri), + Path(Arc), +} + +impl From for UriOrPath { + fn from(value: SharedUri) -> Self { + Self::Uri(value) + } +} + +impl From> for UriOrPath { + fn from(value: Arc) -> Self { + Self::Path(value) + } +} + +/// A trait for asynchronous asset loading. +pub trait Asset { + /// The source of the asset. + type Source: Clone + Hash + Send; + + /// The loaded asset + type Output: Clone + Send; + + /// Load the asset asynchronously + fn load( + source: Self::Source, + cx: &mut WindowContext, + ) -> impl Future + Send + 'static; +} + +/// Use a quick, non-cryptographically secure hash function to get an identifier from data +pub fn hash(data: &T) -> u64 { + let mut hasher = collections::FxHasher::default(); + data.hash(&mut hasher); + hasher.finish() +} + +/// A cache for assets. +#[derive(Clone)] +pub struct AssetCache { + assets: Arc>>>, +} + +impl AssetCache { + pub(crate) fn new() -> Self { + Self { + assets: Default::default(), + } + } + + /// Get the asset from the cache, if it exists. + pub fn get(&self, source: &A::Source) -> Option { + self.assets + .lock() + .get(&(TypeId::of::(), hash(&source))) + .and_then(|task| task.downcast_ref::()) + .cloned() + } + + /// Insert the asset into the cache. + pub fn insert(&mut self, source: A::Source, output: A::Output) { + self.assets + .lock() + .insert((TypeId::of::(), hash(&source)), Box::new(output)); + } + + /// Remove an entry from the asset cache + pub fn remove(&mut self, source: &A::Source) -> Option { + self.assets + .lock() + .remove(&(TypeId::of::(), hash(&source))) + .and_then(|any| any.downcast::().ok()) + .map(|boxed| *boxed) + } +} diff --git a/crates/ming/src/assets.rs b/crates/ming/src/assets.rs new file mode 100644 index 0000000..dd7485a --- /dev/null +++ b/crates/ming/src/assets.rs @@ -0,0 +1,79 @@ +use crate::{size, DevicePixels, Result, SharedString, Size}; +use anyhow::anyhow; +use image::{Bgra, ImageBuffer}; +use std::{ + borrow::Cow, + fmt, + hash::Hash, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, +}; + +/// A source of assets for this app to use. +pub trait AssetSource: 'static + Send + Sync { + /// Load the given asset from the source path. + fn load(&self, path: &str) -> Result>; + + /// List the assets at the given path. + fn list(&self, path: &str) -> Result>; +} + +impl AssetSource for () { + fn load(&self, path: &str) -> Result> { + Err(anyhow!( + "load called on empty asset provider with \"{}\"", + path + )) + } + + fn list(&self, _path: &str) -> Result> { + Ok(vec![]) + } +} + +/// A unique identifier for the image cache +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ImageId(usize); + +#[derive(PartialEq, Eq, Hash, Clone)] +pub(crate) struct RenderImageParams { + pub(crate) image_id: ImageId, +} + +/// A cached and processed image. +pub struct ImageData { + /// The ID associated with this image + pub id: ImageId, + data: ImageBuffer, Vec>, +} + +impl ImageData { + /// Create a new image from the given data. + pub fn new(data: ImageBuffer, Vec>) -> Self { + static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + + Self { + id: ImageId(NEXT_ID.fetch_add(1, SeqCst)), + data, + } + } + + /// Convert this image into a byte slice. + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + /// Get the size of this image, in pixels + pub fn size(&self) -> Size { + let (width, height) = self.data.dimensions(); + size(width.into(), height.into()) + } +} + +impl fmt::Debug for ImageData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageData") + .field("id", &self.id) + .field("size", &self.data.dimensions()) + .finish() + } +} diff --git a/crates/ming/src/bounds_tree.rs b/crates/ming/src/bounds_tree.rs new file mode 100644 index 0000000..789dc6e --- /dev/null +++ b/crates/ming/src/bounds_tree.rs @@ -0,0 +1,292 @@ +use crate::{Bounds, Half}; +use std::{ + cmp, + fmt::Debug, + ops::{Add, Sub}, +}; + +#[derive(Debug)] +pub(crate) struct BoundsTree +where + U: Default + Clone + Debug, +{ + root: Option, + nodes: Vec>, + stack: Vec, +} + +impl BoundsTree +where + U: Clone + Debug + PartialOrd + Add + Sub + Half + Default, +{ + pub fn clear(&mut self) { + self.root = None; + self.nodes.clear(); + self.stack.clear(); + } + + pub fn insert(&mut self, new_bounds: Bounds) -> u32 { + // If the tree is empty, make the root the new leaf. + if self.root.is_none() { + let new_node = self.push_leaf(new_bounds, 1); + self.root = Some(new_node); + return 1; + } + + // Search for the best place to add the new leaf based on heuristics. + let mut max_intersecting_ordering = 0; + let mut index = self.root.unwrap(); + while let Node::Internal { + left, + right, + bounds: node_bounds, + .. + } = &mut self.nodes[index] + { + let left = *left; + let right = *right; + *node_bounds = node_bounds.union(&new_bounds); + self.stack.push(index); + + // Descend to the best-fit child, based on which one would increase + // the surface area the least. This attempts to keep the tree balanced + // in terms of surface area. If there is an intersection with the other child, + // add its keys to the intersections vector. + let left_cost = new_bounds + .union(&self.nodes[left].bounds()) + .half_perimeter(); + let right_cost = new_bounds + .union(&self.nodes[right].bounds()) + .half_perimeter(); + if left_cost < right_cost { + max_intersecting_ordering = + self.find_max_ordering(right, &new_bounds, max_intersecting_ordering); + index = left; + } else { + max_intersecting_ordering = + self.find_max_ordering(left, &new_bounds, max_intersecting_ordering); + index = right; + } + } + + // We've found a leaf ('index' now refers to a leaf node). + // We'll insert a new parent node above the leaf and attach our new leaf to it. + let sibling = index; + + // Check for collision with the located leaf node + let Node::Leaf { + bounds: sibling_bounds, + order: sibling_ordering, + .. + } = &self.nodes[index] + else { + unreachable!(); + }; + if sibling_bounds.intersects(&new_bounds) { + max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering); + } + + let ordering = max_intersecting_ordering + 1; + let new_node = self.push_leaf(new_bounds, ordering); + let new_parent = self.push_internal(sibling, new_node); + + // If there was an old parent, we need to update its children indices. + if let Some(old_parent) = self.stack.last().copied() { + let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else { + unreachable!(); + }; + + if *left == sibling { + *left = new_parent; + } else { + *right = new_parent; + } + } else { + // If the old parent was the root, the new parent is the new root. + self.root = Some(new_parent); + } + + for node_index in self.stack.drain(..) { + let Node::Internal { + max_order: max_ordering, + .. + } = &mut self.nodes[node_index] + else { + unreachable!() + }; + *max_ordering = cmp::max(*max_ordering, ordering); + } + + ordering + } + + fn find_max_ordering(&self, index: usize, bounds: &Bounds, mut max_ordering: u32) -> u32 { + match &self.nodes[index] { + Node::Leaf { + bounds: node_bounds, + order: ordering, + .. + } => { + if bounds.intersects(node_bounds) { + max_ordering = cmp::max(*ordering, max_ordering); + } + } + Node::Internal { + left, + right, + bounds: node_bounds, + max_order: node_max_ordering, + .. + } => { + if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering { + let left_max_ordering = self.nodes[*left].max_ordering(); + let right_max_ordering = self.nodes[*right].max_ordering(); + if left_max_ordering > right_max_ordering { + max_ordering = self.find_max_ordering(*left, bounds, max_ordering); + max_ordering = self.find_max_ordering(*right, bounds, max_ordering); + } else { + max_ordering = self.find_max_ordering(*right, bounds, max_ordering); + max_ordering = self.find_max_ordering(*left, bounds, max_ordering); + } + } + } + } + max_ordering + } + + fn push_leaf(&mut self, bounds: Bounds, order: u32) -> usize { + self.nodes.push(Node::Leaf { bounds, order }); + self.nodes.len() - 1 + } + + fn push_internal(&mut self, left: usize, right: usize) -> usize { + let left_node = &self.nodes[left]; + let right_node = &self.nodes[right]; + let new_bounds = left_node.bounds().union(right_node.bounds()); + let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering()); + self.nodes.push(Node::Internal { + bounds: new_bounds, + left, + right, + max_order: max_ordering, + }); + self.nodes.len() - 1 + } +} + +impl Default for BoundsTree +where + U: Default + Clone + Debug, +{ + fn default() -> Self { + BoundsTree { + root: None, + nodes: Vec::new(), + stack: Vec::new(), + } + } +} + +#[derive(Debug, Clone)] +enum Node +where + U: Clone + Default + Debug, +{ + Leaf { + bounds: Bounds, + order: u32, + }, + Internal { + left: usize, + right: usize, + bounds: Bounds, + max_order: u32, + }, +} + +impl Node +where + U: Clone + Default + Debug, +{ + fn bounds(&self) -> &Bounds { + match self { + Node::Leaf { bounds, .. } => bounds, + Node::Internal { bounds, .. } => bounds, + } + } + + fn max_ordering(&self) -> u32 { + match self { + Node::Leaf { + order: ordering, .. + } => *ordering, + Node::Internal { + max_order: max_ordering, + .. + } => *max_ordering, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Bounds, Point, Size}; + + #[test] + fn test_insert() { + let mut tree = BoundsTree::::default(); + let bounds1 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds2 = Bounds { + origin: Point { x: 5.0, y: 5.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds3 = Bounds { + origin: Point { x: 10.0, y: 10.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + + // Insert the bounds into the tree and verify the order is correct + assert_eq!(tree.insert(bounds1), 1); + assert_eq!(tree.insert(bounds2), 2); + assert_eq!(tree.insert(bounds3), 3); + + // Insert non-overlapping bounds and verify they can reuse orders + let bounds4 = Bounds { + origin: Point { x: 20.0, y: 20.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds5 = Bounds { + origin: Point { x: 40.0, y: 40.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds6 = Bounds { + origin: Point { x: 25.0, y: 25.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3 + assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds + assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order + } +} diff --git a/crates/ming/src/color.rs b/crates/ming/src/color.rs new file mode 100644 index 0000000..7246af4 --- /dev/null +++ b/crates/ming/src/color.rs @@ -0,0 +1,492 @@ +use anyhow::bail; +use serde::de::{self, Deserialize, Deserializer, Visitor}; +use std::fmt; + +/// Convert an RGB hex color code number to a color type +pub fn rgb(hex: u32) -> Rgba { + let r = ((hex >> 16) & 0xFF) as f32 / 255.0; + let g = ((hex >> 8) & 0xFF) as f32 / 255.0; + let b = (hex & 0xFF) as f32 / 255.0; + Rgba { r, g, b, a: 1.0 } +} + +/// Convert an RGBA hex color code number to [`Rgba`] +pub fn rgba(hex: u32) -> Rgba { + let r = ((hex >> 24) & 0xFF) as f32 / 255.0; + let g = ((hex >> 16) & 0xFF) as f32 / 255.0; + let b = ((hex >> 8) & 0xFF) as f32 / 255.0; + let a = (hex & 0xFF) as f32 / 255.0; + Rgba { r, g, b, a } +} + +/// An RGBA color +#[derive(PartialEq, Clone, Copy, Default)] +pub struct Rgba { + /// The red component of the color, in the range 0.0 to 1.0 + pub r: f32, + /// The green component of the color, in the range 0.0 to 1.0 + pub g: f32, + /// The blue component of the color, in the range 0.0 to 1.0 + pub b: f32, + /// The alpha component of the color, in the range 0.0 to 1.0 + pub a: f32, +} + +impl fmt::Debug for Rgba { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "rgba({:#010x})", u32::from(*self)) + } +} + +impl Rgba { + /// Create a new [`Rgba`] color by blending this and another color together + pub fn blend(&self, other: Rgba) -> Self { + if other.a >= 1.0 { + other + } else if other.a <= 0.0 { + return *self; + } else { + return Rgba { + r: (self.r * (1.0 - other.a)) + (other.r * other.a), + g: (self.g * (1.0 - other.a)) + (other.g * other.a), + b: (self.b * (1.0 - other.a)) + (other.b * other.a), + a: self.a, + }; + } + } +} + +impl From for u32 { + fn from(rgba: Rgba) -> Self { + let r = (rgba.r * 255.0) as u32; + let g = (rgba.g * 255.0) as u32; + let b = (rgba.b * 255.0) as u32; + let a = (rgba.a * 255.0) as u32; + (r << 24) | (g << 16) | (b << 8) | a + } +} + +struct RgbaVisitor; + +impl<'de> Visitor<'de> for RgbaVisitor { + type Value = Rgba; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string in the format #rrggbb or #rrggbbaa") + } + + fn visit_str(self, value: &str) -> Result { + Rgba::try_from(value).map_err(E::custom) + } +} + +impl<'de> Deserialize<'de> for Rgba { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(RgbaVisitor) + } +} + +impl From for Rgba { + fn from(color: Hsla) -> Self { + let h = color.h; + let s = color.s; + let l = color.l; + + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + let cm = c + m; + let xm = x + m; + + let (r, g, b) = match (h * 6.0).floor() as i32 { + 0 | 6 => (cm, xm, m), + 1 => (xm, cm, m), + 2 => (m, cm, xm), + 3 => (m, xm, cm), + 4 => (xm, m, cm), + _ => (cm, m, xm), + }; + + Rgba { + r, + g, + b, + a: color.a, + } + } +} + +impl TryFrom<&'_ str> for Rgba { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + const RGB: usize = "rgb".len(); + const RGBA: usize = "rgba".len(); + const RRGGBB: usize = "rrggbb".len(); + const RRGGBBAA: usize = "rrggbbaa".len(); + + const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa"; + + let Some(("", hex)) = value.trim().split_once('#') else { + bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"); + }; + + let (r, g, b, a) = match hex.len() { + RGB | RGBA => { + let r = u8::from_str_radix(&hex[0..1], 16)?; + let g = u8::from_str_radix(&hex[1..2], 16)?; + let b = u8::from_str_radix(&hex[2..3], 16)?; + let a = if hex.len() == RGBA { + u8::from_str_radix(&hex[3..4], 16)? + } else { + 0xf + }; + + /// Duplicates a given hex digit. + /// E.g., `0xf` -> `0xff`. + const fn duplicate(value: u8) -> u8 { + value << 4 | value + } + + (duplicate(r), duplicate(g), duplicate(b), duplicate(a)) + } + RRGGBB | RRGGBBAA => { + let r = u8::from_str_radix(&hex[0..2], 16)?; + let g = u8::from_str_radix(&hex[2..4], 16)?; + let b = u8::from_str_radix(&hex[4..6], 16)?; + let a = if hex.len() == RRGGBBAA { + u8::from_str_radix(&hex[6..8], 16)? + } else { + 0xff + }; + (r, g, b, a) + } + _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"), + }; + + Ok(Rgba { + r: r as f32 / 255., + g: g as f32 / 255., + b: b as f32 / 255., + a: a as f32 / 255., + }) + } +} + +/// An HSLA color +#[derive(Default, Copy, Clone, Debug)] +#[repr(C)] +pub struct Hsla { + /// Hue, in a range from 0 to 1 + pub h: f32, + + /// Saturation, in a range from 0 to 1 + pub s: f32, + + /// Lightness, in a range from 0 to 1 + pub l: f32, + + /// Alpha, in a range from 0 to 1 + pub a: f32, +} + +impl PartialEq for Hsla { + fn eq(&self, other: &Self) -> bool { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + .is_eq() + } +} + +impl PartialOrd for Hsla { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Hsla { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + } +} + +impl Eq for Hsla {} + +/// Construct an [`Hsla`] object from plain values +pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { + Hsla { + h: h.clamp(0., 1.), + s: s.clamp(0., 1.), + l: l.clamp(0., 1.), + a: a.clamp(0., 1.), + } +} + +/// Pure black in [`Hsla`] +pub fn black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 1., + } +} + +/// Transparent black in [`Hsla`] +pub fn transparent_black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 0., + } +} + +/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] +pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { + Hsla { + h: 0., + s: 0., + l: lightness.clamp(0., 1.), + a: opacity.clamp(0., 1.), + } +} + +/// Pure white in [`Hsla`] +pub fn white() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 1., + a: 1., + } +} + +/// The color red in [`Hsla`] +pub fn red() -> Hsla { + Hsla { + h: 0., + s: 1., + l: 0.5, + a: 1., + } +} + +/// The color blue in [`Hsla`] +pub fn blue() -> Hsla { + Hsla { + h: 0.6, + s: 1., + l: 0.5, + a: 1., + } +} + +/// The color green in [`Hsla`] +pub fn green() -> Hsla { + Hsla { + h: 0.33, + s: 1., + l: 0.5, + a: 1., + } +} + +/// The color yellow in [`Hsla`] +pub fn yellow() -> Hsla { + Hsla { + h: 0.16, + s: 1., + l: 0.5, + a: 1., + } +} + +impl Hsla { + /// Converts this HSLA color to an RGBA color. + pub fn to_rgb(self) -> Rgba { + self.into() + } + + /// The color red + pub fn red() -> Self { + red() + } + + /// The color green + pub fn green() -> Self { + green() + } + + /// The color blue + pub fn blue() -> Self { + blue() + } + + /// The color black + pub fn black() -> Self { + black() + } + + /// The color white + pub fn white() -> Self { + white() + } + + /// The color transparent black + pub fn transparent_black() -> Self { + transparent_black() + } + + /// Returns true if the HSLA color is fully transparent, false otherwise. + pub fn is_transparent(&self) -> bool { + self.a == 0.0 + } + + /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors. + /// + /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color. + /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color. + /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values. + /// + /// Assumptions: + /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent. + /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value. + /// - RGB color components are contained in the range [0, 1]. + /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined. + pub fn blend(self, other: Hsla) -> Hsla { + let alpha = other.a; + + if alpha >= 1.0 { + other + } else if alpha <= 0.0 { + return self; + } else { + let converted_self = Rgba::from(self); + let converted_other = Rgba::from(other); + let blended_rgb = converted_self.blend(converted_other); + return Hsla::from(blended_rgb); + } + } + + /// Returns a new HSLA color with the same hue, and lightness, but with no saturation. + pub fn grayscale(&self) -> Self { + Hsla { + h: self.h, + s: 0., + l: self.l, + a: self.a, + } + } + + /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0. + /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color. + pub fn fade_out(&mut self, factor: f32) { + self.a *= 1.0 - factor.clamp(0., 1.); + } +} + +impl From for Hsla { + fn from(color: Rgba) -> Self { + let r = color.r; + let g = color.g; + let b = color.b; + + let max = r.max(g.max(b)); + let min = r.min(g.min(b)); + let delta = max - min; + + let l = (max + min) / 2.0; + let s = if l == 0.0 || l == 1.0 { + 0.0 + } else if l < 0.5 { + delta / (2.0 * l) + } else { + delta / (2.0 - 2.0 * l) + }; + + let h = if delta == 0.0 { + 0.0 + } else if max == r { + ((g - b) / delta).rem_euclid(6.0) / 6.0 + } else if max == g { + ((b - r) / delta + 2.0) / 6.0 + } else { + ((r - g) / delta + 4.0) / 6.0 + }; + + Hsla { + h, + s, + l, + a: color.a, + } + } +} + +impl<'de> Deserialize<'de> for Hsla { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // First, deserialize it into Rgba + let rgba = Rgba::deserialize(deserializer)?; + + // Then, use the From for Hsla implementation to convert it + Ok(Hsla::from(rgba)) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_deserialize_three_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_four_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_six_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_padding_to_rgba() { + let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap(); + + assert_eq!(actual, rgba(0xf5f5f5ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap(); + + assert_eq!(actual, rgba(0xdeadbeef)) + } +} diff --git a/crates/ming/src/element.rs b/crates/ming/src/element.rs new file mode 100644 index 0000000..e4c8ead --- /dev/null +++ b/crates/ming/src/element.rs @@ -0,0 +1,627 @@ +//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of +//! the contents of a window. Elements form a tree and are laid out according to the web layout +//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time, +//! you won't need to interact with this module or these APIs directly. Elements provide their +//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert +//! that element tree into the pixels you see on the screen. +//! +//! # Element Basics +//! +//! Elements are constructed by calling [`Render::render()`] on the root view of the window, which +//! which recursively constructs the element tree from the current state of the application,. +//! These elements are then laid out by Taffy, and painted to the screen according to their own +//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element +//! tree and any callbacks they have registered with GPUI are dropped and the process repeats. +//! +//! But some state is too simple and voluminous to store in every view that needs it, e.g. +//! whether a hover has been started or not. For this, GPUI provides the [`Element::State`], associated type. +//! +//! # Implementing your own elements +//! +//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding, +//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected +//! to stay in the bounds that their parent element gives them. But with [`WindowContext::break_content_mask`], +//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays +//! and popups and anything else that shows up 'on top' of other elements. +//! With great power, comes great responsibility. +//! +//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of +//! elements that should cover most common use cases out of the box and it's recommended that you use those +//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement +//! elements when you need to take manual control of the layout and painting process, such as when using +//! your own custom layout algorithm or rendering a code editor. + +use crate::{ + util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementId, LayoutId, + Pixels, Point, Size, Style, ViewContext, WindowContext, ELEMENT_ARENA, +}; +use derive_more::{Deref, DerefMut}; +pub(crate) use smallvec::SmallVec; +use std::{any::Any, fmt::Debug, mem}; + +/// Implemented by types that participate in laying out and painting the contents of a window. +/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy. +/// You can create custom elements by implementing this trait, see the module-level documentation +/// for more details. +pub trait Element: 'static + IntoElement { + /// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently + /// provided to [`Element::prepaint`] and [`Element::paint`]. + type RequestLayoutState: 'static; + + /// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently + /// provided to [`Element::paint`]. + type PrepaintState: 'static; + + /// If this element has a unique identifier, return it here. This is used to track elements across frames, and + /// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods. + /// + /// The global id can in turn be used to access state that's connected to an element with the same id across + /// frames. This id must be unique among children of the first containing element with an id. + fn id(&self) -> Option; + + /// Before an element can be painted, we need to know where it's going to be and how big it is. + /// Use this method to request a layout from Taffy and initialize the element's state. + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState); + + /// After laying out an element, we need to commit its bounds to the current frame for hitbox + /// purposes. The state argument is the same state that was returned from [`Element::request_layout()`]. + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Self::PrepaintState; + + /// Once layout has been completed, this method will be called to paint the element to the screen. + /// The state argument is the same state that was returned from [`Element::request_layout()`]. + fn paint( + &mut self, + id: Option<&GlobalElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + cx: &mut WindowContext, + ); + + /// Convert this element into a dynamically-typed [`AnyElement`]. + fn into_any(self) -> AnyElement { + AnyElement::new(self) + } +} + +/// Implemented by any type that can be converted into an element. +pub trait IntoElement: Sized { + /// The specific type of element into which the implementing type is converted. + /// Useful for converting other types into elements automatically, like Strings + type Element: Element; + + /// Convert self into a type that implements [`Element`]. + fn into_element(self) -> Self::Element; + + /// Convert self into a dynamically-typed [`AnyElement`]. + fn into_any_element(self) -> AnyElement { + self.into_element().into_any() + } +} + +impl FluentBuilder for T {} + +/// An object that can be drawn to the screen. This is the trait that distinguishes `Views` from +/// models. Views are drawn to the screen and care about the current window's state, models are not and do not. +pub trait Render: 'static + Sized { + /// Render this view into an element tree. + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement; +} + +impl Render for Empty { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + Empty + } +} + +/// You can derive [`IntoElement`] on any type that implements this trait. +/// It is used to construct reusable `components` out of plain data. Think of +/// components as a recipe for a certain pattern of elements. RenderOnce allows +/// you to invoke this pattern, without breaking the fluent builder pattern of +/// the element APIs. +pub trait RenderOnce: 'static { + /// Render this component into an element tree. Note that this method + /// takes ownership of self, as compared to [`Render::render()`] method + /// which takes a mutable reference. + fn render(self, cx: &mut WindowContext) -> impl IntoElement; +} + +/// This is a helper trait to provide a uniform interface for constructing elements that +/// can accept any number of any kind of child elements +pub trait ParentElement { + /// Extend this element's children with the given child elements. + fn extend(&mut self, elements: impl IntoIterator); + + /// Add a single child element to this element. + fn child(mut self, child: impl IntoElement) -> Self + where + Self: Sized, + { + self.extend(std::iter::once(child.into_element().into_any())); + self + } + + /// Add multiple child elements to this element. + fn children(mut self, children: impl IntoIterator) -> Self + where + Self: Sized, + { + self.extend(children.into_iter().map(|child| child.into_any_element())); + self + } +} + +/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro +/// for [`RenderOnce`] +#[doc(hidden)] +pub struct Component(Option); + +impl Component { + /// Create a new component from the given RenderOnce type. + pub fn new(component: C) -> Self { + Component(Some(component)) + } +} + +impl Element for Component { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut element = self.0.take().unwrap().render(cx).into_any_element(); + let layout_id = element.request_layout(cx); + (layout_id, element) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _: Bounds, + element: &mut AnyElement, + cx: &mut WindowContext, + ) { + element.prepaint(cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _: Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + element.paint(cx) + } +} + +impl IntoElement for Component { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// A globally unique identifier for an element, used to track state across frames. +#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)] +pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>); + +trait ElementObject { + fn inner_element(&mut self) -> &mut dyn Any; + + fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId; + + fn prepaint(&mut self, cx: &mut WindowContext); + + fn paint(&mut self, cx: &mut WindowContext); + + fn layout_as_root( + &mut self, + available_space: Size, + cx: &mut WindowContext, + ) -> Size; +} + +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. +pub struct Drawable { + /// The drawn element. + pub element: E, + phase: ElementDrawPhase, +} + +#[derive(Default)] +enum ElementDrawPhase { + #[default] + Start, + RequestLayout { + layout_id: LayoutId, + global_id: Option, + request_layout: RequestLayoutState, + }, + LayoutComputed { + layout_id: LayoutId, + global_id: Option, + available_space: Size, + request_layout: RequestLayoutState, + }, + Prepaint { + node_id: DispatchNodeId, + global_id: Option, + bounds: Bounds, + request_layout: RequestLayoutState, + prepaint: PrepaintState, + }, + Painted, +} + +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. +impl Drawable { + pub(crate) fn new(element: E) -> Self { + Drawable { + element, + phase: ElementDrawPhase::Start, + } + } + + fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { + match mem::take(&mut self.phase) { + ElementDrawPhase::Start => { + let global_id = self.element.id().map(|element_id| { + cx.window.element_id_stack.push(element_id); + GlobalElementId(cx.window.element_id_stack.clone()) + }); + + let (layout_id, request_layout) = + self.element.request_layout(global_id.as_ref(), cx); + + if global_id.is_some() { + cx.window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::RequestLayout { + layout_id, + global_id, + request_layout, + }; + layout_id + } + _ => panic!("must call request_layout only once"), + } + } + + pub(crate) fn prepaint(&mut self, cx: &mut WindowContext) { + match mem::take(&mut self.phase) { + ElementDrawPhase::RequestLayout { + layout_id, + global_id, + mut request_layout, + } + | ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + mut request_layout, + .. + } => { + if let Some(element_id) = self.element.id() { + cx.window.element_id_stack.push(element_id); + debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack); + } + + let bounds = cx.layout_bounds(layout_id); + let node_id = cx.window.next_frame.dispatch_tree.push_node(); + let prepaint = + self.element + .prepaint(global_id.as_ref(), bounds, &mut request_layout, cx); + cx.window.next_frame.dispatch_tree.pop_node(); + + if global_id.is_some() { + cx.window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::Prepaint { + node_id, + global_id, + bounds, + request_layout, + prepaint, + }; + } + _ => panic!("must call request_layout before prepaint"), + } + } + + pub(crate) fn paint( + &mut self, + cx: &mut WindowContext, + ) -> (E::RequestLayoutState, E::PrepaintState) { + match mem::take(&mut self.phase) { + ElementDrawPhase::Prepaint { + node_id, + global_id, + bounds, + mut request_layout, + mut prepaint, + .. + } => { + if let Some(element_id) = self.element.id() { + cx.window.element_id_stack.push(element_id); + debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack); + } + + cx.window.next_frame.dispatch_tree.set_active_node(node_id); + self.element.paint( + global_id.as_ref(), + bounds, + &mut request_layout, + &mut prepaint, + cx, + ); + + if global_id.is_some() { + cx.window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::Painted; + (request_layout, prepaint) + } + _ => panic!("must call prepaint before paint"), + } + } + + pub(crate) fn layout_as_root( + &mut self, + available_space: Size, + cx: &mut WindowContext, + ) -> Size { + if matches!(&self.phase, ElementDrawPhase::Start) { + self.request_layout(cx); + } + + let layout_id = match mem::take(&mut self.phase) { + ElementDrawPhase::RequestLayout { + layout_id, + global_id, + request_layout, + } => { + cx.compute_layout(layout_id, available_space); + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + available_space, + request_layout, + }; + layout_id + } + ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + available_space: prev_available_space, + request_layout, + } => { + if available_space != prev_available_space { + cx.compute_layout(layout_id, available_space); + } + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + available_space, + request_layout, + }; + layout_id + } + _ => panic!("cannot measure after painting"), + }; + + cx.layout_bounds(layout_id).size + } +} + +impl ElementObject for Drawable +where + E: Element, + E::RequestLayoutState: 'static, +{ + fn inner_element(&mut self) -> &mut dyn Any { + &mut self.element + } + + fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { + Drawable::request_layout(self, cx) + } + + fn prepaint(&mut self, cx: &mut WindowContext) { + Drawable::prepaint(self, cx); + } + + fn paint(&mut self, cx: &mut WindowContext) { + Drawable::paint(self, cx); + } + + fn layout_as_root( + &mut self, + available_space: Size, + cx: &mut WindowContext, + ) -> Size { + Drawable::layout_as_root(self, available_space, cx) + } +} + +/// A dynamically typed element that can be used to store any element type. +pub struct AnyElement(ArenaBox); + +impl AnyElement { + pub(crate) fn new(element: E) -> Self + where + E: 'static + Element, + E::RequestLayoutState: Any, + { + let element = ELEMENT_ARENA + .with_borrow_mut(|arena| arena.alloc(|| Drawable::new(element))) + .map(|element| element as &mut dyn ElementObject); + AnyElement(element) + } + + /// Attempt to downcast a reference to the boxed element to a specific type. + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.0.inner_element().downcast_mut::() + } + + /// Request the layout ID of the element stored in this `AnyElement`. + /// Used for laying out child elements in a parent element. + pub fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { + self.0.request_layout(cx) + } + + /// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and + /// request autoscroll before the final paint pass is confirmed. + pub fn prepaint(&mut self, cx: &mut WindowContext) { + self.0.prepaint(cx) + } + + /// Paints the element stored in this `AnyElement`. + pub fn paint(&mut self, cx: &mut WindowContext) { + self.0.paint(cx) + } + + /// Performs layout for this element within the given available space and returns its size. + pub fn layout_as_root( + &mut self, + available_space: Size, + cx: &mut WindowContext, + ) -> Size { + self.0.layout_as_root(available_space, cx) + } + + /// Prepaints this element at the given absolute origin. + pub fn prepaint_at(&mut self, origin: Point, cx: &mut WindowContext) { + cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); + } + + /// Performs layout on this element in the available space, then prepaints it at the given absolute origin. + pub fn prepaint_as_root( + &mut self, + origin: Point, + available_space: Size, + cx: &mut WindowContext, + ) { + self.layout_as_root(available_space, cx); + cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); + } +} + +impl Element for AnyElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.request_layout(cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) { + self.prepaint(cx) + } + + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + self.paint(cx) + } +} + +impl IntoElement for AnyElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + + fn into_any_element(self) -> AnyElement { + self + } +} + +/// The empty element, which renders nothing. +pub struct Empty; + +impl IntoElement for Empty { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Empty { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + (cx.request_layout(Style::default(), None), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: Bounds, + _state: &mut Self::RequestLayoutState, + _cx: &mut WindowContext, + ) { + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + _cx: &mut WindowContext, + ) { + } +} diff --git a/crates/ming/src/elements/anchored.rs b/crates/ming/src/elements/anchored.rs new file mode 100644 index 0000000..ed68e3e --- /dev/null +++ b/crates/ming/src/elements/anchored.rs @@ -0,0 +1,312 @@ +use smallvec::SmallVec; +use taffy::style::{Display, Position}; + +use crate::{ + point, AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, ParentElement, + Pixels, Point, Size, Style, WindowContext, +}; + +/// The state that the anchored element element uses to track its children. +pub struct AnchoredState { + child_layout_ids: SmallVec<[LayoutId; 4]>, +} + +/// An anchored element that can be used to display UI that +/// will avoid overflowing the window bounds. +pub struct Anchored { + children: SmallVec<[AnyElement; 2]>, + anchor_corner: AnchorCorner, + fit_mode: AnchoredFitMode, + anchor_position: Option>, + position_mode: AnchoredPositionMode, +} + +/// anchored gives you an element that will avoid overflowing the window bounds. +/// Its children should have no margin to avoid measurement issues. +pub fn anchored() -> Anchored { + Anchored { + children: SmallVec::new(), + anchor_corner: AnchorCorner::TopLeft, + fit_mode: AnchoredFitMode::SwitchAnchor, + anchor_position: None, + position_mode: AnchoredPositionMode::Window, + } +} + +impl Anchored { + /// Sets which corner of the anchored element should be anchored to the current position. + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor_corner = anchor; + self + } + + /// Sets the position in window coordinates + /// (otherwise the location the anchored element is rendered is used) + pub fn position(mut self, anchor: Point) -> Self { + self.anchor_position = Some(anchor); + self + } + + /// Sets the position mode for this anchored element. Local will have this + /// interpret its [`Anchored::position`] as relative to the parent element. + /// While Window will have it interpret the position as relative to the window. + pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self { + self.position_mode = mode; + self + } + + /// Snap to window edge instead of switching anchor corner when an overflow would occur. + pub fn snap_to_window(mut self) -> Self { + self.fit_mode = AnchoredFitMode::SnapToWindow; + self + } +} + +impl ParentElement for Anchored { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Element for Anchored { + type RequestLayoutState = AnchoredState; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(cx)) + .collect::>(); + + let anchored_style = Style { + position: Position::Absolute, + display: Display::Flex, + ..Style::default() + }; + + let layout_id = cx.request_layout(anchored_style, child_layout_ids.iter().copied()); + + (layout_id, AnchoredState { child_layout_ids }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) { + if request_layout.child_layout_ids.is_empty() { + return; + } + + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + for child_layout_id in &request_layout.child_layout_ids { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); + } + let size: Size = (child_max - child_min).into(); + + let (origin, mut desired) = self.position_mode.get_position_and_bounds( + self.anchor_position, + self.anchor_corner, + size, + bounds, + ); + + let limits = Bounds { + origin: Point::default(), + size: cx.viewport_size(), + }; + + if self.fit_mode == AnchoredFitMode::SwitchAnchor { + let mut anchor_corner = self.anchor_corner; + + if desired.left() < limits.left() || desired.right() > limits.right() { + let switched = anchor_corner + .switch_axis(Axis::Horizontal) + .get_bounds(origin, size); + if !(switched.left() < limits.left() || switched.right() > limits.right()) { + anchor_corner = anchor_corner.switch_axis(Axis::Horizontal); + desired = switched + } + } + + if desired.top() < limits.top() || desired.bottom() > limits.bottom() { + let switched = anchor_corner + .switch_axis(Axis::Vertical) + .get_bounds(origin, size); + if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) { + desired = switched; + } + } + } + + // Snap the horizontal edges of the anchored element to the horizontal edges of the window if + // its horizontal bounds overflow, aligning to the left if it is wider than the limits. + if desired.right() > limits.right() { + desired.origin.x -= desired.right() - limits.right(); + } + if desired.left() < limits.left() { + desired.origin.x = limits.origin.x; + } + + // Snap the vertical edges of the anchored element to the vertical edges of the window if + // its vertical bounds overflow, aligning to the top if it is taller than the limits. + if desired.bottom() > limits.bottom() { + desired.origin.y -= desired.bottom() - limits.bottom(); + } + if desired.top() < limits.top() { + desired.origin.y = limits.origin.y; + } + + let offset = desired.origin - bounds.origin; + let offset = point(offset.x.round(), offset.y.round()); + + cx.with_element_offset(offset, |cx| { + for child in &mut self.children { + child.prepaint(cx); + } + }) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: crate::Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + for child in &mut self.children { + child.paint(cx); + } + } +} + +impl IntoElement for Anchored { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +enum Axis { + Horizontal, + Vertical, +} + +/// Which algorithm to use when fitting the anchored element to be inside the window. +#[derive(Copy, Clone, PartialEq)] +pub enum AnchoredFitMode { + /// Snap the anchored element to the window edge + SnapToWindow, + /// Switch which corner anchor this anchored element is attached to + SwitchAnchor, +} + +/// Which algorithm to use when positioning the anchored element. +#[derive(Copy, Clone, PartialEq)] +pub enum AnchoredPositionMode { + /// Position the anchored element relative to the window + Window, + /// Position the anchored element relative to its parent + Local, +} + +impl AnchoredPositionMode { + fn get_position_and_bounds( + &self, + anchor_position: Option>, + anchor_corner: AnchorCorner, + size: Size, + bounds: Bounds, + ) -> (Point, Bounds) { + match self { + AnchoredPositionMode::Window => { + let anchor_position = anchor_position.unwrap_or(bounds.origin); + let bounds = anchor_corner.get_bounds(anchor_position, size); + (anchor_position, bounds) + } + AnchoredPositionMode::Local => { + let anchor_position = anchor_position.unwrap_or_default(); + let bounds = anchor_corner.get_bounds(bounds.origin + anchor_position, size); + (anchor_position, bounds) + } + } + } +} + +/// Which corner of the anchored element should be considered the anchor. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum AnchorCorner { + /// The top left corner + TopLeft, + /// The top right corner + TopRight, + /// The bottom left corner + BottomLeft, + /// The bottom right corner + BottomRight, +} + +impl AnchorCorner { + fn get_bounds(&self, origin: Point, size: Size) -> Bounds { + let origin = match self { + Self::TopLeft => origin, + Self::TopRight => Point { + x: origin.x - size.width, + y: origin.y, + }, + Self::BottomLeft => Point { + x: origin.x, + y: origin.y - size.height, + }, + Self::BottomRight => Point { + x: origin.x - size.width, + y: origin.y - size.height, + }, + }; + + Bounds { origin, size } + } + + /// Get the point corresponding to this anchor corner in `bounds`. + pub fn corner(&self, bounds: Bounds) -> Point { + match self { + Self::TopLeft => bounds.origin, + Self::TopRight => bounds.upper_right(), + Self::BottomLeft => bounds.lower_left(), + Self::BottomRight => bounds.lower_right(), + } + } + + fn switch_axis(self, axis: Axis) -> Self { + match axis { + Axis::Vertical => match self { + AnchorCorner::TopLeft => AnchorCorner::BottomLeft, + AnchorCorner::TopRight => AnchorCorner::BottomRight, + AnchorCorner::BottomLeft => AnchorCorner::TopLeft, + AnchorCorner::BottomRight => AnchorCorner::TopRight, + }, + Axis::Horizontal => match self { + AnchorCorner::TopLeft => AnchorCorner::TopRight, + AnchorCorner::TopRight => AnchorCorner::TopLeft, + AnchorCorner::BottomLeft => AnchorCorner::BottomRight, + AnchorCorner::BottomRight => AnchorCorner::BottomLeft, + }, + } + } +} diff --git a/crates/ming/src/elements/animation.rs b/crates/ming/src/elements/animation.rs new file mode 100644 index 0000000..29506a6 --- /dev/null +++ b/crates/ming/src/elements/animation.rs @@ -0,0 +1,203 @@ +use std::time::{Duration, Instant}; + +use crate::{AnyElement, Element, ElementId, GlobalElementId, IntoElement}; + +pub use easing::*; + +/// An animation that can be applied to an element. +pub struct Animation { + /// The amount of time for which this animation should run + pub duration: Duration, + /// Whether to repeat this animation when it finishes + pub oneshot: bool, + /// A function that takes a delta between 0 and 1 and returns a new delta + /// between 0 and 1 based on the given easing function. + pub easing: Box f32>, +} + +impl Animation { + /// Create a new animation with the given duration. + /// By default the animation will only run once and will use a linear easing function. + pub fn new(duration: Duration) -> Self { + Self { + duration, + oneshot: true, + easing: Box::new(linear), + } + } + + /// Set the animation to loop when it finishes. + pub fn repeat(mut self) -> Self { + self.oneshot = false; + self + } + + /// Set the easing function to use for this animation. + /// The easing function will take a time delta between 0 and 1 and return a new delta + /// between 0 and 1 + pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { + self.easing = Box::new(easing); + self + } +} + +/// An extension trait for adding the animation wrapper to both Elements and Components +pub trait AnimationExt { + /// Render this component or element with an animation + fn with_animation( + self, + id: impl Into, + animation: Animation, + animator: impl Fn(Self, f32) -> Self + 'static, + ) -> AnimationElement + where + Self: Sized, + { + AnimationElement { + id: id.into(), + element: Some(self), + animator: Box::new(animator), + animation, + } + } +} + +impl AnimationExt for E {} + +/// A GPUI element that applies an animation to another element +pub struct AnimationElement { + id: ElementId, + element: Option, + animation: Animation, + animator: Box E + 'static>, +} + +impl AnimationElement { + /// Returns a new [`AnimationElement`] after applying the given function + /// to the element being animated. + pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement { + self.element = self.element.map(f); + self + } +} + +impl IntoElement for AnimationElement { + type Element = AnimationElement; + + fn into_element(self) -> Self::Element { + self + } +} + +struct AnimationState { + start: Instant, +} + +impl Element for AnimationElement { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut crate::WindowContext, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + cx.with_element_state(global_id.unwrap(), |state, cx| { + let state = state.unwrap_or_else(|| AnimationState { + start: Instant::now(), + }); + let mut delta = + state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32(); + + let mut done = false; + if delta > 1.0 { + if self.animation.oneshot { + done = true; + delta = 1.0; + } else { + delta = delta % 1.0; + } + } + let delta = (self.animation.easing)(delta); + + debug_assert!( + delta >= 0.0 && delta <= 1.0, + "delta should always be between 0 and 1" + ); + + let element = self.element.take().expect("should only be called once"); + let mut element = (self.animator)(element, delta).into_any_element(); + + if !done { + let parent_id = cx.parent_view_id(); + cx.on_next_frame(move |cx| { + if let Some(parent_id) = parent_id { + cx.notify(parent_id) + } else { + cx.refresh() + } + }) + } + + ((element.request_layout(cx), element), state) + }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + cx: &mut crate::WindowContext, + ) -> Self::PrepaintState { + element.prepaint(cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut crate::WindowContext, + ) { + element.paint(cx); + } +} + +mod easing { + /// The linear easing function, or delta itself + pub fn linear(delta: f32) -> f32 { + delta + } + + /// The quadratic easing function, delta * delta + pub fn quadratic(delta: f32) -> f32 { + delta * delta + } + + /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle + pub fn ease_in_out(delta: f32) -> f32 { + if delta < 0.5 { + 2.0 * delta * delta + } else { + let x = -2.0 * delta + 2.0; + 1.0 - x * x / 2.0 + } + } + + /// Apply the given easing function, first in the forward direction and then in the reverse direction + pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { + move |delta| { + if delta < 0.5 { + easing(delta * 2.0) + } else { + easing((1.0 - delta) * 2.0) + } + } + } +} diff --git a/crates/ming/src/elements/canvas.rs b/crates/ming/src/elements/canvas.rs new file mode 100644 index 0000000..ccf29f4 --- /dev/null +++ b/crates/ming/src/elements/canvas.rs @@ -0,0 +1,85 @@ +use refineable::Refineable as _; + +use crate::{ + Bounds, Element, ElementId, GlobalElementId, IntoElement, Pixels, Style, StyleRefinement, + Styled, WindowContext, +}; + +/// Construct a canvas element with the given paint callback. +/// Useful for adding short term custom drawing to a view. +pub fn canvas( + prepaint: impl 'static + FnOnce(Bounds, &mut WindowContext) -> T, + paint: impl 'static + FnOnce(Bounds, T, &mut WindowContext), +) -> Canvas { + Canvas { + prepaint: Some(Box::new(prepaint)), + paint: Some(Box::new(paint)), + style: StyleRefinement::default(), + } +} + +/// A canvas element, meant for accessing the low level paint API without defining a whole +/// custom element +pub struct Canvas { + prepaint: Option, &mut WindowContext) -> T>>, + paint: Option, T, &mut WindowContext)>>, + style: StyleRefinement, +} + +impl IntoElement for Canvas { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Canvas { + type RequestLayoutState = Style; + type PrepaintState = Option; + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = cx.request_layout(style.clone(), []); + (layout_id, style) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + _request_layout: &mut Style, + cx: &mut WindowContext, + ) -> Option { + Some(self.prepaint.take().unwrap()(bounds, cx)) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + style: &mut Style, + prepaint: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + let prepaint = prepaint.take().unwrap(); + style.paint(bounds, cx, |cx| { + (self.paint.take().unwrap())(bounds, prepaint, cx) + }); + } +} + +impl Styled for Canvas { + fn style(&mut self) -> &mut crate::StyleRefinement { + &mut self.style + } +} diff --git a/crates/ming/src/elements/deferred.rs b/crates/ming/src/elements/deferred.rs new file mode 100644 index 0000000..b878897 --- /dev/null +++ b/crates/ming/src/elements/deferred.rs @@ -0,0 +1,85 @@ +use crate::{ + AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, Pixels, WindowContext, +}; + +/// Builds a `Deferred` element, which delays the layout and paint of its child. +pub fn deferred(child: impl IntoElement) -> Deferred { + Deferred { + child: Some(child.into_any_element()), + priority: 0, + } +} + +/// An element which delays the painting of its child until after all of +/// its ancestors, while keeping its layout as part of the current element tree. +pub struct Deferred { + child: Option, + priority: usize, +} + +impl Deferred { + /// Sets the `priority` value of the `deferred` element, which + /// determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + pub fn with_priority(mut self, priority: usize) -> Self { + self.priority = priority; + self + } +} + +impl Element for Deferred { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, ()) { + let layout_id = self.child.as_mut().unwrap().request_layout(cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) { + let child = self.child.take().unwrap(); + let element_offset = cx.element_offset(); + cx.defer_draw(child, element_offset, self.priority) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + _cx: &mut WindowContext, + ) { + } +} + +impl IntoElement for Deferred { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Deferred { + /// Sets a priority for the element. A higher priority conceptually means painting the element + /// on top of deferred draws with a lower priority (i.e. closer to the viewer). + pub fn priority(mut self, priority: usize) -> Self { + self.priority = priority; + self + } +} diff --git a/crates/ming/src/elements/div.rs b/crates/ming/src/elements/div.rs new file mode 100644 index 0000000..00609ae --- /dev/null +++ b/crates/ming/src/elements/div.rs @@ -0,0 +1,2546 @@ +//! Div is the central, reusable element that most GPUI trees will be built from. +//! It functions as a container for other elements, and provides a number of +//! useful features for laying out and styling its children as well as binding +//! mouse events and action handlers. It is meant to be similar to the HTML `
` +//! element, but for GPUI. +//! +//! # Build your own div +//! +//! GPUI does not directly provide APIs for stateful, multi step events like `click` +//! and `drag`. We want GPUI users to be able to build their own abstractions for +//! their own needs. However, as a UI framework, we're also obliged to provide some +//! building blocks to make the process of building your own elements easier. +//! For this we have the [`Interactivity`] and the [`StyleRefinement`] structs, as well +//! as several associated traits. Together, these provide the full suite of Dom-like events +//! and Tailwind-like styling that you can use to build your own custom elements. Div is +//! constructed by combining these two systems into an all-in-one element. + +use crate::{ + point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds, + ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, Global, GlobalElementId, Hitbox, + HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, + ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, + StyleRefinement, Styled, Task, TooltipId, View, Visibility, WindowContext, +}; +use collections::HashMap; +use refineable::Refineable; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + cmp::Ordering, + fmt::Debug, + marker::PhantomData, + mem, + ops::DerefMut, + rc::Rc, + time::Duration, +}; +use taffy::style::Overflow; +use util::ResultExt; + +const DRAG_THRESHOLD: f64 = 2.; +pub(crate) const TOOLTIP_DELAY: Duration = Duration::from_millis(500); + +/// The styling information for a given group. +pub struct GroupStyle { + /// The identifier for this group. + pub group: SharedString, + + /// The specific style refinement that this group would apply + /// to its children. + pub style: Box, +} + +/// An event for when a drag is moving over this element, with the given state type. +pub struct DragMoveEvent { + /// The mouse move event that triggered this drag move event. + pub event: MouseMoveEvent, + + /// The bounds of this element. + pub bounds: Bounds, + drag: PhantomData, +} + +impl DragMoveEvent { + /// Returns the drag state for this event. + pub fn drag<'b>(&self, cx: &'b AppContext) -> &'b T { + cx.active_drag + .as_ref() + .and_then(|drag| drag.value.downcast_ref::()) + .expect("DragMoveEvent is only valid when the stored active drag is of the same type.") + } +} + +impl Interactivity { + /// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase + /// The imperative API equivalent of [`InteractiveElement::on_mouse_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to the view state from this callback. + pub fn on_mouse_down( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && event.button == button && hitbox.is_hovered(cx) + { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn capture_any_mouse_down( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(cx) { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to the mouse down event for any button, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_any_mouse_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_any_mouse_down( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for the given button, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_mouse_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_mouse_up( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && event.button == button && hitbox.is_hovered(cx) + { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for any button, during the capture phase + /// the imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn capture_any_mouse_up( + &mut self, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(cx) { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for any button, during the bubble phase + /// the imperative API equivalent to [`Interactivity::on_any_mouse_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_any_mouse_up( + &mut self, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_mouse_down_out( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture && !hitbox.contains(&cx.mouse_position()) { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_mouse_up_out( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture + && event.button == button + && !hitbox.is_hovered(cx) + { + (listener)(event, cx); + } + })); + } + + /// Bind the given callback to the mouse move event, during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_mouse_move`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_mouse_move( + &mut self, + listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, + ) { + self.mouse_move_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + (listener)(event, cx); + } + })); + } + + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The imperative API equivalent to [`InteractiveElement::on_drag_move`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_drag_move( + &mut self, + listener: impl Fn(&DragMoveEvent, &mut WindowContext) + 'static, + ) where + T: 'static, + { + self.mouse_move_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture + && cx + .active_drag + .as_ref() + .is_some_and(|drag| drag.value.as_ref().type_id() == TypeId::of::()) + { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + }, + cx, + ); + } + })); + } + + /// Bind the given callback to scroll wheel events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_scroll_wheel( + &mut self, + listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, + ) { + self.scroll_wheel_listeners + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + (listener)(event, cx); + } + })); + } + + /// Bind the given callback to an action dispatch during the capture phase + /// The imperative API equivalent to [`InteractiveElement::capture_action`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn capture_action( + &mut self, + listener: impl Fn(&A, &mut WindowContext) + 'static, + ) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Capture { + (listener)(action, cx) + } + }), + )); + } + + /// Bind the given callback to an action dispatch during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_action`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_action(&mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + (listener)(action, cx) + } + }), + )); + } + + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The imperative API equivalent to [`InteractiveElement::on_boxed_action`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_boxed_action( + &mut self, + action: &dyn Action, + listener: impl Fn(&Box, &mut WindowContext) + 'static, + ) { + let action = action.boxed_clone(); + self.action_listeners.push(( + (*action).type_id(), + Box::new(move |_, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener)(&action, cx) + } + }), + )); + } + + /// Bind the given callback to key down events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_key_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_key_down(&mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static) { + self.key_down_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener)(event, cx) + } + })); + } + + /// Bind the given callback to key down events during the capture phase + /// The imperative API equivalent to [`InteractiveElement::capture_key_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn capture_key_down( + &mut self, + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, + ) { + self.key_down_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + listener(event, cx) + } + })); + } + + /// Bind the given callback to key up events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_key_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) { + self.key_up_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + listener(event, cx) + } + })); + } + + /// Bind the given callback to key up events during the capture phase + /// The imperative API equivalent to [`InteractiveElement::on_key_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn capture_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) { + self.key_up_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + listener(event, cx) + } + })); + } + + /// Bind the given callback to modifiers changing events. + /// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_modifiers_changed( + &mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static, + ) { + self.modifiers_changed_listeners + .push(Box::new(move |event, cx| listener(event, cx))); + } + + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element + /// The imperative API equivalent to [`InteractiveElement::on_drop`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_drop(&mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) { + self.drop_listeners.push(( + TypeId::of::(), + Box::new(move |dragged_value, cx| { + listener(dragged_value.downcast_ref().unwrap(), cx); + }), + )); + } + + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element + /// The imperative API equivalent to [`InteractiveElement::can_drop`] + pub fn can_drop(&mut self, predicate: impl Fn(&dyn Any, &mut WindowContext) -> bool + 'static) { + self.can_drop_predicate = Some(Box::new(predicate)); + } + + /// Bind the given callback to click events of this element + /// The imperative API equivalent to [`StatefulInteractiveElement::on_click`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) + where + Self: Sized, + { + self.click_listeners + .push(Box::new(move |event, cx| listener(event, cx))); + } + + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`Self::on_drag_move`] API + /// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_drag( + &mut self, + value: T, + constructor: impl Fn(&T, &mut WindowContext) -> View + 'static, + ) where + Self: Sized, + T: 'static, + W: 'static + Render, + { + debug_assert!( + self.drag_listener.is_none(), + "calling on_drag more than once on the same element is not supported" + ); + self.drag_listener = Some(( + Box::new(value), + Box::new(move |value, cx| constructor(value.downcast_ref().unwrap(), cx).into()), + )); + } + + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) + where + Self: Sized, + { + debug_assert!( + self.hover_listener.is_none(), + "calling on_hover more than once on the same element is not supported" + ); + self.hover_listener = Some(Box::new(listener)); + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The imperative API equivalent to [`InteractiveElement::tooltip`] + pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) + where + Self: Sized, + { + debug_assert!( + self.tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.tooltip_builder = Some(TooltipBuilder { + build: Rc::new(build_tooltip), + hoverable: false, + }); + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into + /// the tooltip. The imperative API equivalent to [`InteractiveElement::hoverable_tooltip`] + pub fn hoverable_tooltip( + &mut self, + build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static, + ) where + Self: Sized, + { + debug_assert!( + self.tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.tooltip_builder = Some(TooltipBuilder { + build: Rc::new(build_tooltip), + hoverable: true, + }); + } + + /// Block the mouse from interacting with this element or any of its children + /// The imperative API equivalent to [`InteractiveElement::block_mouse`] + pub fn occlude_mouse(&mut self) { + self.occlude_mouse = true; + } +} + +/// A trait for elements that want to use the standard GPUI event handlers that don't +/// require any state. +pub trait InteractiveElement: Sized { + /// Retrieve the interactivity state associated with this element + fn interactivity(&mut self) -> &mut Interactivity; + + /// Assign this element to a group of elements that can be styled together + fn group(mut self, group: impl Into) -> Self { + self.interactivity().group = Some(group.into()); + self + } + + /// Assign this element an ID, so that it can be used with interactivity + fn id(mut self, id: impl Into) -> Stateful { + self.interactivity().element_id = Some(id.into()); + + Stateful { element: self } + } + + /// Track the focus state of the given focus handle on this element. + /// If the focus handle is focused by the application, this element will + /// apply its focused styles. + fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { + self.interactivity().focusable = true; + self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); + Focusable { element: self } + } + + /// Set the keymap context for this element. This will be used to determine + /// which action to dispatch from the keymap. + fn key_context(mut self, key_context: C) -> Self + where + C: TryInto, + E: Debug, + { + if let Some(key_context) = key_context.try_into().log_err() { + self.interactivity().key_context = Some(key_context); + } + self + } + + /// Apply the given style to this element when the mouse hovers over it + fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { + debug_assert!( + self.interactivity().hover_style.is_none(), + "hover style already set" + ); + self.interactivity().hover_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Apply the given style to this element when the mouse hovers over a group member + fn group_hover( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_hover_style = Some(GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }); + self + } + + /// Bind the given callback to the mouse down event for the given mouse button, + /// the fluent API equivalent to [`Interactivity::on_mouse_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to the view state from this callback. + fn on_mouse_down( + mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_mouse_down(button, listener); + self + } + + #[cfg(any(test, feature = "test-support"))] + /// Set a key that can be used to look up this element's bounds + /// in the [`VisualTestContext::debug_bounds`] map + /// This is a noop in release builds + fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self { + self.interactivity().debug_selector = Some(f()); + self + } + + #[cfg(not(any(test, feature = "test-support")))] + /// Set a key that can be used to look up this element's bounds + /// in the [`VisualTestContext::debug_bounds`] map + /// This is a noop in release builds + #[inline] + fn debug_selector(self, _: impl FnOnce() -> String) -> Self { + self + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::capture_any_mouse_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn capture_any_mouse_down( + mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().capture_any_mouse_down(listener); + self + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::on_any_mouse_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_any_mouse_down( + mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_any_mouse_down(listener); + self + } + + /// Bind the given callback to the mouse up event for the given button, during the bubble phase + /// the fluent API equivalent to [`Interactivity::on_mouse_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_mouse_up( + mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_mouse_up(button, listener); + self + } + + /// Bind the given callback to the mouse up event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::capture_any_mouse_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn capture_any_mouse_up( + mut self, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().capture_any_mouse_up(listener); + self + } + + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_down_out`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_mouse_down_out( + mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_mouse_down_out(listener); + self + } + + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_up_out`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_mouse_up_out( + mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_mouse_up_out(button, listener); + self + } + + /// Bind the given callback to the mouse move event, during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_mouse_move`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_mouse_move( + mut self, + listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_mouse_move(listener); + self + } + + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The fluent API equivalent to [`Interactivity::on_drag_move`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_drag_move( + mut self, + listener: impl Fn(&DragMoveEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_drag_move(listener); + self + } + + /// Bind the given callback to scroll wheel events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_scroll_wheel`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_scroll_wheel( + mut self, + listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_scroll_wheel(listener); + self + } + + /// Capture the given action, before normal action dispatch can fire + /// The fluent API equivalent to [`Interactivity::on_scroll_wheel`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn capture_action( + mut self, + listener: impl Fn(&A, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().capture_action(listener); + self + } + + /// Bind the given callback to an action dispatch during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_action`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_action(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self { + self.interactivity().on_action(listener); + self + } + + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The fluent API equivalent to [`Interactivity::on_boxed_action`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_boxed_action( + mut self, + action: &dyn Action, + listener: impl Fn(&Box, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_boxed_action(action, listener); + self + } + + /// Bind the given callback to key down events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_key_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_key_down( + mut self, + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_key_down(listener); + self + } + + /// Bind the given callback to key down events during the capture phase + /// The fluent API equivalent to [`Interactivity::capture_key_down`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn capture_key_down( + mut self, + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().capture_key_down(listener); + self + } + + /// Bind the given callback to key up events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_key_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self { + self.interactivity().on_key_up(listener); + self + } + + /// Bind the given callback to key up events during the capture phase + /// The fluent API equivalent to [`Interactivity::capture_key_up`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn capture_key_up( + mut self, + listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().capture_key_up(listener); + self + } + + /// Bind the given callback to modifiers changing events. + /// The fluent API equivalent to [`Interactivity::on_modifiers_changed`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_modifiers_changed( + mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().on_modifiers_changed(listener); + self + } + + /// Apply the given style when the given data type is dragged over this element + fn drag_over( + mut self, + f: impl 'static + Fn(StyleRefinement, &S, &WindowContext) -> StyleRefinement, + ) -> Self { + self.interactivity().drag_over_styles.push(( + TypeId::of::(), + Box::new(move |currently_dragged: &dyn Any, cx| { + f( + StyleRefinement::default(), + currently_dragged.downcast_ref::().unwrap(), + cx, + ) + }), + )); + self + } + + /// Apply the given style when the given data type is dragged over this element's group + fn group_drag_over( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_drag_over_styles.push(( + TypeId::of::(), + GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }, + )); + self + } + + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element + /// The fluent API equivalent to [`Interactivity::on_drop`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_drop(mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) -> Self { + self.interactivity().on_drop(listener); + self + } + + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element + /// The fluent API equivalent to [`Interactivity::can_drop`] + fn can_drop( + mut self, + predicate: impl Fn(&dyn Any, &mut WindowContext) -> bool + 'static, + ) -> Self { + self.interactivity().can_drop(predicate); + self + } + + /// Block the mouse from interacting with this element or any of its children + /// The fluent API equivalent to [`Interactivity::block_mouse`] + fn occlude(mut self) -> Self { + self.interactivity().occlude_mouse(); + self + } +} + +/// A trait for elements that want to use the standard GPUI interactivity features +/// that require state. +pub trait StatefulInteractiveElement: InteractiveElement { + /// Set this element to focusable. + fn focusable(mut self) -> Focusable { + self.interactivity().focusable = true; + Focusable { element: self } + } + + /// Set the overflow x and y to scroll. + fn overflow_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + /// Set the overflow x to scroll. + fn overflow_x_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self + } + + /// Set the overflow y to scroll. + fn overflow_y_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + /// Track the scroll state of this element with the given handle. + fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); + self + } + + /// Set the given styles to be applied when this element is active. + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().active_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Set the given styles to be applied when this element's group is active. + fn group_active( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_active_style = Some(GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }); + self + } + + /// Bind the given callback to click events of this element + /// The fluent API equivalent to [`Interactivity::on_click`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self + where + Self: Sized, + { + self.interactivity().on_click(listener); + self + } + + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`Self::on_drag_move`] API + /// The fluent API equivalent to [`Interactivity::on_drag`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_drag( + mut self, + value: T, + constructor: impl Fn(&T, &mut WindowContext) -> View + 'static, + ) -> Self + where + Self: Sized, + T: 'static, + W: 'static + Render, + { + self.interactivity().on_drag(value, constructor); + self + } + + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// The fluent API equivalent to [`Interactivity::on_hover`] + /// + /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback. + fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self + where + Self: Sized, + { + self.interactivity().on_hover(listener); + self + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The fluent API equivalent to [`Interactivity::tooltip`] + fn tooltip(mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self + where + Self: Sized, + { + self.interactivity().tooltip(build_tooltip); + self + } + + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into + /// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`] + fn hoverable_tooltip( + mut self, + build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().hoverable_tooltip(build_tooltip); + self + } +} + +/// A trait for providing focus related APIs to interactive elements +pub trait FocusableElement: InteractiveElement { + /// Set the given styles to be applied when this element, specifically, is focused. + fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Set the given styles to be applied when this element is inside another element that is focused. + fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default()))); + self + } +} + +pub(crate) type MouseDownListener = + Box; +pub(crate) type MouseUpListener = + Box; + +pub(crate) type MouseMoveListener = + Box; + +pub(crate) type ScrollWheelListener = + Box; + +pub(crate) type ClickListener = Box; + +pub(crate) type DragListener = Box AnyView + 'static>; + +type DropListener = Box; + +type CanDropPredicate = Box bool + 'static>; + +pub(crate) struct TooltipBuilder { + build: Rc AnyView + 'static>, + hoverable: bool, +} + +pub(crate) type KeyDownListener = + Box; + +pub(crate) type KeyUpListener = + Box; + +pub(crate) type ModifiersChangedListener = + Box; + +pub(crate) type ActionListener = Box; + +/// Construct a new [`Div`] element +#[track_caller] +pub fn div() -> Div { + #[cfg(debug_assertions)] + let interactivity = Interactivity { + location: Some(*core::panic::Location::caller()), + ..Default::default() + }; + + #[cfg(not(debug_assertions))] + let interactivity = Interactivity::default(); + + Div { + interactivity, + children: SmallVec::default(), + } +} + +/// A [`Div`] element, the all-in-one element for building complex UIs in GPUI +pub struct Div { + interactivity: Interactivity, + children: SmallVec<[AnyElement; 2]>, +} + +/// A frame state for a `Div` element, which contains layout IDs for its children. +/// +/// This struct is used internally by the `Div` element to manage the layout state of its children +/// during the UI update cycle. It holds a small vector of `LayoutId` values, each corresponding to +/// a child element of the `Div`. These IDs are used to query the layout engine for the computed +/// bounds of the children after the layout phase is complete. +pub struct DivFrameState { + child_layout_ids: SmallVec<[LayoutId; 2]>, +} + +impl Styled for Div { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Div { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl ParentElement for Div { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Element for Div { + type RequestLayoutState = DivFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut child_layout_ids = SmallVec::new(); + let layout_id = self + .interactivity + .request_layout(global_id, cx, |style, cx| { + cx.with_text_style(style.text_style().cloned(), |cx| { + child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(cx)) + .collect::>(); + cx.request_layout(style, child_layout_ids.iter().copied()) + }) + }); + (layout_id, DivFrameState { child_layout_ids }) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Option { + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + let content_size = if request_layout.child_layout_ids.is_empty() { + bounds.size + } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { + let mut state = scroll_handle.0.borrow_mut(); + state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len()); + state.bounds = bounds; + let requested = state.requested_scroll_top.take(); + + for (ix, child_layout_id) in request_layout.child_layout_ids.iter().enumerate() { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); + state.child_bounds.push(child_bounds); + + if let Some(requested) = requested.as_ref() { + if requested.0 == ix { + *state.offset.borrow_mut() = + bounds.origin - (child_bounds.origin - point(px(0.), requested.1)); + } + } + } + (child_max - child_min).into() + } else { + for child_layout_id in &request_layout.child_layout_ids { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); + } + (child_max - child_min).into() + }; + + self.interactivity.prepaint( + global_id, + bounds, + content_size, + cx, + |_style, scroll_offset, hitbox, cx| { + cx.with_element_offset(scroll_offset, |cx| { + for child in &mut self.children { + child.prepaint(cx); + } + }); + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + cx: &mut WindowContext, + ) { + self.interactivity + .paint(global_id, bounds, hitbox.as_ref(), cx, |_style, cx| { + for child in &mut self.children { + child.paint(cx); + } + }); + } +} + +impl IntoElement for Div { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// The interactivity struct. Powers all of the general-purpose +/// interactivity in the `Div` element. +#[derive(Default)] +pub struct Interactivity { + /// The element ID of the element. In id is required to support a stateful subset of the interactivity such as on_click. + pub element_id: Option, + /// Whether the element was clicked. This will only be present after layout. + pub active: Option, + /// Whether the element was hovered. This will only be present after paint if an hitbox + /// was created for the interactive element. + pub hovered: Option, + pub(crate) tooltip_id: Option, + pub(crate) content_size: Size, + pub(crate) key_context: Option, + pub(crate) focusable: bool, + pub(crate) tracked_focus_handle: Option, + pub(crate) tracked_scroll_handle: Option, + pub(crate) scroll_offset: Option>>>, + pub(crate) group: Option, + /// The base style of the element, before any modifications are applied + /// by focus, active, etc. + pub base_style: Box, + pub(crate) focus_style: Option>, + pub(crate) in_focus_style: Option>, + pub(crate) hover_style: Option>, + pub(crate) group_hover_style: Option, + pub(crate) active_style: Option>, + pub(crate) group_active_style: Option, + pub(crate) drag_over_styles: Vec<( + TypeId, + Box StyleRefinement>, + )>, + pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>, + pub(crate) mouse_down_listeners: Vec, + pub(crate) mouse_up_listeners: Vec, + pub(crate) mouse_move_listeners: Vec, + pub(crate) scroll_wheel_listeners: Vec, + pub(crate) key_down_listeners: Vec, + pub(crate) key_up_listeners: Vec, + pub(crate) modifiers_changed_listeners: Vec, + pub(crate) action_listeners: Vec<(TypeId, ActionListener)>, + pub(crate) drop_listeners: Vec<(TypeId, DropListener)>, + pub(crate) can_drop_predicate: Option, + pub(crate) click_listeners: Vec, + pub(crate) drag_listener: Option<(Box, DragListener)>, + pub(crate) hover_listener: Option>, + pub(crate) tooltip_builder: Option, + pub(crate) occlude_mouse: bool, + + #[cfg(debug_assertions)] + pub(crate) location: Option>, + + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_selector: Option, +} + +impl Interactivity { + /// Layout this element according to this interactivity state's configured styles + pub fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + f: impl FnOnce(Style, &mut WindowContext) -> LayoutId, + ) -> LayoutId { + cx.with_optional_element_state::( + global_id, + |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + + if let Some(element_state) = element_state.as_ref() { + if cx.has_active_drag() { + if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() + { + *pending_mouse_down.borrow_mut() = None; + } + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + *clicked_state.borrow_mut() = ElementClickedState::default(); + } + } + } + + // Ensure we store a focus handle in our element state if we're focusable. + // If there's an explicit focus handle we're tracking, use that. Otherwise + // create a new handle and store it in the element state, which lives for as + // as frames contain an element with this id. + if self.focusable { + if self.tracked_focus_handle.is_none() { + if let Some(element_state) = element_state.as_mut() { + self.tracked_focus_handle = Some( + element_state + .focus_handle + .get_or_insert_with(|| cx.focus_handle()) + .clone(), + ); + } + } + } + + if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() { + self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); + } else if self.base_style.overflow.x == Some(Overflow::Scroll) + || self.base_style.overflow.y == Some(Overflow::Scroll) + { + if let Some(element_state) = element_state.as_mut() { + self.scroll_offset = Some( + element_state + .scroll_offset + .get_or_insert_with(|| Rc::default()) + .clone(), + ); + } + } + + let style = self.compute_style_internal(None, element_state.as_mut(), cx); + let layout_id = f(style, cx); + (layout_id, element_state) + }, + ) + } + + /// Commit the bounds of this element according to this interactivity state's configured styles. + pub fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + content_size: Size, + cx: &mut WindowContext, + f: impl FnOnce(&Style, Point, Option, &mut WindowContext) -> R, + ) -> R { + self.content_size = content_size; + cx.with_optional_element_state::( + global_id, + |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(None, element_state.as_mut(), cx); + + if let Some(element_state) = element_state.as_ref() { + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + let clicked_state = clicked_state.borrow(); + self.active = Some(clicked_state.element); + } + + if let Some(active_tooltip) = element_state.active_tooltip.as_ref() { + if let Some(active_tooltip) = active_tooltip.borrow().as_ref() { + if let Some(tooltip) = active_tooltip.tooltip.clone() { + self.tooltip_id = Some(cx.set_tooltip(tooltip)); + } + } + } + } + + cx.with_text_style(style.text_style().cloned(), |cx| { + cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| { + let hitbox = if self.should_insert_hitbox(&style) { + Some(cx.insert_hitbox(bounds, self.occlude_mouse)) + } else { + None + }; + + let scroll_offset = self.clamp_scroll_position(bounds, &style, cx); + let result = f(&style, scroll_offset, hitbox, cx); + (result, element_state) + }) + }) + }, + ) + } + + fn should_insert_hitbox(&self, style: &Style) -> bool { + self.occlude_mouse + || style.mouse_cursor.is_some() + || self.group.is_some() + || self.scroll_offset.is_some() + || self.tracked_focus_handle.is_some() + || self.hover_style.is_some() + || self.group_hover_style.is_some() + || !self.mouse_up_listeners.is_empty() + || !self.mouse_down_listeners.is_empty() + || !self.mouse_move_listeners.is_empty() + || !self.click_listeners.is_empty() + || !self.scroll_wheel_listeners.is_empty() + || self.drag_listener.is_some() + || !self.drop_listeners.is_empty() + || self.tooltip_builder.is_some() + } + + fn clamp_scroll_position( + &mut self, + bounds: Bounds, + style: &Style, + cx: &mut WindowContext, + ) -> Point { + if let Some(scroll_offset) = self.scroll_offset.as_ref() { + if let Some(scroll_handle) = &self.tracked_scroll_handle { + scroll_handle.0.borrow_mut().overflow = style.overflow; + } + + let rem_size = cx.rem_size(); + let padding_size = size( + style + .padding + .left + .to_pixels(bounds.size.width.into(), rem_size) + + style + .padding + .right + .to_pixels(bounds.size.width.into(), rem_size), + style + .padding + .top + .to_pixels(bounds.size.height.into(), rem_size) + + style + .padding + .bottom + .to_pixels(bounds.size.height.into(), rem_size), + ); + let scroll_max = (self.content_size + padding_size - bounds.size).max(&Size::default()); + // Clamp scroll offset in case scroll max is smaller now (e.g., if children + // were removed or the bounds became larger). + let mut scroll_offset = scroll_offset.borrow_mut(); + scroll_offset.x = scroll_offset.x.clamp(-scroll_max.width, px(0.)); + scroll_offset.y = scroll_offset.y.clamp(-scroll_max.height, px(0.)); + *scroll_offset + } else { + Point::default() + } + } + + /// Paint this element according to this interactivity state's configured styles + /// and bind the element's mouse and keyboard events. + /// + /// content_size is the size of the content of the element, which may be larger than the + /// element's bounds if the element is scrollable. + /// + /// the final computed style will be passed to the provided function, along + /// with the current scroll offset + pub fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + hitbox: Option<&Hitbox>, + cx: &mut WindowContext, + f: impl FnOnce(&Style, &mut WindowContext), + ) { + self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(cx)); + cx.with_optional_element_state::( + global_id, + |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + + let style = self.compute_style_internal(hitbox, element_state.as_mut(), cx); + + #[cfg(any(feature = "test-support", test))] + if let Some(debug_selector) = &self.debug_selector { + cx.window + .next_frame + .debug_bounds + .insert(debug_selector.clone(), bounds); + } + + self.paint_hover_group_handler(cx); + + if style.visibility == Visibility::Hidden { + return ((), element_state); + } + + style.paint(bounds, cx, |cx: &mut WindowContext| { + cx.with_text_style(style.text_style().cloned(), |cx| { + cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| { + if let Some(hitbox) = hitbox { + #[cfg(debug_assertions)] + self.paint_debug_info(global_id, hitbox, &style, cx); + + if !cx.has_active_drag() { + if let Some(mouse_cursor) = style.mouse_cursor { + cx.set_cursor_style(mouse_cursor, hitbox); + } + } + + if let Some(group) = self.group.clone() { + GroupHitboxes::push(group, hitbox.id, cx); + } + + self.paint_mouse_listeners(hitbox, element_state.as_mut(), cx); + self.paint_scroll_listener(hitbox, &style, cx); + } + + self.paint_keyboard_listeners(cx); + f(&style, cx); + + if hitbox.is_some() { + if let Some(group) = self.group.as_ref() { + GroupHitboxes::pop(group, cx); + } + } + }); + }); + }); + + ((), element_state) + }, + ); + } + + #[cfg(debug_assertions)] + fn paint_debug_info( + &mut self, + global_id: Option<&GlobalElementId>, + hitbox: &Hitbox, + style: &Style, + cx: &mut WindowContext, + ) { + if global_id.is_some() + && (style.debug || style.debug_below || cx.has_global::()) + && hitbox.is_hovered(cx) + { + const FONT_SIZE: crate::Pixels = crate::Pixels(10.); + let element_id = format!("{:?}", global_id.unwrap()); + let str_len = element_id.len(); + + let render_debug_text = |cx: &mut WindowContext| { + if let Some(text) = cx + .text_system() + .shape_text( + element_id.into(), + FONT_SIZE, + &[cx.text_style().to_run(str_len)], + None, + ) + .ok() + .and_then(|mut text| text.pop()) + { + text.paint(hitbox.origin, FONT_SIZE, cx).ok(); + + let text_bounds = crate::Bounds { + origin: hitbox.origin, + size: text.size(FONT_SIZE), + }; + if self.location.is_some() + && text_bounds.contains(&cx.mouse_position()) + && cx.modifiers().secondary() + { + let secondary_held = cx.modifiers().secondary(); + cx.on_key_event({ + move |e: &crate::ModifiersChangedEvent, _phase, cx| { + if e.modifiers.secondary() != secondary_held + && text_bounds.contains(&cx.mouse_position()) + { + cx.refresh(); + } + } + }); + + let was_hovered = hitbox.is_hovered(cx); + cx.on_mouse_event({ + let hitbox = hitbox.clone(); + move |_: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + let hovered = hitbox.is_hovered(cx); + if hovered != was_hovered { + cx.refresh(); + } + } + } + }); + + cx.on_mouse_event({ + let hitbox = hitbox.clone(); + let location = self.location.unwrap(); + move |e: &crate::MouseDownEvent, phase, cx| { + if text_bounds.contains(&e.position) + && phase.capture() + && hitbox.is_hovered(cx) + { + cx.stop_propagation(); + let Ok(dir) = std::env::current_dir() else { + return; + }; + + eprintln!( + "This element was created at:\n{}:{}:{}", + dir.join(location.file()).to_string_lossy(), + location.line(), + location.column() + ); + } + } + }); + cx.paint_quad(crate::outline( + crate::Bounds { + origin: hitbox.origin + + crate::point(crate::px(0.), FONT_SIZE - px(2.)), + size: crate::Size { + width: text_bounds.size.width, + height: crate::px(1.), + }, + }, + crate::red(), + )) + } + } + }; + + cx.with_text_style( + Some(crate::TextStyleRefinement { + color: Some(crate::red()), + line_height: Some(FONT_SIZE.into()), + background_color: Some(crate::white()), + ..Default::default() + }), + render_debug_text, + ) + } + } + + fn paint_mouse_listeners( + &mut self, + hitbox: &Hitbox, + element_state: Option<&mut InteractiveElementState>, + cx: &mut WindowContext, + ) { + // If this element can be focused, register a mouse down listener + // that will automatically transfer focus when hitting the element. + // This behavior can be suppressed by using `cx.prevent_default()`. + if let Some(focus_handle) = self.tracked_focus_handle.clone() { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(cx) + && !cx.default_prevented() + { + cx.focus(&focus_handle); + // If there is a parent that is also focusable, prevent it + // from transferring focus because we already did so. + cx.prevent_default(); + } + }); + } + + for listener in self.mouse_down_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } + + for listener in self.mouse_up_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } + + for listener in self.mouse_move_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } + + for listener in self.scroll_wheel_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } + + if self.hover_style.is_some() + || self.base_style.mouse_cursor.is_some() + || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() + { + let hitbox = hitbox.clone(); + let was_hovered = hitbox.is_hovered(cx); + cx.on_mouse_event(move |_: &MouseMoveEvent, phase, cx| { + let hovered = hitbox.is_hovered(cx); + if phase == DispatchPhase::Capture && hovered != was_hovered { + cx.refresh(); + } + }); + } + + let mut drag_listener = mem::take(&mut self.drag_listener); + let drop_listeners = mem::take(&mut self.drop_listeners); + let click_listeners = mem::take(&mut self.click_listeners); + let can_drop_predicate = mem::take(&mut self.can_drop_predicate); + + if !drop_listeners.is_empty() { + let hitbox = hitbox.clone(); + cx.on_mouse_event({ + move |_: &MouseUpEvent, phase, cx| { + if let Some(drag) = &cx.active_drag { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + let drag_state_type = drag.value.as_ref().type_id(); + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); + + let mut can_drop = true; + if let Some(predicate) = &can_drop_predicate { + can_drop = predicate(drag.value.as_ref(), cx); + } + + if can_drop { + listener(drag.value.as_ref(), cx); + cx.refresh(); + cx.stop_propagation(); + } + } + } + } + } + } + }); + } + + if let Some(element_state) = element_state { + if !click_listeners.is_empty() || drag_listener.is_some() { + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + + cx.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Left + && hitbox.is_hovered(cx) + { + *pending_mouse_down.borrow_mut() = Some(event.clone()); + cx.refresh(); + } + } + }); + + cx.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + return; + } + + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if let Some(mouse_down) = pending_mouse_down.clone() { + if !cx.has_active_drag() + && (event.position - mouse_down.position).magnitude() + > DRAG_THRESHOLD + { + if let Some((drag_value, drag_listener)) = drag_listener.take() { + *clicked_state.borrow_mut() = ElementClickedState::default(); + let cursor_offset = event.position - hitbox.origin; + let drag = (drag_listener)(drag_value.as_ref(), cx); + cx.active_drag = Some(AnyDrag { + view: drag, + value: drag_value, + cursor_offset, + }); + pending_mouse_down.take(); + cx.refresh(); + cx.stop_propagation(); + } + } + } + } + }); + + cx.on_mouse_event({ + let mut captured_mouse_down = None; + let hitbox = hitbox.clone(); + move |event: &MouseUpEvent, phase, cx| match phase { + // Clear the pending mouse down during the capture phase, + // so that it happens even if another event handler stops + // propagation. + DispatchPhase::Capture => { + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if pending_mouse_down.is_some() && hitbox.is_hovered(cx) { + captured_mouse_down = pending_mouse_down.take(); + cx.refresh(); + } + } + // Fire click handlers during the bubble phase. + DispatchPhase::Bubble => { + if let Some(mouse_down) = captured_mouse_down.take() { + let mouse_click = ClickEvent { + down: mouse_down, + up: event.clone(), + }; + for listener in &click_listeners { + listener(&mouse_click, cx); + } + } + } + } + }); + } + + if let Some(hover_listener) = self.hover_listener.take() { + let hitbox = hitbox.clone(); + let was_hovered = element_state + .hover_state + .get_or_insert_with(Default::default) + .clone(); + let has_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + cx.on_mouse_event(move |_: &MouseMoveEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + let is_hovered = has_mouse_down.borrow().is_none() + && !cx.has_active_drag() + && hitbox.is_hovered(cx); + let mut was_hovered = was_hovered.borrow_mut(); + + if is_hovered != *was_hovered { + *was_hovered = is_hovered; + drop(was_hovered); + + hover_listener(&is_hovered, cx); + } + }); + } + + if let Some(tooltip_builder) = self.tooltip_builder.take() { + let tooltip_is_hoverable = tooltip_builder.hoverable; + let active_tooltip = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .clone(); + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + cx.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + let hitbox = hitbox.clone(); + let tooltip_id = self.tooltip_id; + move |_: &MouseMoveEvent, phase, cx| { + let is_hovered = + pending_mouse_down.borrow().is_none() && hitbox.is_hovered(cx); + let tooltip_is_hovered = + tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)); + if !is_hovered && (!tooltip_is_hoverable || !tooltip_is_hovered) { + if active_tooltip.borrow_mut().take().is_some() { + cx.refresh(); + } + + return; + } + + if phase != DispatchPhase::Bubble { + return; + } + + if active_tooltip.borrow().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let build_tooltip = tooltip_builder.build.clone(); + move |mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + cx.update(|cx| { + active_tooltip.borrow_mut().replace(ActiveTooltip { + tooltip: Some(AnyTooltip { + view: build_tooltip(cx), + mouse_position: cx.mouse_position(), + }), + _task: None, + }); + cx.refresh(); + }) + .ok(); + } + }); + active_tooltip.borrow_mut().replace(ActiveTooltip { + tooltip: None, + _task: Some(task), + }); + } + } + }); + + cx.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + let tooltip_id = self.tooltip_id; + move |_: &MouseDownEvent, _, cx| { + let tooltip_is_hovered = + tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)); + + if !tooltip_is_hoverable || !tooltip_is_hovered { + if active_tooltip.borrow_mut().take().is_some() { + cx.refresh(); + } + } + } + }); + + cx.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + let tooltip_id = self.tooltip_id; + move |_: &ScrollWheelEvent, _, cx| { + let tooltip_is_hovered = + tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)); + if !tooltip_is_hoverable || !tooltip_is_hovered { + if active_tooltip.borrow_mut().take().is_some() { + cx.refresh(); + } + } + } + }) + } + + let active_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + if active_state.borrow().is_clicked() { + cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Capture { + *active_state.borrow_mut() = ElementClickedState::default(); + cx.refresh(); + } + }); + } else { + let active_group_hitbox = self + .group_active_style + .as_ref() + .and_then(|group_active| GroupHitboxes::get(&group_active.group, cx)); + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && !cx.default_prevented() { + let group_hovered = active_group_hitbox + .map_or(false, |group_hitbox_id| group_hitbox_id.is_hovered(cx)); + let element_hovered = hitbox.is_hovered(cx); + if group_hovered || element_hovered { + *active_state.borrow_mut() = ElementClickedState { + group: group_hovered, + element: element_hovered, + }; + cx.refresh(); + } + } + }); + } + } + } + + fn paint_keyboard_listeners(&mut self, cx: &mut WindowContext) { + let key_down_listeners = mem::take(&mut self.key_down_listeners); + let key_up_listeners = mem::take(&mut self.key_up_listeners); + let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners); + let action_listeners = mem::take(&mut self.action_listeners); + if let Some(context) = self.key_context.clone() { + cx.set_key_context(context); + } + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + cx.set_focus_handle(focus_handle); + } + + for listener in key_down_listeners { + cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { + listener(event, phase, cx); + }) + } + + for listener in key_up_listeners { + cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { + listener(event, phase, cx); + }) + } + + for listener in modifiers_changed_listeners { + cx.on_modifiers_changed(move |event: &ModifiersChangedEvent, cx| { + listener(event, cx); + }) + } + + for (action_type, listener) in action_listeners { + cx.on_action(action_type, listener) + } + } + + fn paint_hover_group_handler(&self, cx: &mut WindowContext) { + let group_hitbox = self + .group_hover_style + .as_ref() + .and_then(|group_hover| GroupHitboxes::get(&group_hover.group, cx)); + + if let Some(group_hitbox) = group_hitbox { + let was_hovered = group_hitbox.is_hovered(cx); + cx.on_mouse_event(move |_: &MouseMoveEvent, phase, cx| { + let hovered = group_hitbox.is_hovered(cx); + if phase == DispatchPhase::Capture && hovered != was_hovered { + cx.refresh(); + } + }); + } + } + + fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut WindowContext) { + if let Some(scroll_offset) = self.scroll_offset.clone() { + let overflow = style.overflow; + let line_height = cx.line_height(); + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + let mut scroll_offset = scroll_offset.borrow_mut(); + let old_scroll_offset = *scroll_offset; + let delta = event.delta.pixel_delta(line_height); + + if overflow.x == Overflow::Scroll { + let mut delta_x = Pixels::ZERO; + if !delta.x.is_zero() { + delta_x = delta.x; + } else if overflow.y != Overflow::Scroll { + delta_x = delta.y; + } + + scroll_offset.x += delta_x; + } + + if overflow.y == Overflow::Scroll { + let mut delta_y = Pixels::ZERO; + if !delta.y.is_zero() { + delta_y = delta.y; + } else if overflow.x != Overflow::Scroll { + delta_y = delta.x; + } + + scroll_offset.y += delta_y; + } + + cx.stop_propagation(); + if *scroll_offset != old_scroll_offset { + cx.refresh(); + } + } + }); + } + } + + /// Compute the visual style for this element, based on the current bounds and the element's state. + pub fn compute_style( + &self, + global_id: Option<&GlobalElementId>, + hitbox: Option<&Hitbox>, + cx: &mut WindowContext, + ) -> Style { + cx.with_optional_element_state(global_id, |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(hitbox, element_state.as_mut(), cx); + (style, element_state) + }) + } + + /// Called from internal methods that have already called with_element_state. + fn compute_style_internal( + &self, + hitbox: Option<&Hitbox>, + element_state: Option<&mut InteractiveElementState>, + cx: &mut WindowContext, + ) -> Style { + let mut style = Style::default(); + style.refine(&self.base_style); + + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if let Some(in_focus_style) = self.in_focus_style.as_ref() { + if focus_handle.within_focused(cx) { + style.refine(in_focus_style); + } + } + + if let Some(focus_style) = self.focus_style.as_ref() { + if focus_handle.is_focused(cx) { + style.refine(focus_style); + } + } + } + + if let Some(hitbox) = hitbox { + if !cx.has_active_drag() { + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_hitbox_id) = + GroupHitboxes::get(&group_hover.group, cx.deref_mut()) + { + if group_hitbox_id.is_hovered(cx) { + style.refine(&group_hover.style); + } + } + } + + if let Some(hover_style) = self.hover_style.as_ref() { + if hitbox.is_hovered(cx) { + style.refine(hover_style); + } + } + } + + if let Some(drag) = cx.active_drag.take() { + let mut can_drop = true; + if let Some(can_drop_predicate) = &self.can_drop_predicate { + can_drop = can_drop_predicate(drag.value.as_ref(), cx); + } + + if can_drop { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_hitbox_id) = + GroupHitboxes::get(&group_drag_style.group, cx.deref_mut()) + { + if *state_type == drag.value.as_ref().type_id() + && group_hitbox_id.is_hovered(cx) + { + style.refine(&group_drag_style.style); + } + } + } + + for (state_type, build_drag_over_style) in &self.drag_over_styles { + if *state_type == drag.value.as_ref().type_id() && hitbox.is_hovered(cx) { + style.refine(&build_drag_over_style(drag.value.as_ref(), cx)); + } + } + } + + cx.active_drag = Some(drag); + } + } + + if let Some(element_state) = element_state { + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .borrow(); + if clicked_state.group { + if let Some(group) = self.group_active_style.as_ref() { + style.refine(&group.style) + } + } + + if let Some(active_style) = self.active_style.as_ref() { + if clicked_state.element { + style.refine(active_style) + } + } + } + + style + } +} + +/// The per-frame state of an interactive element. Used for tracking stateful interactions like clicks +/// and scroll offsets. +#[derive(Default)] +pub struct InteractiveElementState { + pub(crate) focus_handle: Option, + pub(crate) clicked_state: Option>>, + pub(crate) hover_state: Option>>, + pub(crate) pending_mouse_down: Option>>>, + pub(crate) scroll_offset: Option>>>, + pub(crate) active_tooltip: Option>>>, +} + +/// The current active tooltip +pub struct ActiveTooltip { + pub(crate) tooltip: Option, + pub(crate) _task: Option>, +} + +/// Whether or not the element or a group that contains it is clicked by the mouse. +#[derive(Copy, Clone, Default, Eq, PartialEq)] +pub struct ElementClickedState { + /// True if this element's group has been clicked, false otherwise + pub group: bool, + + /// True if this element has been clicked, false otherwise + pub element: bool, +} + +impl ElementClickedState { + fn is_clicked(&self) -> bool { + self.group || self.element + } +} + +#[derive(Default)] +pub(crate) struct GroupHitboxes(HashMap>); + +impl Global for GroupHitboxes {} + +impl GroupHitboxes { + pub fn get(name: &SharedString, cx: &mut AppContext) -> Option { + cx.default_global::() + .0 + .get(name) + .and_then(|bounds_stack| bounds_stack.last()) + .cloned() + } + + pub fn push(name: SharedString, hitbox_id: HitboxId, cx: &mut AppContext) { + cx.default_global::() + .0 + .entry(name) + .or_default() + .push(hitbox_id); + } + + pub fn pop(name: &SharedString, cx: &mut AppContext) { + cx.default_global::().0.get_mut(name).unwrap().pop(); + } +} + +/// A wrapper around an element that can be focused. +pub struct Focusable { + /// The element that is focusable + pub element: E, +} + +impl FocusableElement for Focusable {} + +impl InteractiveElement for Focusable +where + E: InteractiveElement, +{ + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl StatefulInteractiveElement for Focusable {} + +impl Styled for Focusable +where + E: Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } +} + +impl Element for Focusable +where + E: Element, +{ + type RequestLayoutState = E::RequestLayoutState; + type PrepaintState = E::PrepaintState; + + fn id(&self) -> Option { + self.element.id() + } + + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + self.element.request_layout(id, cx) + } + + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + bounds: Bounds, + state: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> E::PrepaintState { + self.element.prepaint(id, bounds, state, cx) + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + self.element.paint(id, bounds, request_layout, prepaint, cx) + } +} + +impl IntoElement for Focusable +where + E: IntoElement, +{ + type Element = E::Element; + + fn into_element(self) -> Self::Element { + self.element.into_element() + } +} + +impl ParentElement for Focusable +where + E: ParentElement, +{ + fn extend(&mut self, elements: impl IntoIterator) { + self.element.extend(elements) + } +} + +/// A wrapper around an element that can store state, produced after assigning an ElementId. +pub struct Stateful { + element: E, +} + +impl Styled for Stateful +where + E: Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } +} + +impl StatefulInteractiveElement for Stateful +where + E: Element, + Self: InteractiveElement, +{ +} + +impl InteractiveElement for Stateful +where + E: InteractiveElement, +{ + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl FocusableElement for Stateful {} + +impl Element for Stateful +where + E: Element, +{ + type RequestLayoutState = E::RequestLayoutState; + type PrepaintState = E::PrepaintState; + + fn id(&self) -> Option { + self.element.id() + } + + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + self.element.request_layout(id, cx) + } + + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + bounds: Bounds, + state: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> E::PrepaintState { + self.element.prepaint(id, bounds, state, cx) + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + self.element.paint(id, bounds, request_layout, prepaint, cx); + } +} + +impl IntoElement for Stateful +where + E: Element, +{ + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl ParentElement for Stateful +where + E: ParentElement, +{ + fn extend(&mut self, elements: impl IntoIterator) { + self.element.extend(elements) + } +} + +#[derive(Default)] +struct ScrollHandleState { + offset: Rc>>, + bounds: Bounds, + child_bounds: Vec>, + requested_scroll_top: Option<(usize, Pixels)>, + overflow: Point, +} + +/// A handle to the scrollable aspects of an element. +/// Used for accessing scroll state, like the current scroll offset, +/// and for mutating the scroll state, like scrolling to a specific child. +#[derive(Clone)] +pub struct ScrollHandle(Rc>); + +impl Default for ScrollHandle { + fn default() -> Self { + Self::new() + } +} + +impl ScrollHandle { + /// Construct a new scroll handle. + pub fn new() -> Self { + Self(Rc::default()) + } + + /// Get the current scroll offset. + pub fn offset(&self) -> Point { + *self.0.borrow().offset.borrow() + } + + /// Get the top child that's scrolled into view. + pub fn top_item(&self) -> usize { + let state = self.0.borrow(); + let top = state.bounds.top() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if top < bounds.top() { + Ordering::Greater + } else if top > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + + /// Return the bounds into which this child is painted + pub fn bounds(&self) -> Bounds { + self.0.borrow().bounds + } + + /// Get the bounds for a specific child. + pub fn bounds_for_item(&self, ix: usize) -> Option> { + self.0.borrow().child_bounds.get(ix).cloned() + } + + /// scroll_to_item scrolls the minimal amount to ensure that the child is + /// fully visible + pub fn scroll_to_item(&self, ix: usize) { + let state = self.0.borrow(); + + let Some(bounds) = state.child_bounds.get(ix) else { + return; + }; + + let mut scroll_offset = state.offset.borrow_mut(); + + if state.overflow.y == Overflow::Scroll { + if bounds.top() + scroll_offset.y < state.bounds.top() { + scroll_offset.y = state.bounds.top() - bounds.top(); + } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() { + scroll_offset.y = state.bounds.bottom() - bounds.bottom(); + } + } + + if state.overflow.x == Overflow::Scroll { + if bounds.left() + scroll_offset.x < state.bounds.left() { + scroll_offset.x = state.bounds.left() - bounds.left(); + } else if bounds.right() + scroll_offset.x > state.bounds.right() { + scroll_offset.x = state.bounds.right() - bounds.right(); + } + } + } + + /// Get the logical scroll top, based on a child index and a pixel offset. + pub fn logical_scroll_top(&self) -> (usize, Pixels) { + let ix = self.top_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.top() + state.offset.borrow().y - state.bounds.top(), + ) + } else { + (ix, px(0.)) + } + } + + /// Set the logical scroll top, based on a child index and a pixel offset. + pub fn set_logical_scroll_top(&self, ix: usize, px: Pixels) { + self.0.borrow_mut().requested_scroll_top = Some((ix, px)); + } +} diff --git a/crates/ming/src/elements/img.rs b/crates/ming/src/elements/img.rs new file mode 100644 index 0000000..2c8fcdc --- /dev/null +++ b/crates/ming/src/elements/img.rs @@ -0,0 +1,445 @@ +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::{ + point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element, + ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, + LayoutId, Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, + WindowContext, +}; +use futures::{AsyncReadExt, Future}; +use image::{ImageBuffer, ImageError}; +#[cfg(target_os = "macos")] +use media::core_video::CVImageBuffer; + +use http; +use thiserror::Error; +use util::ResultExt; + +/// A source of image content. +#[derive(Clone, Debug)] +pub enum ImageSource { + /// Image content will be loaded from provided URI at render time. + Uri(SharedUri), + /// Image content will be loaded from the provided file at render time. + File(Arc), + /// Cached image data + Data(Arc), + // TODO: move surface definitions into mac platform module + /// A CoreVideo image buffer + #[cfg(target_os = "macos")] + Surface(CVImageBuffer), +} + +impl From for ImageSource { + fn from(value: SharedUri) -> Self { + Self::Uri(value) + } +} + +impl From<&'static str> for ImageSource { + fn from(uri: &'static str) -> Self { + Self::Uri(uri.into()) + } +} + +impl From for ImageSource { + fn from(uri: String) -> Self { + Self::Uri(uri.into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::File(value) + } +} + +impl From for ImageSource { + fn from(value: PathBuf) -> Self { + Self::File(value.into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Data(value) + } +} + +#[cfg(target_os = "macos")] +impl From for ImageSource { + fn from(value: CVImageBuffer) -> Self { + Self::Surface(value) + } +} + +/// An image element. +pub struct Img { + interactivity: Interactivity, + source: ImageSource, + grayscale: bool, + object_fit: ObjectFit, +} + +/// Create a new image element. +pub fn img(source: impl Into) -> Img { + Img { + interactivity: Interactivity::default(), + source: source.into(), + grayscale: false, + object_fit: ObjectFit::Contain, + } +} + +/// How to fit the image into the bounds of the element. +pub enum ObjectFit { + /// The image will be stretched to fill the bounds of the element. + Fill, + /// The image will be scaled to fit within the bounds of the element. + Contain, + /// The image will be scaled to cover the bounds of the element. + Cover, + /// The image will be scaled down to fit within the bounds of the element. + ScaleDown, + /// The image will maintain its original size. + None, +} + +impl ObjectFit { + /// Get the bounds of the image within the given bounds. + pub fn get_bounds( + &self, + bounds: Bounds, + image_size: Size, + ) -> Bounds { + let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension))); + let image_ratio = image_size.width / image_size.height; + let bounds_ratio = bounds.size.width / bounds.size.height; + + let result_bounds = match self { + ObjectFit::Fill => bounds, + ObjectFit::Contain => { + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } + ObjectFit::ScaleDown => { + // Check if the image is larger than the bounds in either dimension. + if image_size.width > bounds.size.width || image_size.height > bounds.size.height { + // If the image is larger, use the same logic as Contain to scale it down. + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } else { + // If the image is smaller than or equal to the container, display it at its original size, + // centered within the container. + let original_size = size(image_size.width, image_size.height); + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - original_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - original_size.height) / 2.0, + ), + size: original_size, + } + } + } + ObjectFit::Cover => { + let new_size = if bounds_ratio > image_ratio { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + } else { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } + ObjectFit::None => Bounds { + origin: bounds.origin, + size: image_size, + }, + }; + + result_bounds + } +} + +impl Img { + /// A list of all format extensions currently supported by this img element + pub fn extensions() -> &'static [&'static str] { + // This is the list in [image::ImageFormat::from_extension] + `svg` + &[ + "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico", + "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg", + ] + } + + /// Set the image to be displayed in grayscale. + pub fn grayscale(mut self, grayscale: bool) -> Self { + self.grayscale = grayscale; + self + } + /// Set the object fit for the image. + pub fn object_fit(mut self, object_fit: ObjectFit) -> Self { + self.object_fit = object_fit; + self + } +} + +impl Element for Img { + type RequestLayoutState = (); + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self + .interactivity + .request_layout(global_id, cx, |mut style, cx| { + if let Some(data) = self.source.data(cx) { + let image_size = data.size(); + match (style.size.width, style.size.height) { + (Length::Auto, Length::Auto) => { + style.size = Size { + width: Length::Definite(DefiniteLength::Absolute( + AbsoluteLength::Pixels(px(image_size.width.0 as f32)), + )), + height: Length::Definite(DefiniteLength::Absolute( + AbsoluteLength::Pixels(px(image_size.height.0 as f32)), + )), + } + } + _ => {} + } + } + + cx.request_layout(style, []) + }); + (layout_id, ()) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Option { + self.interactivity + .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + let source = self.source.clone(); + self.interactivity + .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| { + let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size()); + + if let Some(data) = source.data(cx) { + let new_bounds = self.object_fit.get_bounds(bounds, data.size()); + cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale) + .log_err(); + } + + match source { + #[cfg(target_os = "macos")] + ImageSource::Surface(surface) => { + let size = size(surface.width().into(), surface.height().into()); + let new_bounds = self.object_fit.get_bounds(bounds, size); + // TODO: Add support for corner_radii and grayscale. + cx.paint_surface(new_bounds, surface); + } + _ => {} + } + }) + } +} + +impl IntoElement for Img { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Img { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Img { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl ImageSource { + fn data(&self, cx: &mut WindowContext) -> Option> { + match self { + ImageSource::Uri(_) | ImageSource::File(_) => { + let uri_or_path: UriOrPath = match self { + ImageSource::Uri(uri) => uri.clone().into(), + ImageSource::File(path) => path.clone().into(), + _ => unreachable!(), + }; + + cx.use_cached_asset::(&uri_or_path)?.log_err() + } + + ImageSource::Data(data) => Some(data.to_owned()), + #[cfg(target_os = "macos")] + ImageSource::Surface(_) => None, + } + } +} + +#[derive(Clone)] +enum Image {} + +impl Asset for Image { + type Source = UriOrPath; + type Output = Result, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut WindowContext, + ) -> impl Future + Send + 'static { + let client = cx.http_client(); + let scale_factor = cx.scale_factor(); + let svg_renderer = cx.svg_renderer(); + async move { + let bytes = match source.clone() { + UriOrPath::Path(uri) => fs::read(uri.as_ref())?, + UriOrPath::Uri(uri) => { + let mut response = client.get(uri.as_ref(), ().into(), true).await?; + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await?; + if !response.status().is_success() { + return Err(ImageCacheError::BadStatus { + status: response.status(), + body: String::from_utf8_lossy(&body).into_owned(), + }); + } + body + } + }; + + let data = if let Ok(format) = image::guess_format(&bytes) { + let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8(); + ImageData::new(data) + } else { + let pixmap = + svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(scale_factor))?; + + let buffer = + ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap(); + + ImageData::new(buffer) + }; + + Ok(Arc::new(data)) + } + } +} + +/// An error that can occur when interacting with the image cache. +#[derive(Debug, Error, Clone)] +pub enum ImageCacheError { + /// An error that occurred while fetching an image from a remote source. + #[error("http error: {0}")] + Client(#[from] http::Error), + /// An error that occurred while reading the image from disk. + #[error("IO error: {0}")] + Io(Arc), + /// An error that occurred while processing an image. + #[error("unexpected http status: {status}, body: {body}")] + BadStatus { + /// The HTTP status code. + status: http::StatusCode, + /// The HTTP response body. + body: String, + }, + /// An error that occurred while processing an image. + #[error("image error: {0}")] + Image(Arc), + /// An error that occurred while processing an SVG. + #[error("svg error: {0}")] + Usvg(Arc), +} + +impl From for ImageCacheError { + fn from(error: std::io::Error) -> Self { + Self::Io(Arc::new(error)) + } +} + +impl From for ImageCacheError { + fn from(error: ImageError) -> Self { + Self::Image(Arc::new(error)) + } +} + +impl From for ImageCacheError { + fn from(error: usvg::Error) -> Self { + Self::Usvg(Arc::new(error)) + } +} diff --git a/crates/ming/src/elements/list.rs b/crates/ming/src/elements/list.rs new file mode 100644 index 0000000..0d4cade --- /dev/null +++ b/crates/ming/src/elements/list.rs @@ -0,0 +1,979 @@ +//! A list element that can be used to render a large number of differently sized elements +//! efficiently. Clients of this API need to ensure that elements outside of the scrolled +//! area do not change their height for this element to function correctly. In order to minimize +//! re-renders, this element's state is stored intrusively on your own views, so that your code +//! can coordinate directly with the list element's cached state. +//! +//! If all of your elements are the same height, see [`UniformList`] for a simpler API + +use crate::{ + point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, + Element, FocusHandle, GlobalElementId, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, + Size, Style, StyleRefinement, Styled, WindowContext, +}; +use collections::VecDeque; +use refineable::Refineable as _; +use std::{cell::RefCell, ops::Range, rc::Rc}; +use sum_tree::{Bias, SumTree}; +use taffy::style::Overflow; + +/// Construct a new list element +pub fn list(state: ListState) -> List { + List { + state, + style: StyleRefinement::default(), + sizing_behavior: ListSizingBehavior::default(), + } +} + +/// A list element +pub struct List { + state: ListState, + style: StyleRefinement, + sizing_behavior: ListSizingBehavior, +} + +impl List { + /// Set the sizing behavior for the list. + pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { + self.sizing_behavior = behavior; + self + } +} + +/// The list state that views must hold on behalf of the list element. +#[derive(Clone)] +pub struct ListState(Rc>); + +struct StateInner { + last_layout_bounds: Option>, + last_padding: Option>, + render_item: Box AnyElement>, + items: SumTree, + logical_scroll_top: Option, + alignment: ListAlignment, + overdraw: Pixels, + reset: bool, + #[allow(clippy::type_complexity)] + scroll_handler: Option>, +} + +/// Whether the list is scrolling from top to bottom or bottom to top. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ListAlignment { + /// The list is scrolling from top to bottom, like most lists. + Top, + /// The list is scrolling from bottom to top, like a chat log. + Bottom, +} + +/// A scroll event that has been converted to be in terms of the list's items. +pub struct ListScrollEvent { + /// The range of items currently visible in the list, after applying the scroll event. + pub visible_range: Range, + + /// The number of items that are currently visible in the list, after applying the scroll event. + pub count: usize, + + /// Whether the list has been scrolled. + pub is_scrolled: bool, +} + +/// The sizing behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum ListSizingBehavior { + /// The list should calculate its size based on the size of its items. + Infer, + /// The list should not calculate a fixed size. + #[default] + Auto, +} + +struct LayoutItemsResponse { + max_item_width: Pixels, + scroll_top: ListOffset, + item_layouts: VecDeque, +} + +struct ItemLayout { + index: usize, + element: AnyElement, + size: Size, +} + +/// Frame state used by the [List] element after layout. +pub struct ListPrepaintState { + hitbox: Hitbox, + layout: LayoutItemsResponse, +} + +#[derive(Clone)] +enum ListItem { + Unmeasured { + focus_handle: Option, + }, + Measured { + size: Size, + focus_handle: Option, + }, +} + +impl ListItem { + fn size(&self) -> Option> { + if let ListItem::Measured { size, .. } = self { + Some(*size) + } else { + None + } + } + + fn focus_handle(&self) -> Option { + match self { + ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => { + focus_handle.clone() + } + } + } + + fn contains_focused(&self, cx: &WindowContext) -> bool { + match self { + ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => { + focus_handle + .as_ref() + .is_some_and(|handle| handle.contains_focused(cx)) + } + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct ListItemSummary { + count: usize, + rendered_count: usize, + unrendered_count: usize, + height: Pixels, + has_focus_handles: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct Count(usize); + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct RenderedCount(usize); + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct UnrenderedCount(usize); + +#[derive(Clone, Debug, Default)] +struct Height(Pixels); + +impl ListState { + /// Construct a new list state, for storage on a view. + /// + /// The overdraw parameter controls how much extra space is rendered + /// above and below the visible area. Elements within this area will + /// be measured even though they are not visible. This can help ensure + /// that the list doesn't flicker or pop in when scrolling. + pub fn new( + item_count: usize, + alignment: ListAlignment, + overdraw: Pixels, + render_item: R, + ) -> Self + where + R: 'static + FnMut(usize, &mut WindowContext) -> AnyElement, + { + let this = Self(Rc::new(RefCell::new(StateInner { + last_layout_bounds: None, + last_padding: None, + render_item: Box::new(render_item), + items: SumTree::new(), + logical_scroll_top: None, + alignment, + overdraw, + scroll_handler: None, + reset: false, + }))); + this.splice(0..0, item_count); + this + } + + /// Reset this instantiation of the list state. + /// + /// Note that this will cause scroll events to be dropped until the next paint. + pub fn reset(&self, element_count: usize) { + let old_count = { + let state = &mut *self.0.borrow_mut(); + state.reset = true; + state.logical_scroll_top = None; + state.items.summary().count + }; + + self.splice(0..old_count, element_count); + } + + /// The number of items in this list. + pub fn item_count(&self) -> usize { + self.0.borrow().items.summary().count + } + + /// Inform the list state that the items in `old_range` have been replaced + /// by `count` new items that must be recalculated. + pub fn splice(&self, old_range: Range, count: usize) { + self.splice_focusable(old_range, (0..count).map(|_| None)) + } + + /// Register with the list state that the items in `old_range` have been replaced + /// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles + /// to be supplied to properly integrate with items in the list that can be focused. If a focused item + /// is scrolled out of view, the list will continue to render it to allow keyboard interaction. + pub fn splice_focusable( + &self, + old_range: Range, + focus_handles: impl IntoIterator>, + ) { + let state = &mut *self.0.borrow_mut(); + + let mut old_items = state.items.cursor::(); + let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right, &()); + old_items.seek_forward(&Count(old_range.end), Bias::Right, &()); + + let mut spliced_count = 0; + new_items.extend( + focus_handles.into_iter().map(|focus_handle| { + spliced_count += 1; + ListItem::Unmeasured { focus_handle } + }), + &(), + ); + new_items.append(old_items.suffix(&()), &()); + drop(old_items); + state.items = new_items; + + if let Some(ListOffset { + item_ix, + offset_in_item, + }) = state.logical_scroll_top.as_mut() + { + if old_range.contains(item_ix) { + *item_ix = old_range.start; + *offset_in_item = px(0.); + } else if old_range.end <= *item_ix { + *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count; + } + } + } + + /// Set a handler that will be called when the list is scrolled. + pub fn set_scroll_handler( + &self, + handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static, + ) { + self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) + } + + /// Get the current scroll offset, in terms of the list's items. + pub fn logical_scroll_top(&self) -> ListOffset { + self.0.borrow().logical_scroll_top() + } + + /// Scroll the list to the given offset + pub fn scroll_to(&self, mut scroll_top: ListOffset) { + let state = &mut *self.0.borrow_mut(); + let item_count = state.items.summary().count; + if scroll_top.item_ix >= item_count { + scroll_top.item_ix = item_count; + scroll_top.offset_in_item = px(0.); + } + + state.logical_scroll_top = Some(scroll_top); + } + + /// Scroll the list to the given item, such that the item is fully visible. + pub fn scroll_to_reveal_item(&self, ix: usize) { + let state = &mut *self.0.borrow_mut(); + + let mut scroll_top = state.logical_scroll_top(); + let height = state + .last_layout_bounds + .map_or(px(0.), |bounds| bounds.size.height); + let padding = state.last_padding.unwrap_or_default(); + + if ix <= scroll_top.item_ix { + scroll_top.item_ix = ix; + scroll_top.offset_in_item = px(0.); + } else { + let mut cursor = state.items.cursor::(); + cursor.seek(&Count(ix + 1), Bias::Right, &()); + let bottom = cursor.start().height + padding.top; + let goal_top = px(0.).max(bottom - height + padding.bottom); + + cursor.seek(&Height(goal_top), Bias::Left, &()); + let start_ix = cursor.start().count; + let start_item_top = cursor.start().height; + + if start_ix >= scroll_top.item_ix { + scroll_top.item_ix = start_ix; + scroll_top.offset_in_item = goal_top - start_item_top; + } + } + + state.logical_scroll_top = Some(scroll_top); + } + + /// Get the bounds for the given item in window coordinates, if it's + /// been rendered. + pub fn bounds_for_item(&self, ix: usize) -> Option> { + let state = &*self.0.borrow(); + + let bounds = state.last_layout_bounds.unwrap_or_default(); + let scroll_top = state.logical_scroll_top(); + if ix < scroll_top.item_ix { + return None; + } + + let mut cursor = state.items.cursor::<(Count, Height)>(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + + let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item; + + cursor.seek_forward(&Count(ix), Bias::Right, &()); + if let Some(&ListItem::Measured { size, .. }) = cursor.item() { + let &(Count(count), Height(top)) = cursor.start(); + if count == ix { + let top = bounds.top() + top - scroll_top; + return Some(Bounds::from_corners( + point(bounds.left(), top), + point(bounds.right(), top + size.height), + )); + } + } + None + } +} + +impl StateInner { + fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range { + let mut cursor = self.items.cursor::(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + let start_y = cursor.start().height + scroll_top.offset_in_item; + cursor.seek_forward(&Height(start_y + height), Bias::Left, &()); + scroll_top.item_ix..cursor.start().count + 1 + } + + fn scroll( + &mut self, + scroll_top: &ListOffset, + height: Pixels, + delta: Point, + cx: &mut WindowContext, + ) { + // Drop scroll events after a reset, since we can't calculate + // the new logical scroll top without the item heights + if self.reset { + return; + } + + let padding = self.last_padding.unwrap_or_default(); + let scroll_max = + (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.)); + let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) + .max(px(0.)) + .min(scroll_max); + + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { + self.logical_scroll_top = None; + } else { + let mut cursor = self.items.cursor::(); + cursor.seek(&Height(new_scroll_top), Bias::Right, &()); + let item_ix = cursor.start().count; + let offset_in_item = new_scroll_top - cursor.start().height; + self.logical_scroll_top = Some(ListOffset { + item_ix, + offset_in_item, + }); + } + + if self.scroll_handler.is_some() { + let visible_range = self.visible_range(height, scroll_top); + self.scroll_handler.as_mut().unwrap()( + &ListScrollEvent { + visible_range, + count: self.items.summary().count, + is_scrolled: self.logical_scroll_top.is_some(), + }, + cx, + ); + } + + cx.refresh(); + } + + fn logical_scroll_top(&self) -> ListOffset { + self.logical_scroll_top + .unwrap_or_else(|| match self.alignment { + ListAlignment::Top => ListOffset { + item_ix: 0, + offset_in_item: px(0.), + }, + ListAlignment::Bottom => ListOffset { + item_ix: self.items.summary().count, + offset_in_item: px(0.), + }, + }) + } + + fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels { + let mut cursor = self.items.cursor::(); + cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &()); + cursor.start().height + logical_scroll_top.offset_in_item + } + + fn layout_items( + &mut self, + available_width: Option, + available_height: Pixels, + padding: &Edges, + cx: &mut WindowContext, + ) -> LayoutItemsResponse { + let old_items = self.items.clone(); + let mut measured_items = VecDeque::new(); + let mut item_layouts = VecDeque::new(); + let mut rendered_height = padding.top; + let mut max_item_width = px(0.); + let mut scroll_top = self.logical_scroll_top(); + let mut rendered_focused_item = false; + + let available_item_space = size( + available_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); + + let mut cursor = old_items.cursor::(); + + // Render items after the scroll top, including those in the trailing overdraw + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + for (ix, item) in cursor.by_ref().enumerate() { + let visible_height = rendered_height - scroll_top.offset_in_item; + if visible_height >= available_height + self.overdraw { + break; + } + + // Use the previously cached height and focus handle if available + let mut size = item.size(); + + // If we're within the visible area or the height wasn't cached, render and measure the item's element + if visible_height < available_height || size.is_none() { + let item_index = scroll_top.item_ix + ix; + let mut element = (self.render_item)(item_index, cx); + let element_size = element.layout_as_root(available_item_space, cx); + size = Some(element_size); + if visible_height < available_height { + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(cx) { + rendered_focused_item = true; + } + } + } + + let size = size.unwrap(); + rendered_height += size.height; + max_item_width = max_item_width.max(size.width); + measured_items.push_back(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } + rendered_height += padding.bottom; + + // Prepare to start walking upward from the item at the scroll top. + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + + // If the rendered items do not fill the visible region, then adjust + // the scroll top upward. + if rendered_height - scroll_top.offset_in_item < available_height { + while rendered_height < available_height { + cursor.prev(&()); + if let Some(item) = cursor.item() { + let item_index = cursor.start().0; + let mut element = (self.render_item)(item_index, cx); + let element_size = element.layout_as_root(available_item_space, cx); + let focus_handle = item.focus_handle(); + rendered_height += element_size.height; + measured_items.push_front(ListItem::Measured { + size: element_size, + focus_handle, + }); + item_layouts.push_front(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(cx) { + rendered_focused_item = true; + } + } else { + break; + } + } + + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + + match self.alignment { + ListAlignment::Top => { + scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); + self.logical_scroll_top = Some(scroll_top); + } + ListAlignment::Bottom => { + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + self.logical_scroll_top = None; + } + }; + } + + // Measure items in the leading overdraw + let mut leading_overdraw = scroll_top.offset_in_item; + while leading_overdraw < self.overdraw { + cursor.prev(&()); + if let Some(item) = cursor.item() { + let size = if let ListItem::Measured { size, .. } = item { + *size + } else { + let mut element = (self.render_item)(cursor.start().0, cx); + element.layout_as_root(available_item_space, cx) + }; + + leading_overdraw += size.height; + measured_items.push_front(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } else { + break; + } + } + + let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); + let mut cursor = old_items.cursor::(); + let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); + new_items.extend(measured_items, &()); + cursor.seek(&Count(measured_range.end), Bias::Right, &()); + new_items.append(cursor.suffix(&()), &()); + self.items = new_items; + + // If none of the visible items are focused, check if an off-screen item is focused + // and include it to be rendered after the visible items so keyboard interaction continues + // to work for it. + if !rendered_focused_item { + let mut cursor = self + .items + .filter::<_, Count>(|summary| summary.has_focus_handles); + cursor.next(&()); + while let Some(item) = cursor.item() { + if item.contains_focused(cx) { + let item_index = cursor.start().0; + let mut element = (self.render_item)(cursor.start().0, cx); + let size = element.layout_as_root(available_item_space, cx); + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size, + }); + break; + } + cursor.next(&()); + } + } + + LayoutItemsResponse { + max_item_width, + scroll_top, + item_layouts, + } + } + + fn prepaint_items( + &mut self, + bounds: Bounds, + padding: Edges, + autoscroll: bool, + cx: &mut WindowContext, + ) -> Result { + cx.transact(|cx| { + let mut layout_response = + self.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx); + + // Avoid honoring autoscroll requests from elements other than our children. + cx.take_autoscroll(); + + // Only paint the visible items, if there is actually any space for them (taking padding into account) + if bounds.size.height > padding.top + padding.bottom { + let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); + item_origin.y -= layout_response.scroll_top.offset_in_item; + for item in &mut layout_response.item_layouts { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + item.element.prepaint_at(item_origin, cx); + }); + + if let Some(autoscroll_bounds) = cx.take_autoscroll() { + if autoscroll { + if autoscroll_bounds.top() < bounds.top() { + return Err(ListOffset { + item_ix: item.index, + offset_in_item: autoscroll_bounds.top() - item_origin.y, + }); + } else if autoscroll_bounds.bottom() > bounds.bottom() { + let mut cursor = self.items.cursor::(); + cursor.seek(&Count(item.index), Bias::Right, &()); + let mut height = bounds.size.height - padding.top - padding.bottom; + + // Account for the height of the element down until the autoscroll bottom. + height -= autoscroll_bounds.bottom() - item_origin.y; + + // Keep decreasing the scroll top until we fill all the available space. + while height > Pixels::ZERO { + cursor.prev(&()); + let Some(item) = cursor.item() else { break }; + + let size = item.size().unwrap_or_else(|| { + let mut item = (self.render_item)(cursor.start().0, cx); + let item_available_size = size( + bounds.size.width.into(), + AvailableSpace::MinContent, + ); + item.layout_as_root(item_available_size, cx) + }); + height -= size.height; + } + + return Err(ListOffset { + item_ix: cursor.start().0, + offset_in_item: if height < Pixels::ZERO { + -height + } else { + Pixels::ZERO + }, + }); + } + } + } + + item_origin.y += item.size.height; + } + } else { + layout_response.item_layouts.clear(); + } + + Ok(layout_response) + }) + } +} + +impl std::fmt::Debug for ListItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unmeasured { .. } => write!(f, "Unrendered"), + Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(), + } + } +} + +/// An offset into the list's items, in terms of the item index and the number +/// of pixels off the top left of the item. +#[derive(Debug, Clone, Copy, Default)] +pub struct ListOffset { + /// The index of an item in the list + pub item_ix: usize, + /// The number of pixels to offset from the item index. + pub offset_in_item: Pixels, +} + +impl Element for List { + type RequestLayoutState = (); + type PrepaintState = ListPrepaintState; + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut crate::WindowContext, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let layout_id = match self.sizing_behavior { + ListSizingBehavior::Infer => { + let mut style = Style::default(); + style.overflow.y = Overflow::Scroll; + style.refine(&self.style); + cx.with_text_style(style.text_style().cloned(), |cx| { + let state = &mut *self.state.0.borrow_mut(); + + let available_height = if let Some(last_bounds) = state.last_layout_bounds { + last_bounds.size.height + } else { + // If we don't have the last layout bounds (first render), + // we might just use the overdraw value as the available height to layout enough items. + state.overdraw + }; + let padding = style.padding.to_pixels( + state.last_layout_bounds.unwrap_or_default().size.into(), + cx.rem_size(), + ); + + let layout_response = state.layout_items(None, available_height, &padding, cx); + let max_element_width = layout_response.max_item_width; + + let summary = state.items.summary(); + let total_height = summary.height; + + cx.request_measured_layout( + style, + move |known_dimensions, available_space, _cx| { + let width = + known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + max_element_width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(height) => total_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + total_height + } + }; + size(width, height) + }, + ) + }) + } + ListSizingBehavior::Auto => { + let mut style = Style::default(); + style.refine(&self.style); + cx.with_text_style(style.text_style().cloned(), |cx| { + cx.request_layout(style, None) + }) + } + }; + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> ListPrepaintState { + let state = &mut *self.state.0.borrow_mut(); + state.reset = false; + + let mut style = Style::default(); + style.refine(&self.style); + + let hitbox = cx.insert_hitbox(bounds, false); + + // If the width of the list has changed, invalidate all cached item heights + if state.last_layout_bounds.map_or(true, |last_bounds| { + last_bounds.size.width != bounds.size.width + }) { + let new_items = SumTree::from_iter( + state.items.iter().map(|item| ListItem::Unmeasured { + focus_handle: item.focus_handle(), + }), + &(), + ); + + state.items = new_items; + } + + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + let layout = match state.prepaint_items(bounds, padding, true, cx) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state.prepaint_items(bounds, padding, false, cx).unwrap() + } + }; + + state.last_layout_bounds = Some(bounds); + state.last_padding = Some(padding); + ListPrepaintState { hitbox, layout } + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + cx: &mut crate::WindowContext, + ) { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + for item in &mut prepaint.layout.item_layouts { + item.element.paint(cx); + } + }); + + let list_state = self.state.clone(); + let height = bounds.size.height; + let scroll_top = prepaint.layout.scroll_top; + let hitbox_id = prepaint.hitbox.id; + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) { + list_state.0.borrow_mut().scroll( + &scroll_top, + height, + event.delta.pixel_delta(px(20.)), + cx, + ) + } + }); + } +} + +impl IntoElement for List { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for List { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl sum_tree::Item for ListItem { + type Summary = ListItemSummary; + + fn summary(&self) -> Self::Summary { + match self { + ListItem::Unmeasured { focus_handle } => ListItemSummary { + count: 1, + rendered_count: 0, + unrendered_count: 1, + height: px(0.), + has_focus_handles: focus_handle.is_some(), + }, + ListItem::Measured { + size, focus_handle, .. + } => ListItemSummary { + count: 1, + rendered_count: 1, + unrendered_count: 0, + height: size.height, + has_focus_handles: focus_handle.is_some(), + }, + } + } +} + +impl sum_tree::Summary for ListItemSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.count += summary.count; + self.rendered_count += summary.rendered_count; + self.unrendered_count += summary.unrendered_count; + self.height += summary.height; + self.has_focus_handles |= summary.has_focus_handles; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.rendered_count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.unrendered_count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.height; + } +} + +impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count { + fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.count).unwrap() + } +} + +impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height { + fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.height).unwrap() + } +} + +#[cfg(test)] +mod test { + + use gpui::{ScrollDelta, ScrollWheelEvent}; + + use crate::{self as gpui, TestAppContext}; + + #[gpui::test] + fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) { + use crate::{div, list, point, px, size, Element, ListState, Styled}; + + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| { + div().h(px(10.)).w_full().into_any() + }); + + // Ensure that the list is scrolled to the top + state.scroll_to(gpui::ListOffset { + item_ix: 0, + offset_in_item: px(0.0), + }); + + // Paint + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_| { + list(state.clone()).w_full().h_full() + }); + + // Reset + state.reset(5); + + // And then receive a scroll event _before_ the next paint + cx.simulate_event(ScrollWheelEvent { + position: point(px(1.), px(1.)), + delta: ScrollDelta::Pixels(point(px(0.), px(-500.))), + ..Default::default() + }); + + // Scroll position should stay at the top of the list + assert_eq!(state.logical_scroll_top().item_ix, 0); + assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); + } +} diff --git a/crates/ming/src/elements/mod.rs b/crates/ming/src/elements/mod.rs new file mode 100644 index 0000000..88f0297 --- /dev/null +++ b/crates/ming/src/elements/mod.rs @@ -0,0 +1,21 @@ +mod anchored; +mod animation; +mod canvas; +mod deferred; +mod div; +mod img; +mod list; +mod svg; +mod text; +mod uniform_list; + +pub use anchored::*; +pub use animation::*; +pub use canvas::*; +pub use deferred::*; +pub use div::*; +pub use img::*; +pub use list::*; +pub use svg::*; +pub use text::*; +pub use uniform_list::*; diff --git a/crates/ming/src/elements/svg.rs b/crates/ming/src/elements/svg.rs new file mode 100644 index 0000000..159b9c0 --- /dev/null +++ b/crates/ming/src/elements/svg.rs @@ -0,0 +1,190 @@ +use crate::{ + geometry::Negate as _, point, px, radians, size, Bounds, Element, GlobalElementId, Hitbox, + InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, + Size, StyleRefinement, Styled, TransformationMatrix, WindowContext, +}; +use util::ResultExt; + +/// An SVG element. +pub struct Svg { + interactivity: Interactivity, + transformation: Option, + path: Option, +} + +/// Create a new SVG element. +pub fn svg() -> Svg { + Svg { + interactivity: Interactivity::default(), + transformation: None, + path: None, + } +} + +impl Svg { + /// Set the path to the SVG file for this element. + pub fn path(mut self, path: impl Into) -> Self { + self.path = Some(path.into()); + self + } + + /// Transform the SVG element with the given transformation. + /// Note that this won't effect the hitbox or layout of the element, only the rendering. + pub fn with_transformation(mut self, transformation: Transformation) -> Self { + self.transformation = Some(transformation); + self + } +} + +impl Element for Svg { + type RequestLayoutState = (); + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self + .interactivity + .request_layout(global_id, cx, |style, cx| cx.request_layout(style, None)); + (layout_id, ()) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Option { + self.interactivity + .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + cx: &mut WindowContext, + ) where + Self: Sized, + { + self.interactivity + .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| { + if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { + let transformation = self + .transformation + .as_ref() + .map(|transformation| { + transformation.into_matrix(bounds.center(), cx.scale_factor()) + }) + .unwrap_or_default(); + + cx.paint_svg(bounds, path.clone(), transformation, color) + .log_err(); + } + }) + } +} + +impl IntoElement for Svg { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Svg { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Svg { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +/// A transformation to apply to an SVG element. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Transformation { + scale: Size, + translate: Point, + rotate: Radians, +} + +impl Default for Transformation { + fn default() -> Self { + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } +} + +impl Transformation { + /// Create a new Transformation with the specified scale along each axis. + pub fn scale(scale: Size) -> Self { + Self { + scale, + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified translation. + pub fn translate(translate: Point) -> Self { + Self { + scale: size(1.0, 1.0), + translate, + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified rotation in radians. + pub fn rotate(rotate: impl Into) -> Self { + let rotate = rotate.into(); + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate, + } + } + + /// Update the scaling factor of this transformation. + pub fn with_scaling(mut self, scale: Size) -> Self { + self.scale = scale; + self + } + + /// Update the translation value of this transformation. + pub fn with_translation(mut self, translate: Point) -> Self { + self.translate = translate; + self + } + + /// Update the rotation angle of this transformation. + pub fn with_rotation(mut self, rotate: impl Into) -> Self { + self.rotate = rotate.into(); + self + } + + fn into_matrix(self, center: Point, scale_factor: f32) -> TransformationMatrix { + //Note: if you read this as a sequence of matrix mulitplications, start from the bottom + TransformationMatrix::unit() + .translate(center.scale(scale_factor) + self.translate.scale(scale_factor)) + .rotate(self.rotate) + .scale(self.scale) + .translate(center.scale(scale_factor).negate()) + } +} diff --git a/crates/ming/src/elements/text.rs b/crates/ming/src/elements/text.rs new file mode 100644 index 0000000..2adb2db --- /dev/null +++ b/crates/ming/src/elements/text.rs @@ -0,0 +1,715 @@ +use crate::{ + ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, + HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, + TOOLTIP_DELAY, +}; +use anyhow::anyhow; +use parking_lot::{Mutex, MutexGuard}; +use smallvec::SmallVec; +use std::{ + cell::{Cell, RefCell}, + mem, + ops::Range, + rc::Rc, + sync::Arc, +}; +use util::ResultExt; + +impl Element for &'static str { + type RequestLayoutState = TextLayout; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut state = TextLayout::default(); + let layout_id = state.layout(SharedString::from(*self), None, cx); + (layout_id, state) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _cx: &mut WindowContext, + ) { + text_layout.prepaint(bounds, self) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: Bounds, + text_layout: &mut TextLayout, + _: &mut (), + cx: &mut WindowContext, + ) { + text_layout.paint(self, cx) + } +} + +impl IntoElement for &'static str { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl IntoElement for String { + type Element = SharedString; + + fn into_element(self) -> Self::Element { + self.into() + } +} + +impl Element for SharedString { + type RequestLayoutState = TextLayout; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + + _id: Option<&GlobalElementId>, + + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut state = TextLayout::default(); + let layout_id = state.layout(self.clone(), None, cx); + (layout_id, state) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _cx: &mut WindowContext, + ) { + text_layout.prepaint(bounds, self.as_ref()) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + text_layout.paint(self.as_ref(), cx) + } +} + +impl IntoElement for SharedString { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// Renders text with runs of different styles. +/// +/// Callers are responsible for setting the correct style for each run. +/// For text with a uniform style, you can usually avoid calling this constructor +/// and just pass text directly. +pub struct StyledText { + text: SharedString, + runs: Option>, + layout: TextLayout, +} + +impl StyledText { + /// Construct a new styled text element from the given string. + pub fn new(text: impl Into) -> Self { + StyledText { + text: text.into(), + runs: None, + layout: TextLayout::default(), + } + } + + /// todo!() + pub fn layout(&self) -> &TextLayout { + &self.layout + } + + /// Set the styling attributes for the given text, as well as + /// as any ranges of text that have had their style customized. + pub fn with_highlights( + mut self, + default_style: &TextStyle, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Self { + let mut runs = Vec::new(); + let mut ix = 0; + for (range, highlight) in highlights { + if ix < range.start { + runs.push(default_style.clone().to_run(range.start - ix)); + } + runs.push( + default_style + .clone() + .highlight(highlight) + .to_run(range.len()), + ); + ix = range.end; + } + if ix < self.text.len() { + runs.push(default_style.to_run(self.text.len() - ix)); + } + self.runs = Some(runs); + self + } + + /// Set the text runs for this piece of text. + pub fn with_runs(mut self, runs: Vec) -> Self { + self.runs = Some(runs); + self + } +} + +impl Element for StyledText { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + + _id: Option<&GlobalElementId>, + + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.layout.layout(self.text.clone(), self.runs.take(), cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + _cx: &mut WindowContext, + ) { + self.layout.prepaint(bounds, &self.text) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _bounds: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + self.layout.paint(&self.text, cx) + } +} + +impl IntoElement for StyledText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// todo!() +#[derive(Default, Clone)] +pub struct TextLayout(Arc>>); + +struct TextLayoutInner { + lines: SmallVec<[WrappedLine; 1]>, + line_height: Pixels, + wrap_width: Option, + size: Option>, + bounds: Option>, +} + +impl TextLayout { + fn lock(&self) -> MutexGuard> { + self.0.lock() + } + + fn layout( + &mut self, + text: SharedString, + runs: Option>, + cx: &mut WindowContext, + ) -> LayoutId { + let text_style = cx.text_style(); + let font_size = text_style.font_size.to_pixels(cx.rem_size()); + let line_height = text_style + .line_height + .to_pixels(font_size.into(), cx.rem_size()); + + let runs = if let Some(runs) = runs { + runs + } else { + vec![text_style.to_run(text.len())] + }; + + let layout_id = cx.request_measured_layout(Default::default(), { + let element_state = self.clone(); + + move |known_dimensions, available_space, cx| { + let wrap_width = if text_style.white_space == WhiteSpace::Normal { + known_dimensions.width.or(match available_space.width { + crate::AvailableSpace::Definite(x) => Some(x), + _ => None, + }) + } else { + None + }; + + if let Some(text_layout) = element_state.0.lock().as_ref() { + if text_layout.size.is_some() + && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) + { + return text_layout.size.unwrap(); + } + } + + let Some(lines) = cx + .text_system() + .shape_text( + text.clone(), + font_size, + &runs, + wrap_width, // Wrap if we know the width. + ) + .log_err() + else { + element_state.lock().replace(TextLayoutInner { + lines: Default::default(), + line_height, + wrap_width, + size: Some(Size::default()), + bounds: None, + }); + return Size::default(); + }; + + let mut size: Size = Size::default(); + for line in &lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width).ceil(); + } + + element_state.lock().replace(TextLayoutInner { + lines, + line_height, + wrap_width, + size: Some(size), + bounds: None, + }); + + size + } + }); + + layout_id + } + + fn prepaint(&mut self, bounds: Bounds, text: &str) { + let mut element_state = self.lock(); + let element_state = element_state + .as_mut() + .ok_or_else(|| anyhow!("measurement has not been performed on {}", text)) + .unwrap(); + element_state.bounds = Some(bounds); + } + + fn paint(&mut self, text: &str, cx: &mut WindowContext) { + let element_state = self.lock(); + let element_state = element_state + .as_ref() + .ok_or_else(|| anyhow!("measurement has not been performed on {}", text)) + .unwrap(); + let bounds = element_state + .bounds + .ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text)) + .unwrap(); + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + for line in &element_state.lines { + line.paint(line_origin, line_height, cx).log_err(); + line_origin.y += line.size(line_height).height; + } + } + + /// todo!() + pub fn index_for_position(&self, mut position: Point) -> Result { + let element_state = self.lock(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let bounds = element_state + .bounds + .expect("prepaint has not been performed"); + + if position.y < bounds.top() { + return Err(0); + } + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + for line in &element_state.lines { + let line_bottom = line_origin.y + line.size(line_height).height; + if position.y > line_bottom { + line_origin.y = line_bottom; + line_start_ix += line.len() + 1; + } else { + let position_within_line = position - line_origin; + match line.index_for_position(position_within_line, line_height) { + Ok(index_within_line) => return Ok(line_start_ix + index_within_line), + Err(index_within_line) => return Err(line_start_ix + index_within_line), + } + } + } + + Err(line_start_ix.saturating_sub(1)) + } + + /// todo!() + pub fn position_for_index(&self, index: usize) -> Option> { + let element_state = self.lock(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let bounds = element_state + .bounds + .expect("prepaint has not been performed"); + let line_height = element_state.line_height; + + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + + for line in &element_state.lines { + let line_end_ix = line_start_ix + line.len(); + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_origin.y += line.size(line_height).height; + line_start_ix = line_end_ix + 1; + continue; + } else { + let ix_within_line = index - line_start_ix; + return Some(line_origin + line.position_for_index(ix_within_line, line_height)?); + } + } + + None + } + + /// todo!() + pub fn bounds(&self) -> Bounds { + self.0.lock().as_ref().unwrap().bounds.unwrap() + } + + /// todo!() + pub fn line_height(&self) -> Pixels { + self.0.lock().as_ref().unwrap().line_height + } +} + +/// A text element that can be interacted with. +pub struct InteractiveText { + element_id: ElementId, + text: StyledText, + click_listener: + Option], InteractiveTextClickEvent, &mut WindowContext<'_>)>>, + hover_listener: Option, MouseMoveEvent, &mut WindowContext<'_>)>>, + tooltip_builder: Option) -> Option>>, + clickable_ranges: Vec>, +} + +struct InteractiveTextClickEvent { + mouse_down_index: usize, + mouse_up_index: usize, +} + +#[doc(hidden)] +#[derive(Default)] +pub struct InteractiveTextState { + mouse_down_index: Rc>>, + hovered_index: Rc>>, + active_tooltip: Rc>>, +} + +/// InteractiveTest is a wrapper around StyledText that adds mouse interactions. +impl InteractiveText { + /// Creates a new InteractiveText from the given text. + pub fn new(id: impl Into, text: StyledText) -> Self { + Self { + element_id: id.into(), + text, + click_listener: None, + hover_listener: None, + tooltip_builder: None, + clickable_ranges: Vec::new(), + } + } + + /// on_click is called when the user clicks on one of the given ranges, passing the index of + /// the clicked range. + pub fn on_click( + mut self, + ranges: Vec>, + listener: impl Fn(usize, &mut WindowContext<'_>) + 'static, + ) -> Self { + self.click_listener = Some(Box::new(move |ranges, event, cx| { + for (range_ix, range) in ranges.iter().enumerate() { + if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) + { + listener(range_ix, cx); + } + } + })); + self.clickable_ranges = ranges; + self + } + + /// on_hover is called when the mouse moves over a character within the text, passing the + /// index of the hovered character, or None if the mouse leaves the text. + pub fn on_hover( + mut self, + listener: impl Fn(Option, MouseMoveEvent, &mut WindowContext<'_>) + 'static, + ) -> Self { + self.hover_listener = Some(Box::new(listener)); + self + } + + /// tooltip lets you specify a tooltip for a given character index in the string. + pub fn tooltip( + mut self, + builder: impl Fn(usize, &mut WindowContext<'_>) -> Option + 'static, + ) -> Self { + self.tooltip_builder = Some(Rc::new(builder)); + self + } +} + +impl Element for InteractiveText { + type RequestLayoutState = (); + type PrepaintState = Hitbox; + + fn id(&self) -> Option { + Some(self.element_id.clone()) + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + self.text.request_layout(None, cx) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + state: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Hitbox { + cx.with_optional_element_state::( + global_id, + |interactive_state, cx| { + let interactive_state = interactive_state + .map(|interactive_state| interactive_state.unwrap_or_default()); + + if let Some(interactive_state) = interactive_state.as_ref() { + if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref() + { + if let Some(tooltip) = active_tooltip.tooltip.clone() { + cx.set_tooltip(tooltip); + } + } + } + + self.text.prepaint(None, bounds, state, cx); + let hitbox = cx.insert_hitbox(bounds, false); + (hitbox, interactive_state) + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + hitbox: &mut Hitbox, + cx: &mut WindowContext, + ) { + let text_layout = self.text.layout().clone(); + cx.with_element_state::( + global_id.unwrap(), + |interactive_state, cx| { + let mut interactive_state = interactive_state.unwrap_or_default(); + if let Some(click_listener) = self.click_listener.take() { + let mouse_position = cx.mouse_position(); + if let Some(ix) = text_layout.index_for_position(mouse_position).ok() { + if self + .clickable_ranges + .iter() + .any(|range| range.contains(&ix)) + { + cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) + } + } + + let text_layout = text_layout.clone(); + let mouse_down = interactive_state.mouse_down_index.clone(); + if let Some(mouse_down_index) = mouse_down.get() { + let hitbox = hitbox.clone(); + let clickable_ranges = mem::take(&mut self.clickable_ranges); + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + if let Some(mouse_up_index) = + text_layout.index_for_position(event.position).ok() + { + click_listener( + &clickable_ranges, + InteractiveTextClickEvent { + mouse_down_index, + mouse_up_index, + }, + cx, + ) + } + + mouse_down.take(); + cx.refresh(); + } + }); + } else { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + if let Some(mouse_down_index) = + text_layout.index_for_position(event.position).ok() + { + mouse_down.set(Some(mouse_down_index)); + cx.refresh(); + } + } + }); + } + } + + cx.on_mouse_event({ + let mut hover_listener = self.hover_listener.take(); + let hitbox = hitbox.clone(); + let text_layout = text_layout.clone(); + let hovered_index = interactive_state.hovered_index.clone(); + move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + let current = hovered_index.get(); + let updated = text_layout.index_for_position(event.position).ok(); + if current != updated { + hovered_index.set(updated); + if let Some(hover_listener) = hover_listener.as_ref() { + hover_listener(updated, event.clone(), cx); + } + cx.refresh(); + } + } + } + }); + + if let Some(tooltip_builder) = self.tooltip_builder.clone() { + let hitbox = hitbox.clone(); + let active_tooltip = interactive_state.active_tooltip.clone(); + let pending_mouse_down = interactive_state.mouse_down_index.clone(); + let text_layout = text_layout.clone(); + + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + let position = text_layout.index_for_position(event.position).ok(); + let is_hovered = position.is_some() + && hitbox.is_hovered(cx) + && pending_mouse_down.get().is_none(); + if !is_hovered { + active_tooltip.take(); + return; + } + let position = position.unwrap(); + + if phase != DispatchPhase::Bubble { + return; + } + + if active_tooltip.borrow().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let tooltip_builder = tooltip_builder.clone(); + + move |mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + cx.update(|cx| { + let new_tooltip = + tooltip_builder(position, cx).map(|tooltip| { + ActiveTooltip { + tooltip: Some(AnyTooltip { + view: tooltip, + mouse_position: cx.mouse_position(), + }), + _task: None, + } + }); + *active_tooltip.borrow_mut() = new_tooltip; + cx.refresh(); + }) + .ok(); + } + }); + *active_tooltip.borrow_mut() = Some(ActiveTooltip { + tooltip: None, + _task: Some(task), + }); + } + }); + + let active_tooltip = interactive_state.active_tooltip.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { + active_tooltip.take(); + }); + } + + self.text.paint(None, bounds, &mut (), &mut (), cx); + + ((), interactive_state) + }, + ); + } +} + +impl IntoElement for InteractiveText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} diff --git a/crates/ming/src/elements/uniform_list.rs b/crates/ming/src/elements/uniform_list.rs new file mode 100644 index 0000000..d922e42 --- /dev/null +++ b/crates/ming/src/elements/uniform_list.rs @@ -0,0 +1,312 @@ +//! A scrollable list of elements with uniform height, optimized for large lists. +//! Rather than use the full taffy layout system, uniform_list simply measures +//! the first element and then lays out all remaining elements in a line based on that +//! measurement. This is much faster than the full layout system, but only works for +//! elements with uniform height. + +use crate::{ + point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId, + GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, + Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, +}; +use smallvec::SmallVec; +use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; +use taffy::style::Overflow; + +/// uniform_list provides lazy rendering for a set of items that are of uniform height. +/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, +/// uniform_list will only render the visible subset of items. +#[track_caller] +pub fn uniform_list( + view: View, + id: I, + item_count: usize, + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, +) -> UniformList +where + I: Into, + R: IntoElement, + V: Render, +{ + let id = id.into(); + let mut base_style = StyleRefinement::default(); + base_style.overflow.y = Some(Overflow::Scroll); + + let render_range = move |range, cx: &mut WindowContext| { + view.update(cx, |this, cx| { + f(this, range, cx) + .into_iter() + .map(|component| component.into_any_element()) + .collect() + }) + }; + + UniformList { + item_count, + item_to_measure_index: 0, + render_items: Box::new(render_range), + interactivity: Interactivity { + element_id: Some(id), + base_style: Box::new(base_style), + + #[cfg(debug_assertions)] + location: Some(*core::panic::Location::caller()), + + ..Default::default() + }, + scroll_handle: None, + } +} + +/// A list element for efficiently laying out and displaying a list of uniform-height elements. +pub struct UniformList { + item_count: usize, + item_to_measure_index: usize, + render_items: + Box Fn(Range, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>, + interactivity: Interactivity, + scroll_handle: Option, +} + +/// Frame state used by the [UniformList]. +pub struct UniformListFrameState { + item_size: Size, + items: SmallVec<[AnyElement; 32]>, +} + +/// A handle for controlling the scroll position of a uniform list. +/// This should be stored in your view and passed to the uniform_list on each frame. +#[derive(Clone, Default)] +pub struct UniformListScrollHandle { + base_handle: ScrollHandle, + deferred_scroll_to_item: Rc>>, +} + +impl UniformListScrollHandle { + /// Create a new scroll handle to bind to a uniform list. + pub fn new() -> Self { + Self { + base_handle: ScrollHandle::new(), + deferred_scroll_to_item: Rc::new(RefCell::new(None)), + } + } + + /// Scroll the list to the given item index. + pub fn scroll_to_item(&mut self, ix: usize) { + self.deferred_scroll_to_item.replace(Some(ix)); + } +} + +impl Styled for UniformList { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl Element for UniformList { + type RequestLayoutState = UniformListFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let max_items = self.item_count; + let item_size = self.measure_item(None, cx); + let layout_id = self + .interactivity + .request_layout(global_id, cx, |style, cx| { + cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| { + let desired_height = item_size.height * max_items; + let width = known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + item_size.width + } + }); + + let height = match available_space.height { + AvailableSpace::Definite(height) => desired_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, + }; + size(width, height) + }) + }); + + ( + layout_id, + UniformListFrameState { + item_size, + items: SmallVec::new(), + }, + ) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + frame_state: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Option { + let style = self.interactivity.compute_style(global_id, None, cx); + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.lower_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); + + let content_size = Size { + width: padded_bounds.size.width, + height: frame_state.item_size.height * self.item_count + padding.top + padding.bottom, + }; + + let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap(); + + let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height; + let shared_scroll_to_item = self + .scroll_handle + .as_mut() + .and_then(|handle| handle.deferred_scroll_to_item.take()); + + self.interactivity.prepaint( + global_id, + bounds, + content_size, + cx, + |style, mut scroll_offset, hitbox, cx| { + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top), + bounds.lower_right() - point(border.right + padding.right, border.bottom), + ); + + if self.item_count > 0 { + let content_height = + item_height * self.item_count + padding.top + padding.bottom; + let min_scroll_offset = padded_bounds.size.height - content_height; + let is_scrolled = scroll_offset.y != px(0.); + + if is_scrolled && scroll_offset.y < min_scroll_offset { + shared_scroll_offset.borrow_mut().y = min_scroll_offset; + scroll_offset.y = min_scroll_offset; + } + + if let Some(ix) = shared_scroll_to_item { + let list_height = padded_bounds.size.height; + let mut updated_scroll_offset = shared_scroll_offset.borrow_mut(); + let item_top = item_height * ix + padding.top; + let item_bottom = item_top + item_height; + let scroll_top = -updated_scroll_offset.y; + if item_top < scroll_top + padding.top { + updated_scroll_offset.y = -(item_top) + padding.top; + } else if item_bottom > scroll_top + list_height - padding.bottom { + updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom; + } + scroll_offset = *updated_scroll_offset; + } + + let first_visible_element_ix = + (-(scroll_offset.y + padding.top) / item_height).floor() as usize; + let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height) + / item_height) + .ceil() as usize; + let visible_range = first_visible_element_ix + ..cmp::min(last_visible_element_ix, self.item_count); + + let mut items = (self.render_items)(visible_range.clone(), cx); + let content_mask = ContentMask { bounds }; + cx.with_content_mask(Some(content_mask), |cx| { + for (mut item, ix) in items.into_iter().zip(visible_range) { + let item_origin = padded_bounds.origin + + point(px(0.), item_height * ix + scroll_offset.y + padding.top); + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.layout_as_root(available_space, cx); + item.prepaint_at(item_origin, cx); + frame_state.items.push(item); + } + }); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + cx: &mut WindowContext, + ) { + self.interactivity + .paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| { + for item in &mut request_layout.items { + item.paint(cx); + } + }) + } +} + +impl IntoElement for UniformList { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl UniformList { + /// Selects a specific list item for measurement. + pub fn with_width_from_item(mut self, item_index: Option) -> Self { + self.item_to_measure_index = item_index.unwrap_or(0); + self + } + + fn measure_item(&self, list_width: Option, cx: &mut WindowContext) -> Size { + if self.item_count == 0 { + return Size::default(); + } + + let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); + let mut items = (self.render_items)(item_ix..item_ix + 1, cx); + let mut item_to_measure = items.pop().unwrap(); + let available_space = size( + list_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); + item_to_measure.layout_as_root(available_space, cx) + } + + /// Track and render scroll state of this list with reference to the given scroll handle. + pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { + self.interactivity.tracked_scroll_handle = Some(handle.base_handle.clone()); + self.scroll_handle = Some(handle); + self + } +} + +impl InteractiveElement for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { + &mut self.interactivity + } +} diff --git a/crates/ming/src/executor.rs b/crates/ming/src/executor.rs new file mode 100644 index 0000000..ae2696a --- /dev/null +++ b/crates/ming/src/executor.rs @@ -0,0 +1,501 @@ +use crate::{AppContext, PlatformDispatcher}; +use tokio::sync::mpsc; +use tokio::spawn; +use std::{ + fmt::Debug, + marker::PhantomData, + mem, + num::NonZeroUsize, + pin::Pin, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, + task::{Context, Poll}, + time::Duration, +}; +use util::TryFutureExt; +use waker_fn::waker_fn; + +#[cfg(any(test, feature = "test-support"))] +use rand::rngs::StdRng; + +/// A pointer to the executor that is currently running, +/// for spawning background tasks. +#[derive(Clone)] +pub struct BackgroundExecutor { + dispatcher: Arc, +} + +/// A pointer to the executor that is currently running, +/// for spawning tasks on the main thread. +#[derive(Clone)] +pub struct ForegroundExecutor { + dispatcher: Arc, + not_send: PhantomData>, +} + +/// Task is a primitive that allows work to happen in the background. +/// +/// It implements [`Future`] so you can `.await` on it. +/// +/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows +/// the task to continue running, but with no way to return a value. +#[must_use] +#[derive(Debug)] +pub enum Task { + /// A task that is ready to return a value + Ready(Option), + + /// A task that is currently running. + Spawned(async_task::Task), +} + +impl Task { + /// Creates a new task that will resolve with the value + pub fn ready(val: T) -> Self { + Task::Ready(Some(val)) + } + + /// Detaching a task runs it to completion in the background + pub fn detach(self) { + match self { + Task::Ready(_) => {} + Task::Spawned(task) => task.detach(), + } + } +} + +impl Task> +where + T: 'static, + E: 'static + Debug, +{ + /// Run the task to completion in the background and log any + /// errors that occur. + #[track_caller] + pub fn detach_and_log_err(self, cx: &AppContext) { + let location = core::panic::Location::caller(); + cx.foreground_executor() + .spawn(self.log_tracked_err(*location)) + .detach(); + } +} + +impl Future for Task { + type Output = T; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match unsafe { self.get_unchecked_mut() } { + Task::Ready(val) => Poll::Ready(val.take().unwrap()), + Task::Spawned(task) => task.poll(cx), + } + } +} + +/// A task label is an opaque identifier that you can use to +/// refer to a task in tests. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct TaskLabel(NonZeroUsize); + +impl Default for TaskLabel { + fn default() -> Self { + Self::new() + } +} + +impl TaskLabel { + /// Construct a new task label. + pub fn new() -> Self { + static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1); + Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap()) + } +} + +type AnyLocalFuture = Pin>>; + +type AnyFuture = Pin>>; + +/// BackgroundExecutor lets you run things on background threads. +/// In production this is a thread pool with no ordering guarantees. +/// In tests this is simulated by running tasks one by one in a deterministic +/// (but arbitrary) order controlled by the `SEED` environment variable. +impl BackgroundExecutor { + #[doc(hidden)] + pub fn new(dispatcher: Arc) -> Self { + Self { dispatcher } + } + + /// Enqueues the given future to be run to completion on a background thread. + pub fn spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.spawn_internal::(Box::pin(future), None) + } + + /// Enqueues the given future to be run to completion on a background thread. + /// The given label can be used to control the priority of the task in tests. + pub fn spawn_labeled( + &self, + label: TaskLabel, + future: impl Future + Send + 'static, + ) -> Task + where + R: Send + 'static, + { + self.spawn_internal::(Box::pin(future), Some(label)) + } + + fn spawn_internal( + &self, + future: AnyFuture, + label: Option, + ) -> Task { + let dispatcher = self.dispatcher.clone(); + let (runnable, task) = + async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label)); + runnable.schedule(); + Task::Spawned(task) + } + + /// Used by the test harness to run an async test in a synchronous fashion. + #[cfg(any(test, feature = "test-support"))] + #[track_caller] + pub fn block_test(&self, future: impl Future) -> R { + if let Ok(value) = self.block_internal(false, future, None) { + value + } else { + unreachable!() + } + } + + /// Block the current thread until the given future resolves. + /// Consider using `block_with_timeout` instead. + pub fn block(&self, future: impl Future) -> R { + if let Ok(value) = self.block_internal(true, future, None) { + value + } else { + unreachable!() + } + } + + #[cfg(not(any(test, feature = "test-support")))] + pub(crate) fn block_internal( + &self, + _background_only: bool, + future: impl Future, + timeout: Option, + ) -> Result> { + use std::time::Instant; + + let mut future = Box::pin(future); + if timeout == Some(Duration::ZERO) { + return Err(future); + } + let deadline = timeout.map(|timeout| Instant::now() + timeout); + + let unparker = self.dispatcher.unparker(); + let waker = waker_fn(move || { + unparker.unpark(); + }); + let mut cx = std::task::Context::from_waker(&waker); + + loop { + match future.as_mut().poll(&mut cx) { + Poll::Ready(result) => return Ok(result), + Poll::Pending => { + let timeout = + deadline.map(|deadline| deadline.saturating_duration_since(Instant::now())); + if !self.dispatcher.park(timeout) { + if deadline.is_some_and(|deadline| deadline < Instant::now()) { + return Err(future); + } + } + } + } + } + } + + #[cfg(any(test, feature = "test-support"))] + #[track_caller] + pub(crate) fn block_internal( + &self, + background_only: bool, + future: impl Future, + timeout: Option, + ) -> Result> { + use std::sync::atomic::AtomicBool; + + let mut future = Box::pin(future); + if timeout == Some(Duration::ZERO) { + return Err(future); + } + let Some(dispatcher) = self.dispatcher.as_test() else { + return Err(future); + }; + + let mut max_ticks = if timeout.is_some() { + dispatcher.gen_block_on_ticks() + } else { + usize::MAX + }; + let unparker = self.dispatcher.unparker(); + let awoken = Arc::new(AtomicBool::new(false)); + let waker = waker_fn({ + let awoken = awoken.clone(); + move || { + awoken.store(true, SeqCst); + unparker.unpark(); + } + }); + let mut cx = std::task::Context::from_waker(&waker); + + loop { + match future.as_mut().poll(&mut cx) { + Poll::Ready(result) => return Ok(result), + Poll::Pending => { + if max_ticks == 0 { + return Err(future); + } + max_ticks -= 1; + + if !dispatcher.tick(background_only) { + if awoken.swap(false, SeqCst) { + continue; + } + + if !dispatcher.parking_allowed() { + let mut backtrace_message = String::new(); + let mut waiting_message = String::new(); + if let Some(backtrace) = dispatcher.waiting_backtrace() { + backtrace_message = + format!("\nbacktrace of waiting future:\n{:?}", backtrace); + } + if let Some(waiting_hint) = dispatcher.waiting_hint() { + waiting_message = format!("\n waiting on: {}\n", waiting_hint); + } + panic!( + "parked with nothing left to run{waiting_message}{backtrace_message}", + ) + } + self.dispatcher.park(None); + } + } + } + } + } + + /// Block the current thread until the given future resolves + /// or `duration` has elapsed. + pub fn block_with_timeout( + &self, + duration: Duration, + future: impl Future, + ) -> Result> { + self.block_internal(true, future, Some(duration)) + } + + /// Scoped lets you start a number of tasks and waits + /// for all of them to complete before returning. + pub async fn scoped<'scope, F>(&self, scheduler: F) + where + F: FnOnce(&mut Scope<'scope>), + { + let mut scope = Scope::new(self.clone()); + (scheduler)(&mut scope); + let spawned = mem::take(&mut scope.futures) + .into_iter() + .map(|f| self.spawn(f)) + .collect::>(); + for task in spawned { + task.await; + } + } + + /// Returns a task that will complete after the given duration. + /// Depending on other concurrent tasks the elapsed duration may be longer + /// than requested. + pub fn timer(&self, duration: Duration) -> Task<()> { + let (runnable, task) = async_task::spawn(async move {}, { + let dispatcher = self.dispatcher.clone(); + move |runnable| dispatcher.dispatch_after(duration, runnable) + }); + runnable.schedule(); + Task::Spawned(task) + } + + /// in tests, start_waiting lets you indicate which task is waiting (for debugging only) + #[cfg(any(test, feature = "test-support"))] + pub fn start_waiting(&self) { + self.dispatcher.as_test().unwrap().start_waiting(); + } + + /// in tests, removes the debugging data added by start_waiting + #[cfg(any(test, feature = "test-support"))] + pub fn finish_waiting(&self) { + self.dispatcher.as_test().unwrap().finish_waiting(); + } + + /// in tests, run an arbitrary number of tasks (determined by the SEED environment variable) + #[cfg(any(test, feature = "test-support"))] + pub fn simulate_random_delay(&self) -> impl Future { + self.dispatcher.as_test().unwrap().simulate_random_delay() + } + + /// in tests, indicate that a given task from `spawn_labeled` should run after everything else + #[cfg(any(test, feature = "test-support"))] + pub fn deprioritize(&self, task_label: TaskLabel) { + self.dispatcher.as_test().unwrap().deprioritize(task_label) + } + + /// in tests, move time forward. This does not run any tasks, but does make `timer`s ready. + #[cfg(any(test, feature = "test-support"))] + pub fn advance_clock(&self, duration: Duration) { + self.dispatcher.as_test().unwrap().advance_clock(duration) + } + + /// in tests, run one task. + #[cfg(any(test, feature = "test-support"))] + pub fn tick(&self) -> bool { + self.dispatcher.as_test().unwrap().tick(false) + } + + /// in tests, run all tasks that are ready to run. If after doing so + /// the test still has outstanding tasks, this will panic. (See also `allow_parking`) + #[cfg(any(test, feature = "test-support"))] + pub fn run_until_parked(&self) { + self.dispatcher.as_test().unwrap().run_until_parked() + } + + /// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks. + /// This is useful when you are integrating other (non-GPUI) futures, like disk access, that + /// do take real async time to run. + #[cfg(any(test, feature = "test-support"))] + pub fn allow_parking(&self) { + self.dispatcher.as_test().unwrap().allow_parking(); + } + + /// undoes the effect of [`allow_parking`]. + #[cfg(any(test, feature = "test-support"))] + pub fn forbid_parking(&self) { + self.dispatcher.as_test().unwrap().forbid_parking(); + } + + /// adds detail to the "parked with nothing let to run" message. + #[cfg(any(test, feature = "test-support"))] + pub fn set_waiting_hint(&self, msg: Option) { + self.dispatcher.as_test().unwrap().set_waiting_hint(msg); + } + + /// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable + #[cfg(any(test, feature = "test-support"))] + pub fn rng(&self) -> StdRng { + self.dispatcher.as_test().unwrap().rng() + } + + /// How many CPUs are available to the dispatcher. + pub fn num_cpus(&self) -> usize { + num_cpus::get() + } + + /// Whether we're on the main thread. + pub fn is_main_thread(&self) -> bool { + self.dispatcher.is_main_thread() + } + + #[cfg(any(test, feature = "test-support"))] + /// in tests, control the number of ticks that `block_with_timeout` will run before timing out. + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.dispatcher.as_test().unwrap().set_block_on_ticks(range); + } +} + +/// ForegroundExecutor runs things on the main thread. +impl ForegroundExecutor { + /// Creates a new ForegroundExecutor from the given PlatformDispatcher. + pub fn new(dispatcher: Arc) -> Self { + Self { + dispatcher, + not_send: PhantomData, + } + } + + /// Enqueues the given Task to run on the main thread at some point in the future. + pub fn spawn(&self, future: impl Future + 'static) -> Task + where + R: 'static, + { + let dispatcher = self.dispatcher.clone(); + fn inner( + dispatcher: Arc, + future: AnyLocalFuture, + ) -> Task { + let (runnable, task) = async_task::spawn_local(future, move |runnable| { + dispatcher.dispatch_on_main_thread(runnable) + }); + runnable.schedule(); + Task::Spawned(task) + } + inner::(dispatcher, Box::pin(future)) + } +} + +/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`]. +pub struct Scope<'a> { + executor: BackgroundExecutor, + futures: Vec + Send + 'static>>>, + tx: Option>, + rx: mpsc::Receiver<()>, + lifetime: PhantomData<&'a ()>, +} + +impl<'a> Scope<'a> { + fn new(executor: BackgroundExecutor) -> Self { + let (tx, rx) = mpsc::channel(1); + Self { + executor, + tx: Some(tx), + rx, + futures: Default::default(), + lifetime: PhantomData, + } + } + + /// How many CPUs are available to the dispatcher. + pub fn num_cpus(&self) -> usize { + self.executor.num_cpus() + } + + /// Spawn a future into this scope. + pub fn spawn(&mut self, f: F) + where + F: Future + Send + 'a, + { + let tx = self.tx.clone().unwrap(); + + // SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because + // dropping this `Scope` blocks until all of the futures have resolved. + let f = unsafe { + mem::transmute::< + Pin + Send + 'a>>, + Pin + Send + 'static>>, + >(Box::pin(async move { + f.await; + drop(tx); + })) + }; + self.futures.push(f); + } +} + +impl<'a> Drop for Scope<'a> { + fn drop(&mut self) { + self.tx.take().unwrap(); + + // Wait until the channel is closed, which means that all of the spawned + // futures have resolved. + self.executor.block(self.rx.next()); + } +} diff --git a/crates/ming/src/geometry.rs b/crates/ming/src/geometry.rs new file mode 100644 index 0000000..75c0fb1 --- /dev/null +++ b/crates/ming/src/geometry.rs @@ -0,0 +1,3037 @@ +//! The GPUI geometry module is a collection of types and traits that +//! can be used to describe common units, concepts, and the relationships +//! between them. + +use core::fmt::Debug; +use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign}; +use refineable::Refineable; +use serde_derive::{Deserialize, Serialize}; +use std::{ + cmp::{self, PartialOrd}, + fmt, + hash::Hash, + ops::{Add, Div, Mul, MulAssign, Sub}, +}; + +use crate::{AppContext, DisplayId}; + +/// An axis along which a measurement can be made. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Axis { + /// The y axis, or up and down + Vertical, + /// The x axis, or left and right + Horizontal, +} + +impl Axis { + /// Swap this axis to the opposite axis. + pub fn invert(self) -> Self { + match self { + Axis::Vertical => Axis::Horizontal, + Axis::Horizontal => Axis::Vertical, + } + } +} + +/// A trait for accessing the given unit along a certain axis. +pub trait Along { + /// The unit associated with this type + type Unit; + + /// Returns the unit along the given axis. + fn along(&self, axis: Axis) -> Self::Unit; + + /// Applies the given function to the unit along the given axis and returns a new value. + fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self; +} + +/// Describes a location in a 2D cartesian coordinate space. +/// +/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. +/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Point; +/// let point = Point { x: 10, y: 20 }; +/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 } +/// ``` +#[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)] +#[refineable(Debug)] +#[repr(C)] +pub struct Point { + /// The x coordinate of the point. + pub x: T, + /// The y coordinate of the point. + pub y: T, +} + +/// Constructs a new `Point` with the given x and y coordinates. +/// +/// # Arguments +/// +/// * `x` - The x coordinate of the point. +/// * `y` - The y coordinate of the point. +/// +/// # Returns +/// +/// Returns a `Point` with the specified coordinates. +/// +/// # Examples +/// +/// ``` +/// # use zed::Point; +/// let p = point(10, 20); +/// assert_eq!(p.x, 10); +/// assert_eq!(p.y, 20); +/// ``` +pub const fn point(x: T, y: T) -> Point { + Point { x, y } +} + +impl Point { + /// Creates a new `Point` with the specified `x` and `y` coordinates. + /// + /// # Arguments + /// + /// * `x` - The horizontal coordinate of the point. + /// * `y` - The vertical coordinate of the point. + /// + /// # Examples + /// + /// ``` + /// let p = Point::new(10, 20); + /// assert_eq!(p.x, 10); + /// assert_eq!(p.y, 20); + /// ``` + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } + + /// Transforms the point to a `Point` by applying the given function to both coordinates. + /// + /// This method allows for converting a `Point` to a `Point` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `x` + /// and `y` coordinates, resulting in a new point of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p = Point { x: 3, y: 4 }; + /// let p_float = p.map(|coord| coord as f32); + /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); + /// ``` + pub fn map(&self, f: impl Fn(T) -> U) -> Point { + Point { + x: f(self.x.clone()), + y: f(self.y.clone()), + } + } +} + +impl Along for Point { + type Unit = T; + + fn along(&self, axis: Axis) -> T { + match axis { + Axis::Horizontal => self.x.clone(), + Axis::Vertical => self.y.clone(), + } + } + + fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point { + match axis { + Axis::Horizontal => Point { + x: f(self.x.clone()), + y: self.y.clone(), + }, + Axis::Vertical => Point { + x: self.x.clone(), + y: f(self.y.clone()), + }, + } + } +} + +impl Negate for Point { + fn negate(self) -> Self { + self.map(Negate::negate) + } +} + +impl Point { + /// Scales the point by a given factor, which is typically derived from the resolution + /// of a target display to ensure proper sizing of UI elements. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to both the x and y coordinates. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Point, Pixels, ScaledPixels}; + /// let p = Point { x: Pixels(10.0), y: Pixels(20.0) }; + /// let scaled_p = p.scale(1.5); + /// assert_eq!(scaled_p, Point { x: ScaledPixels(15.0), y: ScaledPixels(30.0) }); + /// ``` + pub fn scale(&self, factor: f32) -> Point { + Point { + x: self.x.scale(factor), + y: self.y.scale(factor), + } + } + + /// Calculates the Euclidean distance from the origin (0, 0) to this point. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// # use zed::Pixels; + /// let p = Point { x: Pixels(3.0), y: Pixels(4.0) }; + /// assert_eq!(p.magnitude(), 5.0); + /// ``` + pub fn magnitude(&self) -> f64 { + ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt() + } +} + +impl Mul for Point +where + T: Mul + Clone + Default + Debug, + Rhs: Clone + Debug, +{ + type Output = Point; + + fn mul(self, rhs: Rhs) -> Self::Output { + Point { + x: self.x * rhs.clone(), + y: self.y * rhs, + } + } +} + +impl MulAssign for Point +where + T: Clone + Mul + Default + Debug, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.x = self.x.clone() * rhs.clone(); + self.y = self.y.clone() * rhs; + } +} + +impl Div for Point +where + T: Div + Clone + Default + Debug, + S: Clone, +{ + type Output = Self; + + fn div(self, rhs: S) -> Self::Output { + Self { + x: self.x / rhs.clone(), + y: self.y / rhs, + } + } +} + +impl Point +where + T: PartialOrd + Clone + Default + Debug, +{ + /// Returns a new point with the maximum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let max_point = p1.max(&p2); + /// assert_eq!(max_point, Point { x: 5, y: 7 }); + /// ``` + pub fn max(&self, other: &Self) -> Self { + Point { + x: if self.x > other.x { + self.x.clone() + } else { + other.x.clone() + }, + y: if self.y > other.y { + self.y.clone() + } else { + other.y.clone() + }, + } + } + + /// Returns a new point with the minimum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let min_point = p1.min(&p2); + /// assert_eq!(min_point, Point { x: 3, y: 2 }); + /// ``` + pub fn min(&self, other: &Self) -> Self { + Point { + x: if self.x <= other.x { + self.x.clone() + } else { + other.x.clone() + }, + y: if self.y <= other.y { + self.y.clone() + } else { + other.y.clone() + }, + } + } + + /// Clamps the point to a specified range. + /// + /// Given a minimum point and a maximum point, this method constrains the current point + /// such that its coordinates do not exceed the range defined by the minimum and maximum points. + /// If the current point's coordinates are less than the minimum, they are set to the minimum. + /// If they are greater than the maximum, they are set to the maximum. + /// + /// # Arguments + /// + /// * `min` - A reference to a `Point` representing the minimum allowable coordinates. + /// * `max` - A reference to a `Point` representing the maximum allowable coordinates. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p = Point { x: 10, y: 20 }; + /// let min = Point { x: 0, y: 5 }; + /// let max = Point { x: 15, y: 25 }; + /// let clamped_p = p.clamp(&min, &max); + /// assert_eq!(clamped_p, Point { x: 10, y: 20 }); + /// + /// let p_out_of_bounds = Point { x: -5, y: 30 }; + /// let clamped_p_out_of_bounds = p_out_of_bounds.clamp(&min, &max); + /// assert_eq!(clamped_p_out_of_bounds, Point { x: 0, y: 25 }); + /// ``` + pub fn clamp(&self, min: &Self, max: &Self) -> Self { + self.max(min).min(max) + } +} + +impl Clone for Point { + fn clone(&self) -> Self { + Self { + x: self.x.clone(), + y: self.y.clone(), + } + } +} + +/// A structure representing a two-dimensional size with width and height in a given unit. +/// +/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. +/// It is commonly used to specify dimensions for elements in a UI, such as a window or element. +#[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)] +#[refineable(Debug)] +#[repr(C)] +pub struct Size { + /// The width component of the size. + pub width: T, + /// The height component of the size. + pub height: T, +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: Pixels(size.width.0 as f32), + height: Pixels(size.height.0 as f32), + } + } +} + +/// Constructs a new `Size` with the provided width and height. +/// +/// # Arguments +/// +/// * `width` - The width component of the `Size`. +/// * `height` - The height component of the `Size`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Size; +/// let my_size = size(10, 20); +/// assert_eq!(my_size.width, 10); +/// assert_eq!(my_size.height, 20); +/// ``` +pub const fn size(width: T, height: T) -> Size +where + T: Clone + Default + Debug, +{ + Size { width, height } +} + +impl Size +where + T: Clone + Default + Debug, +{ + /// Applies a function to the width and height of the size, producing a new `Size`. + /// + /// This method allows for converting a `Size` to a `Size` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `width` + /// and `height`, resulting in a new size of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Size; + /// let my_size = Size { width: 10, height: 20 }; + /// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5); + /// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 }); + /// ``` + pub fn map(&self, f: impl Fn(T) -> U) -> Size + where + U: Clone + Default + Debug, + { + Size { + width: f(self.width.clone()), + height: f(self.height.clone()), + } + } +} + +impl Size +where + T: Clone + Default + Debug + Half, +{ + /// Compute the center point of the size.g + pub fn center(&self) -> Point { + Point { + x: self.width.half(), + y: self.height.half(), + } + } +} + +impl Size { + /// Scales the size by a given factor. + /// + /// This method multiplies both the width and height by the provided scaling factor, + /// resulting in a new `Size` that is proportionally larger or smaller + /// depending on the factor. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the width and height. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Size, Pixels, ScaledPixels}; + /// let size = Size { width: Pixels(100.0), height: Pixels(50.0) }; + /// let scaled_size = size.scale(2.0); + /// assert_eq!(scaled_size, Size { width: ScaledPixels(200.0), height: ScaledPixels(100.0) }); + /// ``` + pub fn scale(&self, factor: f32) -> Size { + Size { + width: self.width.scale(factor), + height: self.height.scale(factor), + } + } +} + +impl Along for Size +where + T: Clone + Default + Debug, +{ + type Unit = T; + + fn along(&self, axis: Axis) -> T { + match axis { + Axis::Horizontal => self.width.clone(), + Axis::Vertical => self.height.clone(), + } + } + + /// Returns the value of this size along the given axis. + fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self { + match axis { + Axis::Horizontal => Size { + width: f(self.width.clone()), + height: self.height.clone(), + }, + Axis::Vertical => Size { + width: self.width.clone(), + height: f(self.height.clone()), + }, + } + } +} + +impl Size +where + T: PartialOrd + Clone + Default + Debug, +{ + /// Returns a new `Size` with the maximum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let max_size = size1.max(&size2); + /// assert_eq!(max_size, Size { width: 50, height: 40 }); + /// ``` + pub fn max(&self, other: &Self) -> Self { + Size { + width: if self.width >= other.width { + self.width.clone() + } else { + other.width.clone() + }, + height: if self.height >= other.height { + self.height.clone() + } else { + other.height.clone() + }, + } + } + /// Returns a new `Size` with the minimum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let min_size = size1.min(&size2); + /// assert_eq!(min_size, Size { width: 30, height: 20 }); + /// ``` + pub fn min(&self, other: &Self) -> Self { + Size { + width: if self.width >= other.width { + other.width.clone() + } else { + self.width.clone() + }, + height: if self.height >= other.height { + other.height.clone() + } else { + self.height.clone() + }, + } + } +} + +impl Sub for Size +where + T: Sub + Clone + Default + Debug, +{ + type Output = Size; + + fn sub(self, rhs: Self) -> Self::Output { + Size { + width: self.width - rhs.width, + height: self.height - rhs.height, + } + } +} + +impl Add for Size +where + T: Add + Clone + Default + Debug, +{ + type Output = Size; + + fn add(self, rhs: Self) -> Self::Output { + Size { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + +impl Mul for Size +where + T: Mul + Clone + Default + Debug, + Rhs: Clone + Default + Debug, +{ + type Output = Size; + + fn mul(self, rhs: Rhs) -> Self::Output { + Size { + width: self.width * rhs.clone(), + height: self.height * rhs, + } + } +} + +impl MulAssign for Size +where + T: Mul + Clone + Default + Debug, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.width = self.width.clone() * rhs.clone(); + self.height = self.height.clone() * rhs; + } +} + +impl Eq for Size where T: Eq + Default + Debug + Clone {} + +impl Debug for Size +where + T: Clone + Default + Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) + } +} + +impl From> for Size { + fn from(point: Point) -> Self { + Self { + width: point.x, + height: point.y, + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: DevicePixels(size.width.0 as i32), + height: DevicePixels(size.height.0 as i32), + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl Size { + /// Returns a `Size` with both width and height set to fill the available space. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Definite(DefiniteLength::Fraction(1.0))`, + /// which represents 100% of the available space in both dimensions. + /// + /// # Returns + /// + /// A `Size` that will fill the available space when used in a layout. + pub fn full() -> Self { + Self { + width: relative(1.).into(), + height: relative(1.).into(), + } + } +} + +impl Size { + /// Returns a `Size` with both width and height set to `auto`, which allows the layout engine to determine the size. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Auto`, + /// indicating that their size should be computed based on the layout context, such as the content size or + /// available space. + /// + /// # Returns + /// + /// A `Size` with width and height set to `Length::Auto`. + pub fn auto() -> Self { + Self { + width: Length::Auto, + height: Length::Auto, + } + } +} + +/// Represents a rectangular area in a 2D space with an origin point and a size. +/// +/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system. +/// The origin is represented as a `Point` which defines the upper-left corner of the rectangle, +/// and the size is represented as a `Size` which defines the width and height of the rectangle. +/// +/// # Examples +/// +/// ``` +/// # use zed::{Bounds, Point, Size}; +/// let origin = Point { x: 0, y: 0 }; +/// let size = Size { width: 10, height: 20 }; +/// let bounds = Bounds::new(origin, size); +/// +/// assert_eq!(bounds.origin, origin); +/// assert_eq!(bounds.size, size); +/// ``` +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] +#[refineable(Debug)] +#[repr(C)] +pub struct Bounds { + /// The origin point of this area. + pub origin: Point, + /// The size of the rectangle. + pub size: Size, +} + +impl Bounds { + /// Generate a centered bounds for the given display or primary display if none is provided + pub fn centered( + display_id: Option, + size: impl Into>, + cx: &mut AppContext, + ) -> Self { + let display = display_id + .and_then(|id| cx.find_display(id)) + .or_else(|| cx.primary_display()); + + let size = size.into(); + display + .map(|display| { + let center = display.bounds().center(); + Bounds { + origin: point(center.x - size.width / 2, center.y - size.height / 2), + size, + } + }) + .unwrap_or_else(|| Bounds { + origin: point(DevicePixels(0), DevicePixels(0)), + size, + }) + } + + /// Generate maximized bounds for the given display or primary display if none is provided + pub fn maximized(display_id: Option, cx: &mut AppContext) -> Self { + let display = display_id + .and_then(|id| cx.find_display(id)) + .or_else(|| cx.primary_display()); + + display + .map(|display| display.bounds()) + .unwrap_or_else(|| Bounds { + origin: point(DevicePixels(0), DevicePixels(0)), + size: size(DevicePixels(1024), DevicePixels(768)), + }) + } +} + +impl Bounds +where + T: Clone + Debug + Sub + Default, +{ + /// Constructs a `Bounds` from two corner points: the upper-left and lower-right corners. + /// + /// This function calculates the origin and size of the `Bounds` based on the provided corner points. + /// The origin is set to the upper-left corner, and the size is determined by the difference between + /// the x and y coordinates of the lower-right and upper-left points. + /// + /// # Arguments + /// + /// * `upper_left` - A `Point` representing the upper-left corner of the rectangle. + /// * `lower_right` - A `Point` representing the lower-right corner of the rectangle. + /// + /// # Returns + /// + /// Returns a `Bounds` that encompasses the area defined by the two corner points. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point}; + /// let upper_left = Point { x: 0, y: 0 }; + /// let lower_right = Point { x: 10, y: 10 }; + /// let bounds = Bounds::from_corners(upper_left, lower_right); + /// + /// assert_eq!(bounds.origin, upper_left); + /// assert_eq!(bounds.size.width, 10); + /// assert_eq!(bounds.size.height, 10); + /// ``` + pub fn from_corners(upper_left: Point, lower_right: Point) -> Self { + let origin = Point { + x: upper_left.x.clone(), + y: upper_left.y.clone(), + }; + let size = Size { + width: lower_right.x - upper_left.x, + height: lower_right.y - upper_left.y, + }; + Bounds { origin, size } + } + + /// Creates a new `Bounds` with the specified origin and size. + /// + /// # Arguments + /// + /// * `origin` - A `Point` representing the origin of the bounds. + /// * `size` - A `Size` representing the size of the bounds. + /// + /// # Returns + /// + /// Returns a `Bounds` that has the given origin and size. + pub fn new(origin: Point, size: Size) -> Self { + Bounds { origin, size } + } +} + +impl Bounds +where + T: Clone + Debug + PartialOrd + Add + Sub + Default + Half, +{ + /// Checks if this `Bounds` intersects with another `Bounds`. + /// + /// Two `Bounds` instances intersect if they overlap in the 2D space they occupy. + /// This method checks if there is any overlapping area between the two bounds. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to check for intersection with. + /// + /// # Returns + /// + /// Returns `true` if there is any intersection between the two bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds3 = Bounds { + /// origin: Point { x: 20, y: 20 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// + /// assert_eq!(bounds1.intersects(&bounds2), true); // Overlapping bounds + /// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds + /// ``` + pub fn intersects(&self, other: &Bounds) -> bool { + let my_lower_right = self.lower_right(); + let their_lower_right = other.lower_right(); + + self.origin.x < their_lower_right.x + && my_lower_right.x > other.origin.x + && self.origin.y < their_lower_right.y + && my_lower_right.y > other.origin.y + } + + /// Dilates the bounds by a specified amount in all directions. + /// + /// This method expands the bounds by the given `amount`, increasing the size + /// and adjusting the origin so that the bounds grow outwards equally in all directions. + /// The resulting bounds will have its width and height increased by twice the `amount` + /// (since it grows in both directions), and the origin will be moved by `-amount` + /// in both the x and y directions. + /// + /// # Arguments + /// + /// * `amount` - The amount by which to dilate the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let mut bounds = Bounds { + /// origin: Point { x: 10, y: 10 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// bounds.dilate(5); + /// assert_eq!(bounds, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` + pub fn dilate(&mut self, amount: T) { + self.origin.x = self.origin.x.clone() - amount.clone(); + self.origin.y = self.origin.y.clone() - amount.clone(); + let double_amount = amount.clone() + amount; + self.size.width = self.size.width.clone() + double_amount.clone(); + self.size.height = self.size.height.clone() + double_amount; + } + + /// Returns the center point of the bounds. + /// + /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height + /// of the bounds, respectively. The center is represented as a `Point` where `T` is the type of the + /// coordinate system. + /// + /// # Returns + /// + /// A `Point` representing the center of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let center = bounds.center(); + /// assert_eq!(center, Point { x: 5, y: 10 }); + /// ``` + pub fn center(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone().half(), + y: self.origin.y.clone() + self.size.height.clone().half(), + } + } + + /// Calculates the half perimeter of a rectangle defined by the bounds. + /// + /// The half perimeter is calculated as the sum of the width and the height of the rectangle. + /// This method is generic over the type `T` which must implement the `Sub` trait to allow + /// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait + /// to sum the width and height for the half perimeter. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let half_perimeter = bounds.half_perimeter(); + /// assert_eq!(half_perimeter, 30); + /// ``` + pub fn half_perimeter(&self) -> T { + self.size.width.clone() + self.size.height.clone() + } +} + +impl + Sub> Bounds { + /// Calculates the intersection of two `Bounds` objects. + /// + /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, + /// the resulting `Bounds` will have a size with width and height of zero. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to intersect with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the intersection area. If there is no intersection, + /// the returned `Bounds` will have a size with width and height of zero. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let intersection = bounds1.intersect(&bounds2); + /// + /// assert_eq!(intersection, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 5, height: 5 }, + /// }); + /// ``` + pub fn intersect(&self, other: &Self) -> Self { + let upper_left = self.origin.max(&other.origin); + let lower_right = self.lower_right().min(&other.lower_right()); + Self::from_corners(upper_left, lower_right) + } + + /// Computes the union of two `Bounds`. + /// + /// This method calculates the smallest `Bounds` that contains both the current `Bounds` and the `other` `Bounds`. + /// The resulting `Bounds` will have an origin that is the minimum of the origins of the two `Bounds`, + /// and a size that encompasses the furthest extents of both `Bounds`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to create a union with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the union of the two `Bounds`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 15, height: 15 }, + /// }; + /// let union_bounds = bounds1.union(&bounds2); + /// + /// assert_eq!(union_bounds, Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` + pub fn union(&self, other: &Self) -> Self { + let top_left = self.origin.min(&other.origin); + let bottom_right = self.lower_right().max(&other.lower_right()); + Bounds::from_corners(top_left, bottom_right) + } +} + +impl Mul for Bounds +where + T: Mul + Clone + Default + Debug, + Point: Mul>, + Rhs: Clone + Default + Debug, +{ + type Output = Bounds; + + fn mul(self, rhs: Rhs) -> Self::Output { + Bounds { + origin: self.origin * rhs.clone(), + size: self.size * rhs, + } + } +} + +impl MulAssign for Bounds +where + T: Mul + Clone + Default + Debug, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.origin *= rhs.clone(); + self.size *= rhs; + } +} + +impl Div for Bounds +where + Size: Div>, + T: Div + Default + Clone + Debug, + S: Clone, +{ + type Output = Self; + + fn div(self, rhs: S) -> Self { + Self { + origin: self.origin / rhs.clone(), + size: self.size / rhs, + } + } +} + +impl Bounds +where + T: Add + Clone + Default + Debug, +{ + /// Returns the top edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the top edge of the bounds. + pub fn top(&self) -> T { + self.origin.y.clone() + } + + /// Returns the bottom edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the bottom edge of the bounds. + pub fn bottom(&self) -> T { + self.origin.y.clone() + self.size.height.clone() + } + + /// Returns the left edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the left edge of the bounds. + pub fn left(&self) -> T { + self.origin.x.clone() + } + + /// Returns the right edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the right edge of the bounds. + pub fn right(&self) -> T { + self.origin.x.clone() + self.size.width.clone() + } + + /// Returns the upper-right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the upper-right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let upper_right = bounds.upper_right(); + /// assert_eq!(upper_right, Point { x: 10, y: 0 }); + /// ``` + pub fn upper_right(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone(), + y: self.origin.y.clone(), + } + } + + /// Returns the lower-right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the lower-right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let lower_right = bounds.lower_right(); + /// assert_eq!(lower_right, Point { x: 10, y: 20 }); + /// ``` + pub fn lower_right(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone(), + y: self.origin.y.clone() + self.size.height.clone(), + } + } + + /// Returns the lower-left corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the lower-left corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let lower_left = bounds.lower_left(); + /// assert_eq!(lower_left, Point { x: 0, y: 20 }); + /// ``` + pub fn lower_left(&self) -> Point { + Point { + x: self.origin.x.clone(), + y: self.origin.y.clone() + self.size.height.clone(), + } + } +} + +impl Bounds +where + T: Add + PartialOrd + Clone + Default + Debug, +{ + /// Checks if the given point is within the bounds. + /// + /// This method determines whether a point lies inside the rectangle defined by the bounds, + /// including the edges. The point is considered inside if its x-coordinate is greater than + /// or equal to the left edge and less than or equal to the right edge, and its y-coordinate + /// is greater than or equal to the top edge and less than or equal to the bottom edge of the bounds. + /// + /// # Arguments + /// + /// * `point` - A reference to a `Point` that represents the point to check. + /// + /// # Returns + /// + /// Returns `true` if the point is within the bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Point, Bounds}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let inside_point = Point { x: 5, y: 5 }; + /// let outside_point = Point { x: 15, y: 15 }; + /// + /// assert!(bounds.contains_point(&inside_point)); + /// assert!(!bounds.contains_point(&outside_point)); + /// ``` + pub fn contains(&self, point: &Point) -> bool { + point.x >= self.origin.x + && point.x <= self.origin.x.clone() + self.size.width.clone() + && point.y >= self.origin.y + && point.y <= self.origin.y.clone() + self.size.height.clone() + } + + /// Applies a function to the origin and size of the bounds, producing a new `Bounds`. + /// + /// This method allows for converting a `Bounds` to a `Bounds` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to the `origin` and + /// `size` fields, resulting in new bounds of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Bounds` with the origin and size mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map(|value| value as f64 * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); + /// ``` + pub fn map(&self, f: impl Fn(T) -> U) -> Bounds + where + U: Clone + Default + Debug, + { + Bounds { + origin: self.origin.map(&f), + size: self.size.map(f), + } + } + + /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map_origin(|value| value * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }); + /// ``` + pub fn map_origin(self, f: impl Fn(Point) -> Point) -> Bounds { + Bounds { + origin: f(self.origin), + size: self.size, + } + } +} + +/// Checks if the bounds represent an empty area. +/// +/// # Returns +/// +/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. +impl Bounds { + /// Checks if the bounds represent an empty area. + /// + /// # Returns + /// + /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. + pub fn is_empty(&self) -> bool { + self.size.width <= T::default() || self.size.height <= T::default() + } +} + +impl Bounds { + /// Scales the bounds by a given factor, typically used to adjust for display scaling. + /// + /// This method multiplies the origin and size of the bounds by the provided scaling factor, + /// resulting in a new `Bounds` that is proportionally larger or smaller + /// depending on the scaling factor. This can be used to ensure that the bounds are properly + /// scaled for different display densities. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the origin and size, typically the display's scaling factor. + /// + /// # Returns + /// + /// Returns a new `Bounds` that represents the scaled bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size, Pixels}; + /// let bounds = Bounds { + /// origin: Point { x: Pixels(10.0), y: Pixels(20.0) }, + /// size: Size { width: Pixels(30.0), height: Pixels(40.0) }, + /// }; + /// let display_scale_factor = 2.0; + /// let scaled_bounds = bounds.scale(display_scale_factor); + /// assert_eq!(scaled_bounds, Bounds { + /// origin: Point { x: ScaledPixels(20.0), y: ScaledPixels(40.0) }, + /// size: Size { width: ScaledPixels(60.0), height: ScaledPixels(80.0) }, + /// }); + /// ``` + pub fn scale(&self, factor: f32) -> Bounds { + Bounds { + origin: self.origin.scale(factor), + size: self.size.scale(factor), + } + } +} + +impl Copy for Bounds {} + +/// Represents the edges of a box in a 2D space, such as padding or margin. +/// +/// Each field represents the size of the edge on one side of the box: `top`, `right`, `bottom`, and `left`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Edges; +/// let edges = Edges { +/// top: 10.0, +/// right: 20.0, +/// bottom: 30.0, +/// left: 40.0, +/// }; +/// +/// assert_eq!(edges.top, 10.0); +/// assert_eq!(edges.right, 20.0); +/// assert_eq!(edges.bottom, 30.0); +/// assert_eq!(edges.left, 40.0); +/// ``` +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] +#[refineable(Debug)] +#[repr(C)] +pub struct Edges { + /// The size of the top edge. + pub top: T, + /// The size of the right edge. + pub right: T, + /// The size of the bottom edge. + pub bottom: T, + /// The size of the left edge. + pub left: T, +} + +impl Mul for Edges +where + T: Mul + Clone + Default + Debug, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self { + top: self.top.clone() * rhs.top, + right: self.right.clone() * rhs.right, + bottom: self.bottom.clone() * rhs.bottom, + left: self.left.clone() * rhs.left, + } + } +} + +impl MulAssign for Edges +where + T: Mul + Clone + Default + Debug, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.top = self.top.clone() * rhs.clone(); + self.right = self.right.clone() * rhs.clone(); + self.bottom = self.bottom.clone() * rhs.clone(); + self.left = self.left.clone() * rhs; + } +} + +impl Copy for Edges {} + +impl Edges { + /// Constructs `Edges` where all sides are set to the same specified value. + /// + /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform edges around a box, + /// such as padding or margin with the same size on all sides. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four sides of the edges. + /// + /// # Returns + /// + /// An `Edges` instance with all sides set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let uniform_edges = Edges::all(10.0); + /// assert_eq!(uniform_edges.top, 10.0); + /// assert_eq!(uniform_edges.right, 10.0); + /// assert_eq!(uniform_edges.bottom, 10.0); + /// assert_eq!(uniform_edges.left, 10.0); + /// ``` + pub fn all(value: T) -> Self { + Self { + top: value.clone(), + right: value.clone(), + bottom: value.clone(), + left: value, + } + } + + /// Applies a function to each field of the `Edges`, producing a new `Edges`. + /// + /// This method allows for converting an `Edges` to an `Edges` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top`, `right`, `bottom`, `left`), resulting in new edges of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Edges` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 }; + /// let edges_float = edges.map(|&value| value as f32 * 1.1); + /// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 }); + /// ``` + pub fn map(&self, f: impl Fn(&T) -> U) -> Edges + where + U: Clone + Default + Debug, + { + Edges { + top: f(&self.top), + right: f(&self.right), + bottom: f(&self.bottom), + left: f(&self.left), + } + } + + /// Checks if any of the edges satisfy a given predicate. + /// + /// This method applies a predicate function to each field of the `Edges` and returns `true` if any field satisfies the predicate. + /// + /// # Arguments + /// + /// * `predicate` - A closure that takes a reference to a value of type `T` and returns a `bool`. + /// + /// # Returns + /// + /// Returns `true` if the predicate returns `true` for any of the edge values, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let edges = Edges { + /// top: 10, + /// right: 0, + /// bottom: 5, + /// left: 0, + /// }; + /// + /// assert!(edges.any(|value| *value == 0)); + /// assert!(edges.any(|value| *value > 0)); + /// assert!(!edges.any(|value| *value > 10)); + /// ``` + pub fn any bool>(&self, predicate: F) -> bool { + predicate(&self.top) + || predicate(&self.right) + || predicate(&self.bottom) + || predicate(&self.left) + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to `auto`, which is a special value that allows the layout engine to automatically determine the size of the edges. + /// + /// This is typically used in layout contexts where the exact size of the edges is not important, or when the size should be calculated based on the content or container. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to `Length::Auto`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let auto_edges = Edges::auto(); + /// assert_eq!(auto_edges.top, Length::Auto); + /// assert_eq!(auto_edges.right, Length::Auto); + /// assert_eq!(auto_edges.bottom, Length::Auto); + /// assert_eq!(auto_edges.left, Length::Auto); + /// ``` + pub fn auto() -> Self { + Self { + top: Length::Auto, + right: Length::Auto, + bottom: Length::Auto, + left: Length::Auto, + } + } + + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.bottom, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.left, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.right, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.bottom, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.left, DefiniteLength::from(zed::px(0.))); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } + + /// Converts the `DefiniteLength` to `Pixels` based on the parent size and the REM size. + /// + /// This method allows for a `DefiniteLength` value to be converted into pixels, taking into account + /// the size of the parent element (for percentage-based lengths) and the size of a rem unit (for rem-based lengths). + /// + /// # Arguments + /// + /// * `parent_size` - `Size` representing the size of the parent element. + /// * `rem_size` - `Pixels` representing the size of one REM unit. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, DefiniteLength, px, AbsoluteLength, Size}; + /// let edges = Edges { + /// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))), + /// right: DefiniteLength::Fraction(0.5), + /// bottom: DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))), + /// left: DefiniteLength::Fraction(0.25), + /// }; + /// let parent_size = Size { + /// width: AbsoluteLength::Pixels(px(200.0)), + /// height: AbsoluteLength::Pixels(px(100.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(parent_size, rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Absolute length in pixels + /// assert_eq!(edges_in_pixels.right, px(100.0)); // 50% of parent width + /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems + /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width + /// ``` + pub fn to_pixels(&self, parent_size: Size, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(parent_size.height, rem_size), + right: self.right.to_pixels(parent_size.width, rem_size), + bottom: self.bottom.to_pixels(parent_size.height, rem_size), + left: self.left.to_pixels(parent_size.width, rem_size), + } + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.bottom, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.left, AbsoluteLength::Pixels(Pixels(0.0))); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } + + /// Converts the `AbsoluteLength` to `Pixels` based on the `rem_size`. + /// + /// If the `AbsoluteLength` is already in pixels, it simply returns the corresponding `Pixels` value. + /// If the `AbsoluteLength` is in rems, it multiplies the number of rems by the `rem_size` to convert it to pixels. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem unit in pixels. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, AbsoluteLength, Pixels, px}; + /// let edges = Edges { + /// top: AbsoluteLength::Pixels(px(10.0)), + /// right: AbsoluteLength::Rems(rems(1.0)), + /// bottom: AbsoluteLength::Pixels(px(20.0)), + /// left: AbsoluteLength::Rems(rems(2.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.right, px(16.0)); // 1 rem converted to pixels + /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels + /// ``` + pub fn to_pixels(&self, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(rem_size), + right: self.right.to_pixels(rem_size), + bottom: self.bottom.to_pixels(rem_size), + left: self.left.to_pixels(rem_size), + } + } +} + +impl Edges { + /// Scales the `Edges` by a given factor, returning `Edges`. + /// + /// This method is typically used for adjusting the edge sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each edge. + /// + /// # Returns + /// + /// Returns a new `Edges` where each edge is the result of scaling the original edge by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, Pixels}; + /// let edges = Edges { + /// top: Pixels(10.0), + /// right: Pixels(20.0), + /// bottom: Pixels(30.0), + /// left: Pixels(40.0), + /// }; + /// let scaled_edges = edges.scale(2.0); + /// assert_eq!(scaled_edges.top, ScaledPixels(20.0)); + /// assert_eq!(scaled_edges.right, ScaledPixels(40.0)); + /// assert_eq!(scaled_edges.bottom, ScaledPixels(60.0)); + /// assert_eq!(scaled_edges.left, ScaledPixels(80.0)); + /// ``` + pub fn scale(&self, factor: f32) -> Edges { + Edges { + top: self.top.scale(factor), + right: self.right.scale(factor), + bottom: self.bottom.scale(factor), + left: self.left.scale(factor), + } + } + + /// Returns the maximum value of any edge. + /// + /// # Returns + /// + /// The maximum `Pixels` value among all four edges. + pub fn max(&self) -> Pixels { + self.top.max(self.right).max(self.bottom).max(self.left) + } +} + +impl From for Edges { + fn from(val: f32) -> Self { + Edges { + top: val.into(), + right: val.into(), + bottom: val.into(), + left: val.into(), + } + } +} + +/// Represents the corners of a box in a 2D space, such as border radius. +/// +/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] +#[refineable(Debug)] +#[repr(C)] +pub struct Corners { + /// The value associated with the top left corner. + pub top_left: T, + /// The value associated with the top right corner. + pub top_right: T, + /// The value associated with the bottom right corner. + pub bottom_right: T, + /// The value associated with the bottom left corner. + pub bottom_left: T, +} + +impl Corners +where + T: Clone + Default + Debug, +{ + /// Constructs `Corners` where all sides are set to the same specified value. + /// + /// This function creates a `Corners` instance with the `top_left`, `top_right`, `bottom_right`, and `bottom_left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform corners around a box, + /// such as a uniform border radius on a rectangle. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four corners. + /// + /// # Returns + /// + /// An `Corners` instance with all corners set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use zed::Corners; + /// let uniform_corners = Corners::all(5.0); + /// assert_eq!(uniform_corners.top_left, 5.0); + /// assert_eq!(uniform_corners.top_right, 5.0); + /// assert_eq!(uniform_corners.bottom_right, 5.0); + /// assert_eq!(uniform_corners.bottom_left, 5.0); + /// ``` + pub fn all(value: T) -> Self { + Self { + top_left: value.clone(), + top_right: value.clone(), + bottom_right: value.clone(), + bottom_left: value, + } + } +} + +impl Corners { + /// Converts the `AbsoluteLength` to `Pixels` based on the provided size and rem size, ensuring the resulting + /// `Pixels` do not exceed half of the maximum of the provided size's width and height. + /// + /// This method is particularly useful when dealing with corner radii, where the radius in pixels should not + /// exceed half the size of the box it applies to, to avoid the corners overlapping. + /// + /// # Arguments + /// + /// * `size` - The `Size` against which the maximum allowable radius is determined. + /// * `rem_size` - The size of one REM unit in pixels, used for conversion if the `AbsoluteLength` is in REMs. + /// + /// # Returns + /// + /// Returns a `Corners` instance with each corner's length converted to pixels and clamped to the + /// maximum allowable radius based on the provided size. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, AbsoluteLength, Pixels, Size}; + /// let corners = Corners { + /// top_left: AbsoluteLength::Pixels(Pixels(15.0)), + /// top_right: AbsoluteLength::Rems(Rems(1.0)), + /// bottom_right: AbsoluteLength::Pixels(Pixels(20.0)), + /// bottom_left: AbsoluteLength::Rems(Rems(2.0)), + /// }; + /// let size = Size { width: Pixels(100.0), height: Pixels(50.0) }; + /// let rem_size = Pixels(16.0); + /// let corners_in_pixels = corners.to_pixels(size, rem_size); + /// + /// // The resulting corners should not exceed half the size of the smallest dimension (50.0 / 2.0 = 25.0). + /// assert_eq!(corners_in_pixels.top_left, Pixels(15.0)); + /// assert_eq!(corners_in_pixels.top_right, Pixels(16.0)); // 1 rem converted to pixels + /// assert_eq!(corners_in_pixels.bottom_right, Pixels(20.0).min(Pixels(25.0))); // Clamped to 25.0 + /// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0).min(Pixels(25.0))); // 2 rems converted to pixels and clamped + /// ``` + pub fn to_pixels(&self, size: Size, rem_size: Pixels) -> Corners { + let max = size.width.max(size.height) / 2.; + Corners { + top_left: self.top_left.to_pixels(rem_size).min(max), + top_right: self.top_right.to_pixels(rem_size).min(max), + bottom_right: self.bottom_right.to_pixels(rem_size).min(max), + bottom_left: self.bottom_left.to_pixels(rem_size).min(max), + } + } +} + +impl Corners { + /// Scales the `Corners` by a given factor, returning `Corners`. + /// + /// This method is typically used for adjusting the corner sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each corner. + /// + /// # Returns + /// + /// Returns a new `Corners` where each corner is the result of scaling the original corner by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, Pixels}; + /// let corners = Corners { + /// top_left: Pixels(10.0), + /// top_right: Pixels(20.0), + /// bottom_right: Pixels(30.0), + /// bottom_left: Pixels(40.0), + /// }; + /// let scaled_corners = corners.scale(2.0); + /// assert_eq!(scaled_corners.top_left, ScaledPixels(20.0)); + /// assert_eq!(scaled_corners.top_right, ScaledPixels(40.0)); + /// assert_eq!(scaled_corners.bottom_right, ScaledPixels(60.0)); + /// assert_eq!(scaled_corners.bottom_left, ScaledPixels(80.0)); + /// ``` + pub fn scale(&self, factor: f32) -> Corners { + Corners { + top_left: self.top_left.scale(factor), + top_right: self.top_right.scale(factor), + bottom_right: self.bottom_right.scale(factor), + bottom_left: self.bottom_left.scale(factor), + } + } + + /// Returns the maximum value of any corner. + /// + /// # Returns + /// + /// The maximum `Pixels` value among all four corners. + pub fn max(&self) -> Pixels { + self.top_left + .max(self.top_right) + .max(self.bottom_right) + .max(self.bottom_left) + } +} + +impl Corners { + /// Applies a function to each field of the `Corners`, producing a new `Corners`. + /// + /// This method allows for converting a `Corners` to a `Corners` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top_left`, `top_right`, `bottom_right`, `bottom_left`), resulting in new corners of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Corners` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, Pixels}; + /// let corners = Corners { + /// top_left: Pixels(10.0), + /// top_right: Pixels(20.0), + /// bottom_right: Pixels(30.0), + /// bottom_left: Pixels(40.0), + /// }; + /// let corners_in_rems = corners.map(|&px| Rems(px.0 / 16.0)); + /// assert_eq!(corners_in_rems, Corners { + /// top_left: Rems(0.625), + /// top_right: Rems(1.25), + /// bottom_right: Rems(1.875), + /// bottom_left: Rems(2.5), + /// }); + /// ``` + pub fn map(&self, f: impl Fn(&T) -> U) -> Corners + where + U: Clone + Default + Debug, + { + Corners { + top_left: f(&self.top_left), + top_right: f(&self.top_right), + bottom_right: f(&self.bottom_right), + bottom_left: f(&self.bottom_left), + } + } +} + +impl Mul for Corners +where + T: Mul + Clone + Default + Debug, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self { + top_left: self.top_left.clone() * rhs.top_left, + top_right: self.top_right.clone() * rhs.top_right, + bottom_right: self.bottom_right.clone() * rhs.bottom_right, + bottom_left: self.bottom_left.clone() * rhs.bottom_left, + } + } +} + +impl MulAssign for Corners +where + T: Mul + Clone + Default + Debug, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.top_left = self.top_left.clone() * rhs.clone(); + self.top_right = self.top_right.clone() * rhs.clone(); + self.bottom_right = self.bottom_right.clone() * rhs.clone(); + self.bottom_left = self.bottom_left.clone() * rhs; + } +} + +impl Copy for Corners where T: Copy + Clone + Default + Debug {} + +impl From for Corners { + fn from(val: f32) -> Self { + Corners { + top_left: val.into(), + top_right: val.into(), + bottom_right: val.into(), + bottom_left: val.into(), + } + } +} + +impl From for Corners { + fn from(val: Pixels) -> Self { + Corners { + top_left: val, + top_right: val, + bottom_right: val, + bottom_left: val, + } + } +} + +/// Represents an angle in Radians +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Radians(pub f32); + +/// Create a `Radian` from a raw value +pub fn radians(value: f32) -> Radians { + Radians(value) +} + +/// A type representing a percentage value. +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Percentage(pub f32); + +/// Generate a `Radian` from a percentage of a full circle. +pub fn percentage(value: f32) -> Percentage { + debug_assert!( + value >= 0.0 && value <= 1.0, + "Percentage must be between 0 and 1" + ); + Percentage(value) +} + +impl From for Radians { + fn from(value: Percentage) -> Self { + radians(value.0 * std::f32::consts::PI * 2.0) + } +} + +/// Represents a length in pixels, the base unit of measurement in the UI framework. +/// +/// `Pixels` is a value type that represents an absolute length in pixels, which is used +/// for specifying sizes, positions, and distances in the UI. It is the fundamental unit +/// of measurement for all visual elements and layout calculations. +/// +/// The inner value is an `f32`, allowing for sub-pixel precision which can be useful for +/// anti-aliasing and animations. However, when applied to actual pixel grids, the value +/// is typically rounded to the nearest integer. +/// +/// # Examples +/// +/// ``` +/// use zed::Pixels; +/// +/// // Define a length of 10 pixels +/// let length = Pixels(10.0); +/// +/// // Define a length and scale it by a factor of 2 +/// let scaled_length = length.scale(2.0); +/// assert_eq!(scaled_length, Pixels(20.0)); +/// ``` +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, +)] +#[repr(transparent)] +pub struct Pixels(pub f32); + +impl std::ops::Div for Pixels { + type Output = f32; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} + +impl std::ops::DivAssign for Pixels { + fn div_assign(&mut self, rhs: Self) { + *self = Self(self.0 / rhs.0); + } +} + +impl std::ops::RemAssign for Pixels { + fn rem_assign(&mut self, rhs: Self) { + self.0 %= rhs.0; + } +} + +impl std::ops::Rem for Pixels { + type Output = Self; + + fn rem(self, rhs: Self) -> Self { + Self(self.0 % rhs.0) + } +} + +impl Mul for Pixels { + type Output = Pixels; + + fn mul(self, other: f32) -> Pixels { + Pixels(self.0 * other) + } +} + +impl Mul for Pixels { + type Output = Pixels; + + fn mul(self, other: usize) -> Pixels { + Pixels(self.0 * other as f32) + } +} + +impl Mul for f32 { + type Output = Pixels; + + fn mul(self, rhs: Pixels) -> Self::Output { + Pixels(self * rhs.0) + } +} + +impl MulAssign for Pixels { + fn mul_assign(&mut self, other: f32) { + self.0 *= other; + } +} + +impl Pixels { + /// Represents zero pixels. + pub const ZERO: Pixels = Pixels(0.0); + /// The maximum value that can be represented by `Pixels`. + pub const MAX: Pixels = Pixels(f32::MAX); + + /// Floors the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the floored value. + pub fn floor(&self) -> Self { + Self(self.0.floor()) + } + + /// Rounds the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the rounded value. + pub fn round(&self) -> Self { + Self(self.0.round()) + } + + /// Returns the ceiling of the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the ceiling value. + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } + + /// Scales the `Pixels` value by a given factor, producing `ScaledPixels`. + /// + /// This method is used when adjusting pixel values for display scaling factors, + /// such as high DPI (dots per inch) or Retina displays, where the pixel density is higher and + /// thus requires scaling to maintain visual consistency and readability. + /// + /// The resulting `ScaledPixels` represent the scaled value which can be used for rendering + /// calculations where display scaling is considered. + pub fn scale(&self, factor: f32) -> ScaledPixels { + ScaledPixels(self.0 * factor) + } + + /// Raises the `Pixels` value to a given power. + /// + /// # Arguments + /// + /// * `exponent` - The exponent to raise the `Pixels` value by. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the value raised to the given exponent. + pub fn pow(&self, exponent: f32) -> Self { + Self(self.0.powf(exponent)) + } + + /// Returns the absolute value of the `Pixels`. + /// + /// # Returns + /// + /// A new `Pixels` instance with the absolute value of the original `Pixels`. + pub fn abs(&self) -> Self { + Self(self.0.abs()) + } +} + +impl Mul for Pixels { + type Output = Pixels; + + fn mul(self, rhs: Pixels) -> Self::Output { + Pixels(self.0 * rhs.0) + } +} + +impl Eq for Pixels {} + +impl PartialOrd for Pixels { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Pixels { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.0.total_cmp(&other.0) + } +} + +impl std::hash::Hash for Pixels { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + +impl From for Pixels { + fn from(pixels: f64) -> Self { + Pixels(pixels as f32) + } +} + +impl From for Pixels { + fn from(pixels: f32) -> Self { + Pixels(pixels) + } +} + +impl Debug for Pixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} px", self.0) + } +} + +impl From for f32 { + fn from(pixels: Pixels) -> Self { + pixels.0 + } +} + +impl From<&Pixels> for f32 { + fn from(pixels: &Pixels) -> Self { + pixels.0 + } +} + +impl From for f64 { + fn from(pixels: Pixels) -> Self { + pixels.0 as f64 + } +} + +impl From for u32 { + fn from(pixels: Pixels) -> Self { + pixels.0 as u32 + } +} + +impl From for Pixels { + fn from(pixels: u32) -> Self { + Pixels(pixels as f32) + } +} + +impl From for usize { + fn from(pixels: Pixels) -> Self { + pixels.0 as usize + } +} + +impl From for Pixels { + fn from(pixels: usize) -> Self { + Pixels(pixels as f32) + } +} + +/// Represents physical pixels on the display. +/// +/// `DevicePixels` is a unit of measurement that refers to the actual pixels on a device's screen. +/// This type is used when precise pixel manipulation is required, such as rendering graphics or +/// interfacing with hardware that operates on the pixel level. Unlike logical pixels that may be +/// affected by the device's scale factor, `DevicePixels` always correspond to real pixels on the +/// display. +#[derive( + Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign, +)] +#[repr(transparent)] +pub struct DevicePixels(pub(crate) i32); + +impl DevicePixels { + /// Converts the `DevicePixels` value to the number of bytes needed to represent it in memory. + /// + /// This function is useful when working with graphical data that needs to be stored in a buffer, + /// such as images or framebuffers, where each pixel may be represented by a specific number of bytes. + /// + /// # Arguments + /// + /// * `bytes_per_pixel` - The number of bytes used to represent a single pixel. + /// + /// # Returns + /// + /// The number of bytes required to represent the `DevicePixels` value in memory. + /// + /// # Examples + /// + /// ``` + /// # use zed::DevicePixels; + /// let pixels = DevicePixels(10); // 10 device pixels + /// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA) + /// let total_bytes = pixels.to_bytes(bytes_per_pixel); + /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes + /// ``` + pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 { + self.0 as u32 * bytes_per_pixel as u32 + } +} + +impl fmt::Debug for DevicePixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} px (device)", self.0) + } +} + +impl From for i32 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 + } +} + +impl From for DevicePixels { + fn from(device_pixels: i32) -> Self { + DevicePixels(device_pixels) + } +} + +impl From for DevicePixels { + fn from(device_pixels: u32) -> Self { + DevicePixels(device_pixels as i32) + } +} + +impl From for u32 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as u32 + } +} + +impl From for u64 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as u64 + } +} + +impl From for DevicePixels { + fn from(device_pixels: u64) -> Self { + DevicePixels(device_pixels as i32) + } +} + +impl From for usize { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as usize + } +} + +impl From for DevicePixels { + fn from(device_pixels: usize) -> Self { + DevicePixels(device_pixels as i32) + } +} + +/// Represents scaled pixels that take into account the device's scale factor. +/// +/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices +/// with different pixel densities. When a device has a higher scale factor (such as Retina displays), +/// a single logical pixel may correspond to multiple physical pixels. By using `ScaledPixels`, +/// dimensions and positions can be specified in a way that scales appropriately across different +/// display resolutions. +#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] +#[repr(transparent)] +pub struct ScaledPixels(pub(crate) f32); + +impl ScaledPixels { + /// Floors the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the floored value. + pub fn floor(&self) -> Self { + Self(self.0.floor()) + } + + /// Rounds the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the rounded value. + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } +} + +impl Eq for ScaledPixels {} + +impl Debug for ScaledPixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} px (scaled)", self.0) + } +} + +impl From for DevicePixels { + fn from(scaled: ScaledPixels) -> Self { + DevicePixels(scaled.0.ceil() as i32) + } +} + +impl From for ScaledPixels { + fn from(device: DevicePixels) -> Self { + ScaledPixels(device.0 as f32) + } +} + +impl From for f64 { + fn from(scaled_pixels: ScaledPixels) -> Self { + scaled_pixels.0 as f64 + } +} + +/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`WindowContext::set_rem_size`][set_rem_size]. +/// +/// Rems are used for defining lengths that are scalable and consistent across different UI elements. +/// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), +/// making it a flexible unit that adapts to the user's text size preferences. In this framework, `rems` serve a similar +/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. +/// +/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. +/// +/// [set_rem_size]: crate::WindowContext::set_rem_size +#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg, PartialEq)] +pub struct Rems(pub f32); + +impl Rems { + /// Convert this Rem value to pixels. + pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { + *self * rem_size + } +} + +impl Mul for Rems { + type Output = Pixels; + + fn mul(self, other: Pixels) -> Pixels { + Pixels(self.0 * other.0) + } +} + +impl Debug for Rems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} rem", self.0) + } +} + +/// Represents an absolute length in pixels or rems. +/// +/// `AbsoluteLength` can be either a fixed number of pixels, which is an absolute measurement not +/// affected by the current font size, or a number of rems, which is relative to the font size of +/// the root element. It is used for specifying dimensions that are either independent of or +/// related to the typographic scale. +#[derive(Clone, Copy, Debug, Neg, PartialEq)] +pub enum AbsoluteLength { + /// A length in pixels. + Pixels(Pixels), + /// A length in rems. + Rems(Rems), +} + +impl AbsoluteLength { + /// Checks if the absolute length is zero. + pub fn is_zero(&self) -> bool { + match self { + AbsoluteLength::Pixels(px) => px.0 == 0.0, + AbsoluteLength::Rems(rems) => rems.0 == 0.0, + } + } +} + +impl From for AbsoluteLength { + fn from(pixels: Pixels) -> Self { + AbsoluteLength::Pixels(pixels) + } +} + +impl From for AbsoluteLength { + fn from(rems: Rems) -> Self { + AbsoluteLength::Rems(rems) + } +} + +impl AbsoluteLength { + /// Converts an `AbsoluteLength` to `Pixels` based on a given `rem_size`. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem in pixels. + /// + /// # Returns + /// + /// Returns the `AbsoluteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{AbsoluteLength, Pixels}; + /// let length_in_pixels = AbsoluteLength::Pixels(Pixels(42.0)); + /// let length_in_rems = AbsoluteLength::Rems(Rems(2.0)); + /// let rem_size = Pixels(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0)); + /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0)); + /// ``` + pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { + match self { + AbsoluteLength::Pixels(pixels) => *pixels, + AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), + } + } +} + +impl Default for AbsoluteLength { + fn default() -> Self { + px(0.).into() + } +} + +/// A non-auto length that can be defined in pixels, rems, or percent of parent. +/// +/// This enum represents lengths that have a specific value, as opposed to lengths that are automatically +/// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a +/// fraction of the parent's size. +#[derive(Clone, Copy, Neg, PartialEq)] +pub enum DefiniteLength { + /// An absolute length specified in pixels or rems. + Absolute(AbsoluteLength), + /// A relative length specified as a fraction of the parent's size, between 0 and 1. + Fraction(f32), +} + +impl DefiniteLength { + /// Converts the `DefiniteLength` to `Pixels` based on a given `base_size` and `rem_size`. + /// + /// If the `DefiniteLength` is an absolute length, it will be directly converted to `Pixels`. + /// If it is a fraction, the fraction will be multiplied by the `base_size` to get the length in pixels. + /// + /// # Arguments + /// + /// * `base_size` - The base size in `AbsoluteLength` to which the fraction will be applied. + /// * `rem_size` - The size of one rem in pixels, used to convert rems to pixels. + /// + /// # Returns + /// + /// Returns the `DefiniteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{DefiniteLength, AbsoluteLength, Pixels, px, rems}; + /// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))); + /// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))); + /// let length_as_fraction = DefiniteLength::Fraction(0.5); + /// let base_size = AbsoluteLength::Pixels(px(100.0)); + /// let rem_size = px(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(base_size, rem_size), Pixels(42.0)); + /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0)); + /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0)); + /// ``` + pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { + match self { + DefiniteLength::Absolute(size) => size.to_pixels(rem_size), + DefiniteLength::Fraction(fraction) => match base_size { + AbsoluteLength::Pixels(px) => px * *fraction, + AbsoluteLength::Rems(rems) => rems * rem_size * *fraction, + }, + } + } +} + +impl Debug for DefiniteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DefiniteLength::Absolute(length) => Debug::fmt(length, f), + DefiniteLength::Fraction(fract) => write!(f, "{}%", (fract * 100.0) as i32), + } + } +} + +impl From for DefiniteLength { + fn from(pixels: Pixels) -> Self { + Self::Absolute(pixels.into()) + } +} + +impl From for DefiniteLength { + fn from(rems: Rems) -> Self { + Self::Absolute(rems.into()) + } +} + +impl From for DefiniteLength { + fn from(length: AbsoluteLength) -> Self { + Self::Absolute(length) + } +} + +impl Default for DefiniteLength { + fn default() -> Self { + Self::Absolute(AbsoluteLength::default()) + } +} + +/// A length that can be defined in pixels, rems, percent of parent, or auto. +#[derive(Clone, Copy)] +pub enum Length { + /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. + Definite(DefiniteLength), + /// An automatic length that is determined by the context in which it is used. + Auto, +} + +impl Debug for Length { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Length::Definite(definite_length) => write!(f, "{:?}", definite_length), + Length::Auto => write!(f, "auto"), + } + } +} + +/// Constructs a `DefiniteLength` representing a relative fraction of a parent size. +/// +/// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension. +/// The fraction should be a floating-point number between 0.0 and 1.0, where 1.0 represents 100% of the parent's size. +/// +/// # Arguments +/// +/// * `fraction` - The fraction of the parent's size, between 0.0 and 1.0. +/// +/// # Returns +/// +/// A `DefiniteLength` representing the relative length as a fraction of the parent's size. +pub fn relative(fraction: f32) -> DefiniteLength { + DefiniteLength::Fraction(fraction) +} + +/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`. +pub fn phi() -> DefiniteLength { + relative(1.618_034) +} + +/// Constructs a `Rems` value representing a length in rems. +/// +/// # Arguments +/// +/// * `rems` - The number of rems for the length. +/// +/// # Returns +/// +/// A `Rems` representing the specified number of rems. +pub fn rems(rems: f32) -> Rems { + Rems(rems) +} + +/// Constructs a `Pixels` value representing a length in pixels. +/// +/// # Arguments +/// +/// * `pixels` - The number of pixels for the length. +/// +/// # Returns +/// +/// A `Pixels` representing the specified number of pixels. +pub const fn px(pixels: f32) -> Pixels { + Pixels(pixels) +} + +/// Returns a `Length` representing an automatic length. +/// +/// The `auto` length is often used in layout calculations where the length should be determined +/// by the layout context itself rather than being explicitly set. This is commonly used in CSS +/// for properties like `width`, `height`, `margin`, `padding`, etc., where `auto` can be used +/// to instruct the layout engine to calculate the size based on other factors like the size of the +/// container or the intrinsic size of the content. +/// +/// # Returns +/// +/// A `Length` variant set to `Auto`. +pub fn auto() -> Length { + Length::Auto +} + +impl From for Length { + fn from(pixels: Pixels) -> Self { + Self::Definite(pixels.into()) + } +} + +impl From for Length { + fn from(rems: Rems) -> Self { + Self::Definite(rems.into()) + } +} + +impl From for Length { + fn from(length: DefiniteLength) -> Self { + Self::Definite(length) + } +} + +impl From for Length { + fn from(length: AbsoluteLength) -> Self { + Self::Definite(length.into()) + } +} + +impl Default for Length { + fn default() -> Self { + Self::Definite(DefiniteLength::default()) + } +} + +impl From<()> for Length { + fn from(_: ()) -> Self { + Self::Definite(DefiniteLength::default()) + } +} + +/// Provides a trait for types that can calculate half of their value. +/// +/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type +/// representing half of the original value. This is commonly used for types that represent measurements or sizes, +/// such as lengths or pixels, where halving is a frequent operation during layout calculations or animations. +pub trait Half { + /// Returns half of the current value. + /// + /// # Returns + /// + /// A new instance of the implementing type, representing half of the original value. + fn half(&self) -> Self; +} + +impl Half for i32 { + fn half(&self) -> Self { + self / 2 + } +} + +impl Half for f32 { + fn half(&self) -> Self { + self / 2. + } +} + +impl Half for DevicePixels { + fn half(&self) -> Self { + Self(self.0 / 2) + } +} + +impl Half for ScaledPixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Pixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Rems { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +/// Provides a trait for types that can negate their values. +pub trait Negate { + /// Returns the negation of the given value + fn negate(self) -> Self; +} + +impl Negate for i32 { + fn negate(self) -> Self { + -self + } +} + +impl Negate for f32 { + fn negate(self) -> Self { + -self + } +} + +impl Negate for DevicePixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for ScaledPixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for Pixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for Rems { + fn negate(self) -> Self { + Self(-self.0) + } +} + +/// A trait for checking if a value is zero. +/// +/// This trait provides a method to determine if a value is considered to be zero. +/// It is implemented for various numeric and length-related types where the concept +/// of zero is applicable. This can be useful for comparisons, optimizations, or +/// determining if an operation has a neutral effect. +pub trait IsZero { + /// Determines if the value is zero. + /// + /// # Returns + /// + /// Returns `true` if the value is zero, `false` otherwise. + fn is_zero(&self) -> bool; +} + +impl IsZero for DevicePixels { + fn is_zero(&self) -> bool { + self.0 == 0 + } +} + +impl IsZero for ScaledPixels { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for Pixels { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for Rems { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for AbsoluteLength { + fn is_zero(&self) -> bool { + match self { + AbsoluteLength::Pixels(pixels) => pixels.is_zero(), + AbsoluteLength::Rems(rems) => rems.is_zero(), + } + } +} + +impl IsZero for DefiniteLength { + fn is_zero(&self) -> bool { + match self { + DefiniteLength::Absolute(length) => length.is_zero(), + DefiniteLength::Fraction(fraction) => *fraction == 0., + } + } +} + +impl IsZero for Length { + fn is_zero(&self) -> bool { + match self { + Length::Definite(length) => length.is_zero(), + Length::Auto => false, + } + } +} + +impl IsZero for Point { + fn is_zero(&self) -> bool { + self.x.is_zero() && self.y.is_zero() + } +} + +impl IsZero for Size +where + T: IsZero + Default + Debug + Clone, +{ + fn is_zero(&self) -> bool { + self.width.is_zero() || self.height.is_zero() + } +} + +impl IsZero for Bounds { + fn is_zero(&self) -> bool { + self.size.is_zero() + } +} + +impl IsZero for Corners +where + T: IsZero + Clone + Default + Debug, +{ + fn is_zero(&self) -> bool { + self.top_left.is_zero() + && self.top_right.is_zero() + && self.bottom_right.is_zero() + && self.bottom_left.is_zero() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bounds_intersects() { + let bounds1 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + let bounds2 = Bounds { + origin: Point { x: 4.0, y: 4.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + let bounds3 = Bounds { + origin: Point { x: 10.0, y: 10.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + + // Test Case 1: Intersecting bounds + assert_eq!(bounds1.intersects(&bounds2), true); + + // Test Case 2: Non-Intersecting bounds + assert_eq!(bounds1.intersects(&bounds3), false); + + // Test Case 3: Bounds intersecting with themselves + assert_eq!(bounds1.intersects(&bounds1), true); + } +} diff --git a/crates/ming/src/gpui.rs b/crates/ming/src/gpui.rs new file mode 100644 index 0000000..315ad92 --- /dev/null +++ b/crates/ming/src/gpui.rs @@ -0,0 +1,341 @@ +//! # Welcome to GPUI! +//! +//! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework +//! for Rust, designed to support a wide variety of applications. +//! +//! ## Getting Started +//! +//! GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io. +//! You'll also need to use the latest version of stable rust. Add the following to your Cargo.toml: +//! +//! ``` +//! gpui = { git = "https://github.com/zed-industries/zed" } +//! ``` +//! +//! Everything in GPUI starts with an [`App`]. You can create one with [`App::new`], and +//! kick off your application by passing a callback to [`App::run`]. Inside this callback, +//! you can create a new window with [`AppContext::open_window`], and register your first root +//! view. See [gpui.rs](https://www.gpui.rs/) for a complete example. +//! +//! ## The Big Picture +//! +//! GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs: +//! +//! - State management and communication with Models. Whenever you need to store application state +//! that communicates between different parts of your application, you'll want to use GPUI's +//! models. Models are owned by GPUI and are only accessible through an owned smart pointer +//! similar to an [`Rc`]. See the [`app::model_context`] module for more information. +//! +//! - High level, declarative UI with Views. All UI in GPUI starts with a View. A view is simply +//! a model that can be rendered, via the [`Render`] trait. At the start of each frame, GPUI +//! will call this render method on the root view of a given window. Views build a tree of +//! `elements`, lay them out and style them with a tailwind-style API, and then give them to +//! GPUI to turn into pixels. See the [`elements::Div`] element for an all purpose swiss-army +//! knife for UI. +//! +//! - Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they +//! provide a nice wrapper around an imperative API that provides as much flexibility and control as +//! you need. Elements have total control over how they and their child elements are rendered and +//! can be used for making efficient views into large lists, implement custom layouting for a code editor, +//! and anything else you can think of. See the [`element`] module for more information. +//! +//! Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. +//! This context is your main interface to GPUI, and is used extensively throughout the framework. +//! +//! ## Other Resources +//! +//! In addition to the systems above, GPUI provides a range of smaller services that are useful for building +//! complex applications: +//! +//! - Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. +//! Use this for implementing keyboard shortcuts, such as cmd-q. See the [`action`] module for more information. +//! - Platform services, such as `quit the app` or `open a URL` are available as methods on the [`app::AppContext`]. +//! - An async executor that is integrated with the platform's event loop. See the [`executor`] module for more information., +//! - The [gpui::test] macro provides a convenient way to write tests for your GPUI applications. Tests also have their +//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`app::test_context`] +//! and [`test`] modules for more details. +//! +//! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop +//! a question in the [Zed Discord](https://discord.gg/U4qhCEhMXP). We're working on improving the documentation, creating more examples, +//! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). + +#![deny(missing_docs)] +#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks +#![allow(clippy::collapsible_else_if)] // False positives in platform specific code +#![allow(unused_mut)] // False positives in platform specific code + +#[macro_use] +mod action; +mod app; + +mod arena; +mod asset_cache; +mod assets; +mod bounds_tree; +mod color; +mod element; +mod elements; +mod executor; +mod geometry; +mod input; +mod interactive; +mod key_dispatch; +mod keymap; +mod platform; +pub mod prelude; +mod scene; +mod shared_string; +mod shared_uri; +mod style; +mod styled; +mod subscription; +mod svg_renderer; +mod taffy; +#[cfg(any(test, feature = "test-support"))] +pub mod test; +mod text_system; +mod util; +mod view; +mod window; + +/// Do not touch, here be dragons for use by gpui_macros and such. +#[doc(hidden)] +pub mod private { + pub use linkme; + pub use serde; + pub use serde_derive; + pub use serde_json; +} + +mod seal { + /// A mechanism for restricting implementations of a trait to only those in GPUI. + /// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ + pub trait Sealed {} +} + +pub use action::*; +pub use anyhow::Result; +pub use app::*; +pub(crate) use arena::*; +pub use asset_cache::*; +pub use assets::*; +pub use color::*; +pub use ctor::ctor; +pub use element::*; +pub use elements::*; +pub use executor::*; +pub use geometry::*; +pub use gpui_macros::{register_action, test, IntoElement, Render}; +pub use input::*; +pub use interactive::*; +use key_dispatch::*; +pub use keymap::*; +pub use platform::*; +pub use refineable::*; +pub use scene::*; +use seal::Sealed; +pub use shared_string::*; +pub use shared_uri::*; +pub use smol::Timer; +pub use style::*; +pub use styled::*; +pub use subscription::*; +use svg_renderer::*; +pub use taffy::{AvailableSpace, LayoutId}; +#[cfg(any(test, feature = "test-support"))] +pub use test::*; +pub use text_system::*; +pub use util::arc_cow::ArcCow; +pub use view::*; +pub use window::*; + +use std::{any::Any, borrow::BorrowMut}; +use taffy::TaffyLayoutEngine; + +/// The context trait, allows the different contexts in GPUI to be used +/// interchangeably for certain operations. +pub trait Context { + /// The result type for this context, used for async contexts that + /// can't hold a direct reference to the application context. + type Result; + + /// Create a new model in the app context. + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result>; + + /// Reserve a slot for a model to be inserted later. + /// The returned [Reservation] allows you to obtain the [EntityId] for the future model. + fn reserve_model(&mut self) -> Self::Result>; + + /// Insert a new model in the app context based on a [Reservation] previously obtained from [`reserve_model`]. + /// + /// [`reserve_model`]: Self::reserve_model + fn insert_model( + &mut self, + reservation: Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result>; + + /// Update a model in the app context. + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Self::Result + where + T: 'static; + + /// Read a model from the app context. + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static; + + /// Update a window for the given handle. + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; + + /// Read a window off of the application context. + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static; +} + +/// Returned by [Context::reserve_model] to later be passed to [Context::insert_model]. +/// Allows you to obtain the [EntityId] for a model before it is created. +pub struct Reservation(pub(crate) Slot); + +impl Reservation { + /// Returns the [EntityId] that will be associated with the model once it is inserted. + pub fn entity_id(&self) -> EntityId { + self.0.entity_id() + } +} + +/// This trait is used for the different visual contexts in GPUI that +/// require a window to be present. +pub trait VisualContext: Context { + /// Construct a new view in the window referenced by this context. + fn new_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render; + + /// Update a view with the given callback + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Self::Result; + + /// Replace the root view of a window with a new view. + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render; + + /// Focus a view in the window, if it implements the [`FocusableView`] trait. + fn focus_view(&mut self, view: &View) -> Self::Result<()> + where + V: FocusableView; + + /// Dismiss a view in the window, if it implements the [`ManagedView`] trait. + fn dismiss_view(&mut self, view: &View) -> Self::Result<()> + where + V: ManagedView; +} + +/// A trait that allows models and views to be interchangeable in certain operations +pub trait Entity: Sealed { + /// The weak reference type for this entity. + type Weak: 'static; + + /// The ID for this entity + fn entity_id(&self) -> EntityId; + + /// Downgrade this entity to a weak reference. + fn downgrade(&self) -> Self::Weak; + + /// Upgrade this entity from a weak reference. + fn upgrade_from(weak: &Self::Weak) -> Option + where + Self: Sized; +} + +/// A trait for tying together the types of a GPUI entity and the events it can +/// emit. +pub trait EventEmitter: 'static {} + +/// A helper trait for auto-implementing certain methods on contexts that +/// can be used interchangeably. +pub trait BorrowAppContext { + /// Set a global value on the context. + fn set_global(&mut self, global: T); + /// Updates the global state of the given type. + fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global; +} + +impl BorrowAppContext for C +where + C: BorrowMut, +{ + fn set_global(&mut self, global: G) { + self.borrow_mut().set_global(global) + } + + fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global, + { + let mut global = self.borrow_mut().lease_global::(); + let result = f(&mut global, self); + self.borrow_mut().end_global_lease(global); + result + } +} + +/// A flatten equivalent for anyhow `Result`s. +pub trait Flatten { + /// Convert this type into a simple `Result`. + fn flatten(self) -> Result; +} + +impl Flatten for Result> { + fn flatten(self) -> Result { + self? + } +} + +impl Flatten for Result { + fn flatten(self) -> Result { + self + } +} + +/// A marker trait for types that can be stored in GPUI's global state. +/// +/// This trait exists to provide type-safe access to globals by restricting +/// the scope from which they can be accessed. For instance, the actual type +/// that implements [`Global`] can be private, with public accessor functions +/// that enforce correct usage. +/// +/// Implement this on types you want to store in the context as a global. +pub trait Global: 'static { + // This trait is intentionally left empty, by virtue of being a marker trait. +} diff --git a/crates/ming/src/input.rs b/crates/ming/src/input.rs new file mode 100644 index 0000000..440d6d1 --- /dev/null +++ b/crates/ming/src/input.rs @@ -0,0 +1,125 @@ +use crate::{Bounds, InputHandler, Pixels, View, ViewContext, WindowContext}; +use std::ops::Range; + +/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. +/// +/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler`]. +/// This input handler can then be assigned during paint by calling [`WindowContext::handle_input`]. +/// +/// See [`InputHandler`] for details on how to implement each method. +pub trait ViewInputHandler: 'static + Sized { + /// See [`InputHandler::text_for_range`] for details + fn text_for_range(&mut self, range: Range, cx: &mut ViewContext) + -> Option; + + /// See [`InputHandler::selected_text_range`] for details + fn selected_text_range(&mut self, cx: &mut ViewContext) -> Option>; + + /// See [`InputHandler::marked_text_range`] for details + fn marked_text_range(&self, cx: &mut ViewContext) -> Option>; + + /// See [`InputHandler::unmark_text`] for details + fn unmark_text(&mut self, cx: &mut ViewContext); + + /// See [`InputHandler::replace_text_in_range`] for details + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut ViewContext, + ); + + /// See [`InputHandler::replace_and_mark_text_in_range`] for details + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut ViewContext, + ); + + /// See [`InputHandler::bounds_for_range`] for details + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: Bounds, + cx: &mut ViewContext, + ) -> Option>; +} + +/// The canonical implementation of [`PlatformInputHandler`]. Call [`WindowContext::handle_input`] +/// with an instance during your element's paint. +pub struct ElementInputHandler { + view: View, + element_bounds: Bounds, +} + +impl ElementInputHandler { + /// Used in [`Element::paint`][element_paint] with the element's bounds and a view context for its + /// containing view. + /// + /// [element_paint]: crate::Element::paint + pub fn new(element_bounds: Bounds, view: View) -> Self { + ElementInputHandler { + view, + element_bounds, + } + } +} + +impl InputHandler for ElementInputHandler { + fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option> { + self.view + .update(cx, |view, cx| view.selected_text_range(cx)) + } + + fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option> { + self.view.update(cx, |view, cx| view.marked_text_range(cx)) + } + + fn text_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option { + self.view + .update(cx, |view, cx| view.text_for_range(range_utf16, cx)) + } + + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + cx: &mut WindowContext, + ) { + self.view.update(cx, |view, cx| { + view.replace_text_in_range(replacement_range, text, cx) + }); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut WindowContext, + ) { + self.view.update(cx, |view, cx| { + view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx) + }); + } + + fn unmark_text(&mut self, cx: &mut WindowContext) { + self.view.update(cx, |view, cx| view.unmark_text(cx)); + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option> { + self.view.update(cx, |view, cx| { + view.bounds_for_range(range_utf16, self.element_bounds, cx) + }) + } +} diff --git a/crates/ming/src/interactive.rs b/crates/ming/src/interactive.rs new file mode 100644 index 0000000..c92b58b --- /dev/null +++ b/crates/ming/src/interactive.rs @@ -0,0 +1,510 @@ +use crate::{ + point, seal::Sealed, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, + ViewContext, +}; +use smallvec::SmallVec; +use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; + +/// An event from a platform input source. +pub trait InputEvent: Sealed + 'static { + /// Convert this event into the platform input enum. + fn to_platform_input(self) -> PlatformInput; +} + +/// A key event from the platform. +pub trait KeyEvent: InputEvent {} + +/// A mouse event from the platform. +pub trait MouseEvent: InputEvent {} + +/// The key down event equivalent for the platform. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeyDownEvent { + /// The keystroke that was generated. + pub keystroke: Keystroke, + + /// Whether the key is currently held down. + pub is_held: bool, +} + +impl Sealed for KeyDownEvent {} +impl InputEvent for KeyDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyDown(self) + } +} +impl KeyEvent for KeyDownEvent {} + +/// The key up event equivalent for the platform. +#[derive(Clone, Debug)] +pub struct KeyUpEvent { + /// The keystroke that was released. + pub keystroke: Keystroke, +} + +impl Sealed for KeyUpEvent {} +impl InputEvent for KeyUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyUp(self) + } +} +impl KeyEvent for KeyUpEvent {} + +/// The modifiers changed event equivalent for the platform. +#[derive(Clone, Debug, Default)] +pub struct ModifiersChangedEvent { + /// The new state of the modifier keys + pub modifiers: Modifiers, +} + +impl Sealed for ModifiersChangedEvent {} +impl InputEvent for ModifiersChangedEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ModifiersChanged(self) + } +} +impl KeyEvent for ModifiersChangedEvent {} + +impl Deref for ModifiersChangedEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The phase of a touch motion event. +/// Based on the winit enum of the same name. +#[derive(Clone, Copy, Debug, Default)] +pub enum TouchPhase { + /// The touch started. + Started, + /// The touch event is moving. + #[default] + Moved, + /// The touch phase has ended + Ended, +} + +/// A mouse down event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseDownEvent { + /// Which mouse button was pressed. + pub button: MouseButton, + + /// The position of the mouse on the window. + pub position: Point, + + /// The modifiers that were held down when the mouse was pressed. + pub modifiers: Modifiers, + + /// The number of times the button has been clicked. + pub click_count: usize, + + /// Whether this is the first, focusing click. + pub first_mouse: bool, +} + +impl Sealed for MouseDownEvent {} +impl InputEvent for MouseDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseDown(self) + } +} +impl MouseEvent for MouseDownEvent {} + +/// A mouse up event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseUpEvent { + /// Which mouse button was released. + pub button: MouseButton, + + /// The position of the mouse on the window. + pub position: Point, + + /// The modifiers that were held down when the mouse was released. + pub modifiers: Modifiers, + + /// The number of times the button has been clicked. + pub click_count: usize, +} + +impl Sealed for MouseUpEvent {} +impl InputEvent for MouseUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseUp(self) + } +} +impl MouseEvent for MouseUpEvent {} + +/// A click event, generated when a mouse button is pressed and released. +#[derive(Clone, Debug, Default)] +pub struct ClickEvent { + /// The mouse event when the button was pressed. + pub down: MouseDownEvent, + + /// The mouse event when the button was released. + pub up: MouseUpEvent, +} + +/// An enum representing the mouse button that was pressed. +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +pub enum MouseButton { + /// The left mouse button. + Left, + + /// The right mouse button. + Right, + + /// The middle mouse button. + Middle, + + /// A navigation button, such as back or forward. + Navigate(NavigationDirection), +} + +impl MouseButton { + /// Get all the mouse buttons in a list. + pub fn all() -> Vec { + vec![ + MouseButton::Left, + MouseButton::Right, + MouseButton::Middle, + MouseButton::Navigate(NavigationDirection::Back), + MouseButton::Navigate(NavigationDirection::Forward), + ] + } +} + +impl Default for MouseButton { + fn default() -> Self { + Self::Left + } +} + +/// A navigation direction, such as back or forward. +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +pub enum NavigationDirection { + /// The back button. + Back, + + /// The forward button. + Forward, +} + +impl Default for NavigationDirection { + fn default() -> Self { + Self::Back + } +} + +/// A mouse move event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseMoveEvent { + /// The position of the mouse on the window. + pub position: Point, + + /// The mouse button that was pressed, if any. + pub pressed_button: Option, + + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, +} + +impl Sealed for MouseMoveEvent {} +impl InputEvent for MouseMoveEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseMove(self) + } +} +impl MouseEvent for MouseMoveEvent {} + +impl MouseMoveEvent { + /// Returns true if the left mouse button is currently held down. + pub fn dragging(&self) -> bool { + self.pressed_button == Some(MouseButton::Left) + } +} + +/// A mouse wheel event from the platform +#[derive(Clone, Debug, Default)] +pub struct ScrollWheelEvent { + /// The position of the mouse on the window. + pub position: Point, + + /// The change in scroll wheel position for this event. + pub delta: ScrollDelta, + + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, + + /// The phase of the touch event. + pub touch_phase: TouchPhase, +} + +impl Sealed for ScrollWheelEvent {} +impl InputEvent for ScrollWheelEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ScrollWheel(self) + } +} +impl MouseEvent for ScrollWheelEvent {} + +impl Deref for ScrollWheelEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The scroll delta for a scroll wheel event. +#[derive(Clone, Copy, Debug)] +pub enum ScrollDelta { + /// An exact scroll delta in pixels. + Pixels(Point), + /// An inexact scroll delta in lines. + Lines(Point), +} + +impl Default for ScrollDelta { + fn default() -> Self { + Self::Lines(Default::default()) + } +} + +impl ScrollDelta { + /// Returns true if this is a precise scroll delta in pixels. + pub fn precise(&self) -> bool { + match self { + ScrollDelta::Pixels(_) => true, + ScrollDelta::Lines(_) => false, + } + } + + /// Converts this scroll event into exact pixels. + pub fn pixel_delta(&self, line_height: Pixels) -> Point { + match self { + ScrollDelta::Pixels(delta) => *delta, + ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y), + } + } + + /// Combines two scroll deltas into one. + pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta { + match (self, other) { + (ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => { + ScrollDelta::Pixels(px_a + px_b) + } + + (ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => { + ScrollDelta::Lines(lines_a + lines_b) + } + + _ => other, + } + } +} + +/// A mouse exit event from the platform, generated when the mouse leaves the window. +#[derive(Clone, Debug, Default)] +pub struct MouseExitEvent { + /// The position of the mouse relative to the window. + pub position: Point, + /// The mouse button that was pressed, if any. + pub pressed_button: Option, + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, +} + +impl Sealed for MouseExitEvent {} +impl InputEvent for MouseExitEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseExited(self) + } +} +impl MouseEvent for MouseExitEvent {} + +impl Deref for MouseExitEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// A collection of paths from the platform, such as from a file drop. +#[derive(Debug, Clone, Default)] +pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); + +impl ExternalPaths { + /// Convert this collection of paths into a slice. + pub fn paths(&self) -> &[PathBuf] { + &self.0 + } +} + +impl Render for ExternalPaths { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + // the platform will render icons for the dragged files + Empty + } +} + +/// A file drop event from the platform, generated when files are dragged and dropped onto the window. +#[derive(Debug, Clone)] +pub enum FileDropEvent { + /// The files have entered the window. + Entered { + /// The position of the mouse relative to the window. + position: Point, + /// The paths of the files that are being dragged. + paths: ExternalPaths, + }, + /// The files are being dragged over the window + Pending { + /// The position of the mouse relative to the window. + position: Point, + }, + /// The files have been dropped onto the window. + Submit { + /// The position of the mouse relative to the window. + position: Point, + }, + /// The user has stopped dragging the files over the window. + Exited, +} + +impl Sealed for FileDropEvent {} +impl InputEvent for FileDropEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::FileDrop(self) + } +} +impl MouseEvent for FileDropEvent {} + +/// An enum corresponding to all kinds of platform input events. +#[derive(Clone, Debug)] +pub enum PlatformInput { + /// A key was pressed. + KeyDown(KeyDownEvent), + /// A key was released. + KeyUp(KeyUpEvent), + /// The keyboard modifiers were changed. + ModifiersChanged(ModifiersChangedEvent), + /// The mouse was pressed. + MouseDown(MouseDownEvent), + /// The mouse was released. + MouseUp(MouseUpEvent), + /// The mouse was moved. + MouseMove(MouseMoveEvent), + /// The mouse exited the window. + MouseExited(MouseExitEvent), + /// The scroll wheel was used. + ScrollWheel(ScrollWheelEvent), + /// Files were dragged and dropped onto the window. + FileDrop(FileDropEvent), +} + +impl PlatformInput { + pub(crate) fn mouse_event(&self) -> Option<&dyn Any> { + match self { + PlatformInput::KeyDown { .. } => None, + PlatformInput::KeyUp { .. } => None, + PlatformInput::ModifiersChanged { .. } => None, + PlatformInput::MouseDown(event) => Some(event), + PlatformInput::MouseUp(event) => Some(event), + PlatformInput::MouseMove(event) => Some(event), + PlatformInput::MouseExited(event) => Some(event), + PlatformInput::ScrollWheel(event) => Some(event), + PlatformInput::FileDrop(event) => Some(event), + } + } + + pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> { + match self { + PlatformInput::KeyDown(event) => Some(event), + PlatformInput::KeyUp(event) => Some(event), + PlatformInput::ModifiersChanged(event) => Some(event), + PlatformInput::MouseDown(_) => None, + PlatformInput::MouseUp(_) => None, + PlatformInput::MouseMove(_) => None, + PlatformInput::MouseExited(_) => None, + PlatformInput::ScrollWheel(_) => None, + PlatformInput::FileDrop(_) => None, + } + } +} + +#[cfg(test)] +mod test { + + use crate::{ + self as gpui, div, Element, FocusHandle, InteractiveElement, IntoElement, KeyBinding, + Keystroke, ParentElement, Render, TestAppContext, VisualContext, + }; + + struct TestView { + saw_key_down: bool, + saw_action: bool, + focus_handle: FocusHandle, + } + + actions!(test, [TestAction]); + + impl Render for TestView { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl Element { + div().id("testview").child( + div() + .key_context("parent") + .on_key_down(cx.listener(|this, _, cx| { + cx.stop_propagation(); + this.saw_key_down = true + })) + .on_action( + cx.listener(|this: &mut TestView, _: &TestAction, _| { + this.saw_action = true + }), + ) + .child( + div() + .key_context("nested") + .track_focus(&self.focus_handle) + .into_element(), + ), + ) + } + } + + #[gpui::test] + fn test_on_events(cx: &mut TestAppContext) { + let window = cx.update(|cx| { + cx.open_window(Default::default(), |cx| { + cx.new_view(|cx| TestView { + saw_key_down: false, + saw_action: false, + focus_handle: cx.focus_handle(), + }) + }) + }); + + cx.update(|cx| { + cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]); + }); + + window + .update(cx, |test_view, cx| cx.focus(&test_view.focus_handle)) + .unwrap(); + + cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap()); + cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap()); + + window + .update(cx, |test_view, _| { + assert!(test_view.saw_key_down || test_view.saw_action); + assert!(test_view.saw_key_down); + assert!(test_view.saw_action); + }) + .unwrap(); + } +} diff --git a/crates/ming/src/key_dispatch.rs b/crates/ming/src/key_dispatch.rs new file mode 100644 index 0000000..0031ed8 --- /dev/null +++ b/crates/ming/src/key_dispatch.rs @@ -0,0 +1,608 @@ +/// KeyDispatch is where GPUI deals with binding actions to key events. +/// +/// The key pieces to making a key binding work are to define an action, +/// implement a method that takes that action as a type parameter, +/// and then to register the action during render on a focused node +/// with a keymap context: +/// +/// ```rust +/// actions!(editor,[Undo, Redo]);; +/// +/// impl Editor { +/// fn undo(&mut self, _: &Undo, _cx: &mut ViewContext) { ... } +/// fn redo(&mut self, _: &Redo, _cx: &mut ViewContext) { ... } +/// } +/// +/// impl Render for Editor { +/// fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { +/// div() +/// .track_focus(&self.focus_handle) +/// .keymap_context("Editor") +/// .on_action(cx.listener(Editor::undo)) +/// .on_action(cx.listener(Editor::redo)) +/// ... +/// } +/// } +///``` +/// +/// The keybindings themselves are managed independently by calling cx.bind_keys(). +/// (Though mostly when developing Zed itself, you just need to add a new line to +/// assets/keymaps/default.json). +/// +/// ```rust +/// cx.bind_keys([ +/// KeyBinding::new("cmd-z", Editor::undo, Some("Editor")), +/// KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")), +/// ]) +/// ``` +/// +/// With all of this in place, GPUI will ensure that if you have an Editor that contains +/// the focus, hitting cmd-z will Undo. +/// +/// In real apps, it is a little more complicated than this, because typically you have +/// several nested views that each register keyboard handlers. In this case action matching +/// bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined +/// then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace. +/// +/// In GPUI, keybindings are not limited to just single keystrokes, you can define +/// sequences by separating the keys with a space: +/// +/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) +/// +use crate::{ + Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, + KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent, WindowContext, +}; +use collections::FxHashMap; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + mem, + ops::Range, + rc::Rc, +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub(crate) struct DispatchNodeId(usize); + +pub(crate) struct DispatchTree { + node_stack: Vec, + pub(crate) context_stack: Vec, + view_stack: Vec, + nodes: Vec, + focusable_node_ids: FxHashMap, + view_node_ids: FxHashMap, + keystroke_matchers: FxHashMap, KeystrokeMatcher>, + keymap: Rc>, + action_registry: Rc, +} + +#[derive(Default)] +pub(crate) struct DispatchNode { + pub key_listeners: Vec, + pub action_listeners: Vec, + pub modifiers_changed_listeners: Vec, + pub context: Option, + pub focus_id: Option, + view_id: Option, + parent: Option, +} + +pub(crate) struct ReusedSubtree { + old_range: Range, + new_range: Range, +} + +impl ReusedSubtree { + pub fn refresh_node_id(&self, node_id: DispatchNodeId) -> DispatchNodeId { + debug_assert!( + self.old_range.contains(&node_id.0), + "node {} was not part of the reused subtree {:?}", + node_id.0, + self.old_range + ); + DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start) + } +} + +type KeyListener = Rc; +type ModifiersChangedListener = Rc; + +#[derive(Clone)] +pub(crate) struct DispatchActionListener { + pub(crate) action_type: TypeId, + pub(crate) listener: Rc, +} + +impl DispatchTree { + pub fn new(keymap: Rc>, action_registry: Rc) -> Self { + Self { + node_stack: Vec::new(), + context_stack: Vec::new(), + view_stack: Vec::new(), + nodes: Vec::new(), + focusable_node_ids: FxHashMap::default(), + view_node_ids: FxHashMap::default(), + keystroke_matchers: FxHashMap::default(), + keymap, + action_registry, + } + } + + pub fn clear(&mut self) { + self.node_stack.clear(); + self.context_stack.clear(); + self.view_stack.clear(); + self.nodes.clear(); + self.focusable_node_ids.clear(); + self.view_node_ids.clear(); + self.keystroke_matchers.clear(); + } + + pub fn len(&self) -> usize { + self.nodes.len() + } + + pub fn push_node(&mut self) -> DispatchNodeId { + let parent = self.node_stack.last().copied(); + let node_id = DispatchNodeId(self.nodes.len()); + + self.nodes.push(DispatchNode { + parent, + ..Default::default() + }); + self.node_stack.push(node_id); + node_id + } + + pub fn set_active_node(&mut self, node_id: DispatchNodeId) { + let next_node_parent = self.nodes[node_id.0].parent; + while self.node_stack.last().copied() != next_node_parent && !self.node_stack.is_empty() { + self.pop_node(); + } + + if self.node_stack.last().copied() == next_node_parent { + self.node_stack.push(node_id); + let active_node = &self.nodes[node_id.0]; + if let Some(view_id) = active_node.view_id { + self.view_stack.push(view_id) + } + if let Some(context) = active_node.context.clone() { + self.context_stack.push(context); + } + } else { + debug_assert_eq!(self.node_stack.len(), 0); + + let mut current_node_id = Some(node_id); + while let Some(node_id) = current_node_id { + let node = &self.nodes[node_id.0]; + if let Some(context) = node.context.clone() { + self.context_stack.push(context); + } + if node.view_id.is_some() { + self.view_stack.push(node.view_id.unwrap()); + } + self.node_stack.push(node_id); + current_node_id = node.parent; + } + + self.context_stack.reverse(); + self.view_stack.reverse(); + self.node_stack.reverse(); + } + } + + pub fn set_key_context(&mut self, context: KeyContext) { + self.active_node().context = Some(context.clone()); + self.context_stack.push(context); + } + + pub fn set_focus_id(&mut self, focus_id: FocusId) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].focus_id = Some(focus_id); + self.focusable_node_ids.insert(focus_id, node_id); + } + + pub fn parent_view_id(&mut self) -> Option { + self.view_stack.last().copied() + } + + pub fn set_view_id(&mut self, view_id: EntityId) { + if self.view_stack.last().copied() != Some(view_id) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].view_id = Some(view_id); + self.view_node_ids.insert(view_id, node_id); + self.view_stack.push(view_id); + } + } + + pub fn pop_node(&mut self) { + let node = &self.nodes[self.active_node_id().unwrap().0]; + if node.context.is_some() { + self.context_stack.pop(); + } + if node.view_id.is_some() { + self.view_stack.pop(); + } + self.node_stack.pop(); + } + + fn move_node(&mut self, source: &mut DispatchNode) { + self.push_node(); + if let Some(context) = source.context.clone() { + self.set_key_context(context); + } + if let Some(focus_id) = source.focus_id { + self.set_focus_id(focus_id); + } + if let Some(view_id) = source.view_id { + self.set_view_id(view_id); + } + + let target = self.active_node(); + target.key_listeners = mem::take(&mut source.key_listeners); + target.action_listeners = mem::take(&mut source.action_listeners); + target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners); + } + + pub fn reuse_subtree(&mut self, old_range: Range, source: &mut Self) -> ReusedSubtree { + let new_range = self.nodes.len()..self.nodes.len() + old_range.len(); + + let mut source_stack = vec![]; + for (source_node_id, source_node) in source + .nodes + .iter_mut() + .enumerate() + .skip(old_range.start) + .take(old_range.len()) + { + let source_node_id = DispatchNodeId(source_node_id); + while let Some(source_ancestor) = source_stack.last() { + if source_node.parent == Some(*source_ancestor) { + break; + } else { + source_stack.pop(); + self.pop_node(); + } + } + + source_stack.push(source_node_id); + self.move_node(source_node); + } + + while !source_stack.is_empty() { + source_stack.pop(); + self.pop_node(); + } + + ReusedSubtree { + old_range, + new_range, + } + } + + pub fn truncate(&mut self, index: usize) { + for node in &self.nodes[index..] { + if let Some(focus_id) = node.focus_id { + self.focusable_node_ids.remove(&focus_id); + } + + if let Some(view_id) = node.view_id { + self.view_node_ids.remove(&view_id); + } + } + self.nodes.truncate(index); + } + + pub fn clear_pending_keystrokes(&mut self) { + self.keystroke_matchers.clear(); + } + + /// Preserve keystroke matchers from previous frames to support multi-stroke + /// bindings across multiple frames. + pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option) { + if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) { + let dispatch_path = self.dispatch_path(node_id); + + self.context_stack.clear(); + for node_id in dispatch_path { + let node = self.node(node_id); + if let Some(context) = node.context.clone() { + self.context_stack.push(context); + } + + if let Some((context_stack, matcher)) = old_tree + .keystroke_matchers + .remove_entry(self.context_stack.as_slice()) + { + self.keystroke_matchers.insert(context_stack, matcher); + } + } + } + } + + pub fn on_key_event(&mut self, listener: KeyListener) { + self.active_node().key_listeners.push(listener); + } + + pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) { + self.active_node() + .modifiers_changed_listeners + .push(listener); + } + + pub fn on_action( + &mut self, + action_type: TypeId, + listener: Rc, + ) { + self.active_node() + .action_listeners + .push(DispatchActionListener { + action_type, + listener, + }); + } + + pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool { + if parent == child { + return true; + } + + if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) { + let mut current_node_id = self.focusable_node_ids.get(&child).copied(); + while let Some(node_id) = current_node_id { + if node_id == *parent_node_id { + return true; + } + current_node_id = self.nodes[node_id.0].parent; + } + } + false + } + + pub fn available_actions(&self, target: DispatchNodeId) -> Vec> { + let mut actions = Vec::>::new(); + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + for DispatchActionListener { action_type, .. } in &node.action_listeners { + if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) + { + // Intentionally silence these errors without logging. + // If an action cannot be built by default, it's not available. + let action = self.action_registry.build_action_type(action_type).ok(); + if let Some(action) = action { + actions.insert(ix, action); + } + } + } + } + actions + } + + pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool { + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; + } + } + false + } + + pub fn bindings_for_action( + &self, + action: &dyn Action, + context_stack: &[KeyContext], + ) -> Vec { + let keymap = self.keymap.borrow(); + keymap + .bindings_for_action(action) + .filter(|binding| { + for i in 0..context_stack.len() { + let context = &context_stack[0..=i]; + if keymap.binding_enabled(binding, context) { + return true; + } + } + false + }) + .cloned() + .collect() + } + + // dispatch_key pushes the next keystroke into any key binding matchers. + // any matching bindings are returned in the order that they should be dispatched: + // * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first) + // * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a + // binding for "b", the Editor action fires first). + pub fn dispatch_key( + &mut self, + keystroke: &Keystroke, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> KeymatchResult { + let mut bindings = SmallVec::<[KeyBinding; 1]>::new(); + let mut pending = false; + + let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new(); + for node_id in dispatch_path { + let node = self.node(*node_id); + + if let Some(context) = node.context.clone() { + context_stack.push(context); + } + } + + while !context_stack.is_empty() { + let keystroke_matcher = self + .keystroke_matchers + .entry(context_stack.clone()) + .or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone())); + + let result = keystroke_matcher.match_keystroke(keystroke, &context_stack); + if result.pending && !pending && !bindings.is_empty() { + context_stack.pop(); + continue; + } + + pending = result.pending || pending; + for new_binding in result.bindings { + match bindings + .iter() + .position(|el| el.keystrokes.len() < new_binding.keystrokes.len()) + { + Some(idx) => { + bindings.insert(idx, new_binding); + } + None => bindings.push(new_binding), + } + } + context_stack.pop(); + } + + KeymatchResult { bindings, pending } + } + + pub fn has_pending_keystrokes(&self) -> bool { + self.keystroke_matchers + .iter() + .any(|(_, matcher)| matcher.has_pending_keystrokes()) + } + + pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); + let mut current_node_id = Some(target); + while let Some(node_id) = current_node_id { + dispatch_path.push(node_id); + current_node_id = self.nodes[node_id.0].parent; + } + dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. + dispatch_path + } + + pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> { + let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new(); + let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied(); + while let Some(node_id) = current_node_id { + let node = self.node(node_id); + if let Some(focus_id) = node.focus_id { + focus_path.push(focus_id); + } + current_node_id = node.parent; + } + focus_path.reverse(); // Reverse the path so it goes from the root to the focused node. + focus_path + } + + pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> { + let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new(); + let mut current_node_id = self.view_node_ids.get(&view_id).copied(); + while let Some(node_id) = current_node_id { + let node = self.node(node_id); + if let Some(view_id) = node.view_id { + view_path.push(view_id); + } + current_node_id = node.parent; + } + view_path.reverse(); // Reverse the path so it goes from the root to the view node. + view_path + } + + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { + &self.nodes[node_id.0] + } + + fn active_node(&mut self) -> &mut DispatchNode { + let active_node_id = self.active_node_id().unwrap(); + &mut self.nodes[active_node_id.0] + } + + pub fn focusable_node_id(&self, target: FocusId) -> Option { + self.focusable_node_ids.get(&target).copied() + } + + pub fn root_node_id(&self) -> DispatchNodeId { + debug_assert!(!self.nodes.is_empty()); + DispatchNodeId(0) + } + + pub fn active_node_id(&self) -> Option { + self.node_stack.last().copied() + } +} + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, rc::Rc}; + + use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap}; + + #[derive(PartialEq, Eq)] + struct TestAction; + + impl Action for TestAction { + fn name(&self) -> &'static str { + "test::TestAction" + } + + fn debug_name() -> &'static str + where + Self: ::std::marker::Sized, + { + "test::TestAction" + } + + fn partial_eq(&self, action: &dyn Action) -> bool { + action + .as_any() + .downcast_ref::() + .map_or(false, |a| self == a) + } + + fn boxed_clone(&self) -> std::boxed::Box { + Box::new(TestAction) + } + + fn as_any(&self) -> &dyn ::std::any::Any { + self + } + + fn build(_value: serde_json::Value) -> anyhow::Result> + where + Self: Sized, + { + Ok(Box::new(TestAction)) + } + } + + #[test] + fn test_keybinding_for_action_bounds() { + let keymap = Keymap::new(vec![KeyBinding::new( + "cmd-n", + TestAction, + Some("ProjectPanel"), + )]); + + let mut registry = ActionRegistry::default(); + + registry.load_action::(); + + let keymap = Rc::new(RefCell::new(keymap)); + + let tree = DispatchTree::new(keymap, Rc::new(registry)); + + let contexts = vec![ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("ProjectPanel").unwrap(), + ]; + + let keybinding = tree.bindings_for_action(&TestAction, &contexts); + + assert!(keybinding[0].action.partial_eq(&TestAction)) + } +} diff --git a/crates/ming/src/keymap.rs b/crates/ming/src/keymap.rs new file mode 100644 index 0000000..d6b84f1 --- /dev/null +++ b/crates/ming/src/keymap.rs @@ -0,0 +1,183 @@ +mod binding; +mod context; +mod matcher; + +pub use binding::*; +pub use context::*; +pub(crate) use matcher::*; + +use crate::{Action, Keystroke, NoAction}; +use collections::{HashMap, HashSet}; +use smallvec::SmallVec; +use std::any::{Any, TypeId}; + +/// An opaque identifier of which version of the keymap is currently active. +/// The keymap's version is changed whenever bindings are added or removed. +#[derive(Copy, Clone, Eq, PartialEq, Default)] +pub struct KeymapVersion(usize); + +/// A collection of key bindings for the user's application. +#[derive(Default)] +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_id: HashMap>, + disabled_keystrokes: + HashMap, HashSet>>, + version: KeymapVersion, +} + +impl Keymap { + /// Create a new keymap with the given bindings. + pub fn new(bindings: Vec) -> Self { + let mut this = Self::default(); + this.add_bindings(bindings); + this + } + + /// Get the current version of the keymap. + pub fn version(&self) -> KeymapVersion { + self.version + } + + /// Add more bindings to the keymap. + pub fn add_bindings>(&mut self, bindings: T) { + let no_action_id = (NoAction {}).type_id(); + + for binding in bindings { + let action_id = binding.action().as_any().type_id(); + if action_id == no_action_id { + self.disabled_keystrokes + .entry(binding.keystrokes) + .or_default() + .insert(binding.context_predicate); + } else { + self.binding_indices_by_action_id + .entry(action_id) + .or_default() + .push(self.bindings.len()); + self.bindings.push(binding); + } + } + + self.version.0 += 1; + } + + /// Reset this keymap to its initial state. + pub fn clear(&mut self) { + self.bindings.clear(); + self.binding_indices_by_action_id.clear(); + self.disabled_keystrokes.clear(); + self.version.0 += 1; + } + + /// Iterate over all bindings, in the order they were added. + pub fn bindings(&self) -> impl DoubleEndedIterator { + self.bindings.iter() + } + + /// Iterate over all bindings for the given action, in the order they were added. + pub fn bindings_for_action<'a>( + &'a self, + action: &'a dyn Action, + ) -> impl 'a + DoubleEndedIterator { + let action_id = action.type_id(); + self.binding_indices_by_action_id + .get(&action_id) + .map_or(&[] as _, SmallVec::as_slice) + .iter() + .map(|ix| &self.bindings[*ix]) + .filter(move |binding| binding.action().partial_eq(action)) + } + + /// Check if the given binding is enabled, given a certain key context. + pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool { + // If binding has a context predicate, it must match the current context, + if let Some(predicate) = &binding.context_predicate { + if !predicate.eval(context) { + return false; + } + } + + if let Some(disabled_predicates) = self.disabled_keystrokes.get(&binding.keystrokes) { + for disabled_predicate in disabled_predicates { + match disabled_predicate { + // The binding must not be globally disabled. + None => return false, + + // The binding must not be disabled in the current context. + Some(predicate) => { + if predicate.eval(context) { + return false; + } + } + } + } + } + + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as gpui; + use gpui::actions; + + actions!( + keymap_test, + [ActionAlpha, ActionBeta, ActionGamma, ActionDelta,] + ); + + #[test] + fn test_keymap() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, None), + KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), + KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // global bindings are enabled in all contexts + assert!(keymap.binding_enabled(&bindings[0], &[])); + assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()])); + + // contextual bindings are enabled in contexts that match their predicate + assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()])); + assert!(keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()])); + + assert!(!keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()])); + assert!(keymap.binding_enabled( + &bindings[2], + &[KeyContext::parse("editor mode=full").unwrap()] + )); + } + + #[test] + fn test_keymap_disabled() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")), + KeyBinding::new("ctrl-b", NoAction {}, None), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // binding is only enabled in a specific context + assert!(!keymap.binding_enabled(&bindings[0], &[KeyContext::parse("barf").unwrap()])); + assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("editor").unwrap()])); + + // binding is disabled in a more specific context + assert!(!keymap.binding_enabled( + &bindings[0], + &[KeyContext::parse("editor mode=full").unwrap()] + )); + + // binding is globally disabled + assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf").unwrap()])); + } +} diff --git a/crates/ming/src/keymap/binding.rs b/crates/ming/src/keymap/binding.rs new file mode 100644 index 0000000..5e97e26 --- /dev/null +++ b/crates/ming/src/keymap/binding.rs @@ -0,0 +1,81 @@ +use crate::{Action, KeyBindingContextPredicate, KeyMatch, Keystroke}; +use anyhow::Result; +use smallvec::SmallVec; + +/// A keybinding and its associated metadata, from the keymap. +pub struct KeyBinding { + pub(crate) action: Box, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, + pub(crate) context_predicate: Option, +} + +impl Clone for KeyBinding { + fn clone(&self) -> Self { + KeyBinding { + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + context_predicate: self.context_predicate.clone(), + } + } +} + +impl KeyBinding { + /// Construct a new keybinding from the given data. + pub fn new(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self { + Self::load(keystrokes, Box::new(action), context_predicate).unwrap() + } + + /// Load a keybinding from the given raw data. + pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { + let context = if let Some(context) = context { + Some(KeyBindingContextPredicate::parse(context)?) + } else { + None + }; + + let keystrokes = keystrokes + .split_whitespace() + .map(Keystroke::parse) + .collect::>()?; + + Ok(Self { + keystrokes, + action, + context_predicate: context, + }) + } + + /// Check if the given keystrokes match this binding. + pub fn match_keystrokes(&self, pending_keystrokes: &[Keystroke]) -> KeyMatch { + if self.keystrokes.as_ref().starts_with(pending_keystrokes) { + // If the binding is completed, push it onto the matches list + if self.keystrokes.as_ref().len() == pending_keystrokes.len() { + KeyMatch::Matched + } else { + KeyMatch::Pending + } + } else { + KeyMatch::None + } + } + + /// Get the keystrokes associated with this binding + pub fn keystrokes(&self) -> &[Keystroke] { + self.keystrokes.as_slice() + } + + /// Get the action associated with this binding + pub fn action(&self) -> &dyn Action { + self.action.as_ref() + } +} + +impl std::fmt::Debug for KeyBinding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyBinding") + .field("keystrokes", &self.keystrokes) + .field("context_predicate", &self.context_predicate) + .field("action", &self.action.name()) + .finish() + } +} diff --git a/crates/ming/src/keymap/context.rs b/crates/ming/src/keymap/context.rs new file mode 100644 index 0000000..6ac22d2 --- /dev/null +++ b/crates/ming/src/keymap/context.rs @@ -0,0 +1,522 @@ +use crate::SharedString; +use anyhow::{anyhow, Result}; +use smallvec::SmallVec; +use std::fmt; + +/// A datastructure for resolving whether an action should be dispatched +/// at this point in the element tree. Contains a set of identifiers +/// and/or key value pairs representing the current context for the +/// keymap. +#[derive(Clone, Default, Eq, PartialEq, Hash)] +pub struct KeyContext(SmallVec<[ContextEntry; 1]>); + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +struct ContextEntry { + key: SharedString, + value: Option, +} + +impl<'a> TryFrom<&'a str> for KeyContext { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + Self::parse(value) + } +} + +impl KeyContext { + /// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`. + pub fn new_with_defaults() -> Self { + let mut context = Self::default(); + #[cfg(target_os = "macos")] + context.set("os", "macos"); + #[cfg(target_os = "linux")] + context.set("os", "linux"); + #[cfg(target_os = "windows")] + context.set("os", "windows"); + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + context.set("os", "unknown"); + context + } + + /// Parse a key context from a string. + /// The key context format is very simple: + /// - either a single identifier, such as `StatusBar` + /// - or a key value pair, such as `mode = visible` + /// - separated by whitespace, such as `StatusBar mode = visible` + pub fn parse(source: &str) -> Result { + let mut context = Self::default(); + let source = skip_whitespace(source); + Self::parse_expr(source, &mut context)?; + Ok(context) + } + + fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { + if source.is_empty() { + return Ok(()); + } + + let key = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[key.len()..]); + if let Some(suffix) = source.strip_prefix('=') { + source = skip_whitespace(suffix); + let value = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[value.len()..]); + context.set(key, value); + } else { + context.add(key); + } + + Self::parse_expr(source, context) + } + + /// Check if this context is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Clear this context. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Extend this context with another context. + pub fn extend(&mut self, other: &Self) { + for entry in &other.0 { + if !self.contains(&entry.key) { + self.0.push(entry.clone()); + } + } + } + + /// Add an identifier to this context, if it's not already in this context. + pub fn add>(&mut self, identifier: I) { + let key = identifier.into(); + + if !self.contains(&key) { + self.0.push(ContextEntry { key, value: None }) + } + } + + /// Set a key value pair in this context, if it's not already set. + pub fn set, S2: Into>(&mut self, key: S1, value: S2) { + let key = key.into(); + if !self.contains(&key) { + self.0.push(ContextEntry { + key, + value: Some(value.into()), + }) + } + } + + /// Check if this context contains a given identifier or key. + pub fn contains(&self, key: &str) -> bool { + self.0.iter().any(|entry| entry.key.as_ref() == key) + } + + /// Get the associated value for a given identifier or key. + pub fn get(&self, key: &str) -> Option<&SharedString> { + self.0 + .iter() + .find(|entry| entry.key.as_ref() == key)? + .value + .as_ref() + } +} + +impl fmt::Debug for KeyContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut entries = self.0.iter().peekable(); + while let Some(entry) = entries.next() { + if let Some(ref value) = entry.value { + write!(f, "{}={}", entry.key, value)?; + } else { + write!(f, "{}", entry.key)?; + } + if entries.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } +} + +/// A datastructure for resolving whether an action should be dispatched +/// Representing a small language for describing which contexts correspond +/// to which actions. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum KeyBindingContextPredicate { + /// A predicate that will match a given identifier. + Identifier(SharedString), + /// A predicate that will match a given key-value pair. + Equal(SharedString, SharedString), + /// A predicate that will match a given key-value pair not being present. + NotEqual(SharedString, SharedString), + /// A predicate that will match a given predicate appearing below another predicate. + /// in the element tree + Child( + Box, + Box, + ), + /// Predicate that will invert another predicate. + Not(Box), + /// A predicate that will match if both of its children match. + And( + Box, + Box, + ), + /// A predicate that will match if either of its children match. + Or( + Box, + Box, + ), +} + +impl KeyBindingContextPredicate { + /// Parse a string in the same format as the keymap's context field. + /// + /// A basic equivalence check against a set of identifiers can performed by + /// simply writing a string: + /// + /// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar` + /// + /// You can also specify a key-value pair: + /// + /// `mode == visible` -> A predicate that will match a context with the key `mode` + /// with the value `visible` + /// + /// And a logical operations combining these two checks: + /// + /// `StatusBar && mode == visible` -> A predicate that will match a context with the + /// identifier `StatusBar` and the key `mode` + /// with the value `visible` + /// + /// + /// There is also a special child `>` operator that will match a predicate that is + /// below another predicate: + /// + /// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar` + /// and a child context that has the key `mode` with the + /// value `visible` + /// + /// This syntax supports `!=`, `||` and `&&` as logical operators. + /// You can also preface an operation or check with a `!` to negate it. + pub fn parse(source: &str) -> Result { + let source = skip_whitespace(source); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if let Some(next) = rest.chars().next() { + Err(anyhow!("unexpected character {next:?}")) + } else { + Ok(predicate) + } + } + + /// Eval a predicate against a set of contexts, arranged from lowest to highest. + pub fn eval(&self, contexts: &[KeyContext]) -> bool { + let Some(context) = contexts.last() else { + return false; + }; + match self { + Self::Identifier(name) => context.contains(name), + Self::Equal(left, right) => context + .get(left) + .map(|value| value == right) + .unwrap_or(false), + Self::NotEqual(left, right) => context + .get(left) + .map(|value| value != right) + .unwrap_or(true), + Self::Not(pred) => !pred.eval(contexts), + Self::Child(parent, child) => { + parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts) + } + Self::And(left, right) => left.eval(contexts) && right.eval(contexts), + Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), + } + } + + fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { + type Op = fn( + KeyBindingContextPredicate, + KeyBindingContextPredicate, + ) -> Result; + + let (mut predicate, rest) = Self::parse_primary(source)?; + source = rest; + + 'parse: loop { + for (operator, precedence, constructor) in [ + (">", PRECEDENCE_CHILD, Self::new_child as Op), + ("&&", PRECEDENCE_AND, Self::new_and as Op), + ("||", PRECEDENCE_OR, Self::new_or as Op), + ("==", PRECEDENCE_EQ, Self::new_eq as Op), + ("!=", PRECEDENCE_EQ, Self::new_neq as Op), + ] { + if source.starts_with(operator) && precedence >= min_precedence { + source = skip_whitespace(&source[operator.len()..]); + let (right, rest) = Self::parse_expr(source, precedence + 1)?; + predicate = constructor(predicate, right)?; + source = rest; + continue 'parse; + } + } + break; + } + + Ok((predicate, source)) + } + + fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { + let next = source + .chars() + .next() + .ok_or_else(|| anyhow!("unexpected eof"))?; + match next { + '(' => { + source = skip_whitespace(&source[1..]); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if let Some(stripped) = rest.strip_prefix(')') { + source = skip_whitespace(stripped); + Ok((predicate, source)) + } else { + Err(anyhow!("expected a ')'")) + } + } + '!' => { + let source = skip_whitespace(&source[1..]); + let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?; + Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source)) + } + _ if is_identifier_char(next) => { + let len = source + .find(|c: char| !is_identifier_char(c)) + .unwrap_or(source.len()); + let (identifier, rest) = source.split_at(len); + source = skip_whitespace(rest); + Ok(( + KeyBindingContextPredicate::Identifier(identifier.to_string().into()), + source, + )) + } + _ => Err(anyhow!("unexpected character {next:?}")), + } + } + + fn new_or(self, other: Self) -> Result { + Ok(Self::Or(Box::new(self), Box::new(other))) + } + + fn new_and(self, other: Self) -> Result { + Ok(Self::And(Box::new(self), Box::new(other))) + } + + fn new_child(self, other: Self) -> Result { + Ok(Self::Child(Box::new(self), Box::new(other))) + } + + fn new_eq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::Equal(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } + + fn new_neq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::NotEqual(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } +} + +const PRECEDENCE_CHILD: u32 = 1; +const PRECEDENCE_OR: u32 = 2; +const PRECEDENCE_AND: u32 = 3; +const PRECEDENCE_EQ: u32 = 4; +const PRECEDENCE_NOT: u32 = 5; + +fn is_identifier_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' +} + +fn skip_whitespace(source: &str) -> &str { + let len = source + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source.len()); + &source[len..] +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as gpui; + use KeyBindingContextPredicate::*; + + #[test] + fn test_actions_definition() { + { + actions!(test, [A, B, C, D, E, F, G]); + } + + { + actions!( + test, + [ + A, + B, + C, + D, + E, + F, + G, // Don't wrap, test the trailing comma + ] + ); + } + } + + #[test] + fn test_parse_context() { + let mut expected = KeyContext::default(); + expected.add("baz"); + expected.set("foo", "bar"); + assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected); + assert_eq!( + KeyContext::parse(" baz foo = bar baz").unwrap(), + expected + ); + assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected); + } + + #[test] + fn test_parse_identifiers() { + // Identifiers + assert_eq!( + KeyBindingContextPredicate::parse("abc12").unwrap(), + Identifier("abc12".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("_1a").unwrap(), + Identifier("_1a".into()) + ); + } + + #[test] + fn test_parse_negations() { + assert_eq!( + KeyBindingContextPredicate::parse("!abc").unwrap(), + Not(Box::new(Identifier("abc".into()))) + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ! ! abc").unwrap(), + Not(Box::new(Not(Box::new(Identifier("abc".into()))))) + ); + } + + #[test] + fn test_parse_equality_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a == b").unwrap(), + Equal("a".into(), "b".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c!=d").unwrap(), + NotEqual("c".into(), "d".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c == !d") + .unwrap_err() + .to_string(), + "operands must be identifiers" + ); + } + + #[test] + fn test_parse_boolean_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a || b").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a || !b && c").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(And( + Box::new(Not(Box::new(Identifier("b".into())))), + Box::new(Identifier("c".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(), + Or( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(And( + Box::new(Identifier("c".into())), + Box::new(Identifier("d".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(), + Or( + Box::new(And( + Box::new(Equal("a".into(), "b".into())), + Box::new(Identifier("c".into())) + )), + Box::new(And( + Box::new(Equal("d".into(), "e".into())), + Box::new(Identifier("f".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b && c && d").unwrap(), + And( + Box::new(And( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(Identifier("c".into())), + )), + Box::new(Identifier("d".into())) + ), + ); + } + + #[test] + fn test_parse_parenthesized_expressions() { + assert_eq!( + KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )), + ), + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())), + ) + ); + } +} diff --git a/crates/ming/src/keymap/matcher.rs b/crates/ming/src/keymap/matcher.rs new file mode 100644 index 0000000..c2dec94 --- /dev/null +++ b/crates/ming/src/keymap/matcher.rs @@ -0,0 +1,102 @@ +use crate::{KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke}; +use smallvec::SmallVec; +use std::{cell::RefCell, rc::Rc}; + +pub(crate) struct KeystrokeMatcher { + pending_keystrokes: Vec, + keymap: Rc>, + keymap_version: KeymapVersion, +} + +pub struct KeymatchResult { + pub bindings: SmallVec<[KeyBinding; 1]>, + pub pending: bool, +} + +impl KeystrokeMatcher { + pub fn new(keymap: Rc>) -> Self { + let keymap_version = keymap.borrow().version(); + Self { + pending_keystrokes: Vec::new(), + keymap_version, + keymap, + } + } + + pub fn has_pending_keystrokes(&self) -> bool { + !self.pending_keystrokes.is_empty() + } + + /// Pushes a keystroke onto the matcher. + /// The result of the new keystroke is returned: + /// - KeyMatch::None => + /// No match is valid for this key given any pending keystrokes. + /// - KeyMatch::Pending => + /// There exist bindings which are still waiting for more keys. + /// - KeyMatch::Complete(matches) => + /// One or more bindings have received the necessary key presses. + /// Bindings added later will take precedence over earlier bindings. + pub(crate) fn match_keystroke( + &mut self, + keystroke: &Keystroke, + context_stack: &[KeyContext], + ) -> KeymatchResult { + let keymap = self.keymap.borrow(); + + // Clear pending keystrokes if the keymap has changed since the last matched keystroke. + if keymap.version() != self.keymap_version { + self.keymap_version = keymap.version(); + self.pending_keystrokes.clear(); + } + + let mut pending_key = None; + let mut bindings = SmallVec::new(); + + for binding in keymap.bindings().rev() { + if !keymap.binding_enabled(binding, context_stack) { + continue; + } + + for candidate in keystroke.match_candidates() { + self.pending_keystrokes.push(candidate.clone()); + match binding.match_keystrokes(&self.pending_keystrokes) { + KeyMatch::Matched => { + bindings.push(binding.clone()); + } + KeyMatch::Pending => { + pending_key.get_or_insert(candidate); + } + KeyMatch::None => {} + } + self.pending_keystrokes.pop(); + } + } + + if bindings.is_empty() && pending_key.is_none() && !self.pending_keystrokes.is_empty() { + drop(keymap); + self.pending_keystrokes.remove(0); + return self.match_keystroke(keystroke, context_stack); + } + + let pending = if let Some(pending_key) = pending_key { + self.pending_keystrokes.push(pending_key); + true + } else { + self.pending_keystrokes.clear(); + false + }; + + KeymatchResult { bindings, pending } + } +} + +/// The result of matching a keystroke against a given keybinding. +/// - KeyMatch::None => No match is valid for this key given any pending keystrokes. +/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys. +/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses. +#[derive(Debug, PartialEq)] +pub enum KeyMatch { + None, + Pending, + Matched, +} diff --git a/crates/ming/src/platform.rs b/crates/ming/src/platform.rs new file mode 100644 index 0000000..25f6a3e --- /dev/null +++ b/crates/ming/src/platform.rs @@ -0,0 +1,854 @@ +// todo(linux): remove +#![cfg_attr(target_os = "linux", allow(dead_code))] +// todo(windows): remove +#![cfg_attr(windows, allow(dead_code))] + +mod app_menu; +mod keystroke; + +#[cfg(not(target_os = "macos"))] +mod cosmic_text; + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "macos")] +mod mac; + +#[cfg(any(target_os = "linux", target_os = "windows", feature = "macos-blade"))] +mod blade; + +#[cfg(any(test, feature = "test-support"))] +mod test; + +#[cfg(target_os = "windows")] +mod windows; + +use crate::{ + Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, + DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, Keymap, + LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, + RenderSvgParams, Scene, SharedString, Size, Task, TaskLabel, WindowContext, +}; +use anyhow::Result; +use async_task::Runnable; +use futures::channel::oneshot; +use parking::Unparker; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use seahash::SeaHasher; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::hash::{Hash, Hasher}; +use std::time::Duration; +use std::{ + fmt::{self, Debug}, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +use uuid::Uuid; + +pub use app_menu::*; +pub use keystroke::*; + +#[cfg(not(target_os = "macos"))] +pub(crate) use cosmic_text::*; +#[cfg(target_os = "linux")] +pub(crate) use linux::*; +#[cfg(target_os = "macos")] +pub(crate) use mac::*; +pub use semantic_version::SemanticVersion; +#[cfg(any(test, feature = "test-support"))] +pub(crate) use test::*; +use time::UtcOffset; +#[cfg(target_os = "windows")] +pub(crate) use windows::*; + +#[cfg(target_os = "macos")] +pub(crate) fn current_platform() -> Rc { + Rc::new(MacPlatform::new()) +} +#[cfg(target_os = "linux")] +pub(crate) fn current_platform() -> Rc { + let wayland_display = std::env::var_os("WAYLAND_DISPLAY"); + let x11_display = std::env::var_os("DISPLAY"); + + let use_wayland = wayland_display.is_some_and(|display| !display.is_empty()); + let use_x11 = x11_display.is_some_and(|display| !display.is_empty()); + + if use_wayland { + Rc::new(WaylandClient::new()) + } else if use_x11 { + Rc::new(X11Client::new()) + } else { + Rc::new(HeadlessClient::new()) + } +} +// todo("windows") +#[cfg(target_os = "windows")] +pub(crate) fn current_platform() -> Rc { + Rc::new(WindowsPlatform::new()) +} + +pub(crate) trait Platform: 'static { + fn background_executor(&self) -> BackgroundExecutor; + fn foreground_executor(&self) -> ForegroundExecutor; + fn text_system(&self) -> Arc; + + fn run(&self, on_finish_launching: Box); + fn quit(&self); + fn restart(&self, binary_path: Option); + fn activate(&self, ignoring_other_apps: bool); + fn hide(&self); + fn hide_other_apps(&self); + fn unhide_other_apps(&self); + + fn displays(&self) -> Vec>; + fn primary_display(&self) -> Option>; + fn active_window(&self) -> Option; + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Box; + + /// Returns the appearance of the application's windows. + fn window_appearance(&self) -> WindowAppearance; + + fn open_url(&self, url: &str); + fn on_open_urls(&self, callback: Box)>); + fn register_url_scheme(&self, url: &str) -> Task>; + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>; + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>; + fn reveal_path(&self, path: &Path); + + fn on_quit(&self, callback: Box); + fn on_reopen(&self, callback: Box); + + fn set_menus(&self, menus: Vec, keymap: &Keymap); + fn add_recent_document(&self, _path: &Path) {} + fn on_app_menu_action(&self, callback: Box); + fn on_will_open_app_menu(&self, callback: Box); + fn on_validate_app_menu_command(&self, callback: Box bool>); + + fn os_name(&self) -> &'static str; + fn os_version(&self) -> Result; + fn app_version(&self) -> Result; + fn app_path(&self) -> Result; + fn local_timezone(&self) -> UtcOffset; + fn path_for_auxiliary_executable(&self, name: &str) -> Result; + + fn set_cursor_style(&self, style: CursorStyle); + fn should_auto_hide_scrollbars(&self) -> bool; + + fn write_to_primary(&self, item: ClipboardItem); + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_primary(&self) -> Option; + fn read_from_clipboard(&self) -> Option; + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; + fn read_credentials(&self, url: &str) -> Task)>>>; + fn delete_credentials(&self, url: &str) -> Task>; +} + +/// A handle to a platform's display, e.g. a monitor or laptop screen. +pub trait PlatformDisplay: Send + Sync + Debug { + /// Get the ID for this display + fn id(&self) -> DisplayId; + + /// Returns a stable identifier for this display that can be persisted and used + /// across system restarts. + fn uuid(&self) -> Result; + + /// Get the bounds for this display + fn bounds(&self) -> Bounds; +} + +/// An opaque identifier for a hardware display +#[derive(PartialEq, Eq, Hash, Copy, Clone)] +pub struct DisplayId(pub(crate) u32); + +impl Debug for DisplayId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DisplayId({})", self.0) + } +} + +unsafe impl Send for DisplayId {} + +pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { + fn bounds(&self) -> Bounds; + fn is_maximized(&self) -> bool; + fn window_bounds(&self) -> WindowBounds; + fn content_size(&self) -> Size; + fn scale_factor(&self) -> f32; + fn appearance(&self) -> WindowAppearance; + fn display(&self) -> Rc; + fn mouse_position(&self) -> Point; + fn modifiers(&self) -> Modifiers; + fn set_input_handler(&mut self, input_handler: PlatformInputHandler); + fn take_input_handler(&mut self) -> Option; + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[&str], + ) -> Option>; + fn activate(&self); + fn is_active(&self) -> bool; + fn set_title(&mut self, title: &str); + fn set_app_id(&mut self, app_id: &str); + fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance); + fn set_edited(&mut self, edited: bool); + fn show_character_palette(&self); + fn minimize(&self); + fn zoom(&self); + fn toggle_fullscreen(&self); + fn is_fullscreen(&self) -> bool; + fn on_request_frame(&self, callback: Box); + fn on_input(&self, callback: Box DispatchEventResult>); + fn on_active_status_change(&self, callback: Box); + fn on_resize(&self, callback: Box, f32)>); + fn on_moved(&self, callback: Box); + fn on_should_close(&self, callback: Box bool>); + fn on_close(&self, callback: Box); + fn on_appearance_changed(&self, callback: Box); + fn draw(&self, scene: &Scene); + fn completed_frame(&self) {} + fn sprite_atlas(&self) -> Arc; + + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::HWND; + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&mut self) -> Option<&mut TestWindow> { + None + } +} + +/// This type is public so that our test macro can generate and use it, but it should not +/// be considered part of our public API. +#[doc(hidden)] +pub trait PlatformDispatcher: Send + Sync { + fn is_main_thread(&self) -> bool; + fn dispatch(&self, runnable: Runnable, label: Option); + fn dispatch_on_main_thread(&self, runnable: Runnable); + fn dispatch_after(&self, duration: Duration, runnable: Runnable); + fn park(&self, timeout: Option) -> bool; + fn unparker(&self) -> Unparker; + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&self) -> Option<&TestDispatcher> { + None + } +} + +pub(crate) trait PlatformTextSystem: Send + Sync { + fn add_fonts(&self, fonts: Vec>) -> Result<()>; + fn all_font_names(&self) -> Vec; + fn all_font_families(&self) -> Vec; + fn font_id(&self, descriptor: &Font) -> Result; + fn font_metrics(&self, font_id: FontId) -> FontMetrics; + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)>; + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; +} + +/// Basic metadata about the current application and operating system. +#[derive(Clone, Debug)] +pub struct AppMetadata { + /// The name of the current operating system + pub os_name: &'static str, + + /// The operating system's version + pub os_version: Option, + + /// The current version of the application + pub app_version: Option, +} + +#[derive(PartialEq, Eq, Hash, Clone)] +pub(crate) enum AtlasKey { + Glyph(RenderGlyphParams), + Svg(RenderSvgParams), + Image(RenderImageParams), +} + +impl AtlasKey { + pub(crate) fn texture_kind(&self) -> AtlasTextureKind { + match self { + AtlasKey::Glyph(params) => { + if params.is_emoji { + AtlasTextureKind::Polychrome + } else { + AtlasTextureKind::Monochrome + } + } + AtlasKey::Svg(_) => AtlasTextureKind::Monochrome, + AtlasKey::Image(_) => AtlasTextureKind::Polychrome, + } + } +} + +impl From for AtlasKey { + fn from(params: RenderGlyphParams) -> Self { + Self::Glyph(params) + } +} + +impl From for AtlasKey { + fn from(params: RenderSvgParams) -> Self { + Self::Svg(params) + } +} + +impl From for AtlasKey { + fn from(params: RenderImageParams) -> Self { + Self::Image(params) + } +} + +pub(crate) trait PlatformAtlas: Send + Sync { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result<(Size, Cow<'a, [u8]>)>, + ) -> Result; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[repr(C)] +pub(crate) struct AtlasTile { + pub(crate) texture_id: AtlasTextureId, + pub(crate) tile_id: TileId, + pub(crate) padding: u32, + pub(crate) bounds: Bounds, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(C)] +pub(crate) struct AtlasTextureId { + // We use u32 instead of usize for Metal Shader Language compatibility + pub(crate) index: u32, + pub(crate) kind: AtlasTextureKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(C)] +pub(crate) enum AtlasTextureKind { + Monochrome = 0, + Polychrome = 1, + Path = 2, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(C)] +pub(crate) struct TileId(pub(crate) u32); + +impl From for TileId { + fn from(id: etagere::AllocId) -> Self { + Self(id.serialize()) + } +} + +impl From for etagere::AllocId { + fn from(id: TileId) -> Self { + Self::deserialize(id.0) + } +} + +pub(crate) struct PlatformInputHandler { + cx: AsyncWindowContext, + handler: Box, +} + +impl PlatformInputHandler { + pub fn new(cx: AsyncWindowContext, handler: Box) -> Self { + Self { cx, handler } + } + + fn selected_text_range(&mut self) -> Option> { + self.cx + .update(|cx| self.handler.selected_text_range(cx)) + .ok() + .flatten() + } + + fn marked_text_range(&mut self) -> Option> { + self.cx + .update(|cx| self.handler.marked_text_range(cx)) + .ok() + .flatten() + } + + fn text_for_range(&mut self, range_utf16: Range) -> Option { + self.cx + .update(|cx| self.handler.text_for_range(range_utf16, cx)) + .ok() + .flatten() + } + + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { + self.cx + .update(|cx| { + self.handler + .replace_text_in_range(replacement_range, text, cx); + }) + .ok(); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + self.cx + .update(|cx| { + self.handler.replace_and_mark_text_in_range( + range_utf16, + new_text, + new_selected_range, + cx, + ) + }) + .ok(); + } + + fn unmark_text(&mut self) { + self.cx.update(|cx| self.handler.unmark_text(cx)).ok(); + } + + fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { + self.cx + .update(|cx| self.handler.bounds_for_range(range_utf16, cx)) + .ok() + .flatten() + } + + pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) { + self.handler.replace_text_in_range(None, input, cx); + } +} + +/// Zed's interface for handling text input from the platform's IME system +/// This is currently a 1:1 exposure of the NSTextInputClient API: +/// +/// +pub trait InputHandler: 'static { + /// Get the range of the user's currently selected text, if any + /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option>; + + /// Get the range of the currently marked text, if any + /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option>; + + /// Get the text for the given document range in UTF-16 characters + /// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring) + /// + /// range_utf16 is in terms of UTF-16 characters + fn text_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option; + + /// Replace the text in the given document range with the given text + /// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext) + /// + /// replacement_range is in terms of UTF-16 characters + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + cx: &mut WindowContext, + ); + + /// Replace the text in the given document range with the given text, + /// and mark the given text as part of of an IME 'composing' state + /// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext) + /// + /// range_utf16 is in terms of UTF-16 characters + /// new_selected_range is in terms of UTF-16 characters + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut WindowContext, + ); + + /// Remove the IME 'composing' state from the document + /// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext) + fn unmark_text(&mut self, cx: &mut WindowContext); + + /// Get the bounds of the given document range in screen coordinates + /// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect) + /// + /// This is used for positioning the IME candidate window + fn bounds_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option>; +} + +/// The variables that can be configured when creating a new window +#[derive(Debug)] +pub struct WindowOptions { + /// Specifies the state and bounds of the window in screen coordinates. + /// - `None`: Inherit the bounds. + /// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size. + pub window_bounds: Option, + + /// The titlebar configuration of the window + pub titlebar: Option, + + /// Whether the window should be focused when created + pub focus: bool, + + /// Whether the window should be shown when created + pub show: bool, + + /// The kind of window to create + pub kind: WindowKind, + + /// Whether the window should be movable by the user + pub is_movable: bool, + + /// The display to create the window on, if this is None, + /// the window will be created on the main display + pub display_id: Option, + + /// The appearance of the window background. + pub window_background: WindowBackgroundAppearance, + + /// Application identifier of the window. Can by used by desktop environments to group applications together. + pub app_id: Option, +} + +/// The variables that can be configured when creating a new window +#[derive(Debug)] +pub(crate) struct WindowParams { + pub bounds: Bounds, + + /// The titlebar configuration of the window + pub titlebar: Option, + + /// The kind of window to create + pub kind: WindowKind, + + /// Whether the window should be movable by the user + pub is_movable: bool, + + pub focus: bool, + + pub show: bool, + + pub display_id: Option, + + pub window_background: WindowBackgroundAppearance, +} + +/// Represents the status of how a window should be opened. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum WindowBounds { + /// Indicates that the window should open in a windowed state with the given bounds. + Windowed(Bounds), + /// Indicates that the window should open in a maximized state. + /// The bounds provided here represent the restore size of the window. + Maximized(Bounds), + /// Indicates that the window should open in fullscreen mode. + /// The bounds provided here represent the restore size of the window. + Fullscreen(Bounds), +} + +impl Default for WindowBounds { + fn default() -> Self { + WindowBounds::Windowed(Bounds::default()) + } +} + +impl WindowBounds { + /// Retrieve the inner bounds + pub fn get_bounds(&self) -> Bounds { + match self { + WindowBounds::Windowed(bounds) => *bounds, + WindowBounds::Maximized(bounds) => *bounds, + WindowBounds::Fullscreen(bounds) => *bounds, + } + } +} + +impl Default for WindowOptions { + fn default() -> Self { + Self { + window_bounds: None, + titlebar: Some(TitlebarOptions { + title: Default::default(), + appears_transparent: Default::default(), + traffic_light_position: Default::default(), + }), + focus: true, + show: true, + kind: WindowKind::Normal, + is_movable: true, + display_id: None, + window_background: WindowBackgroundAppearance::default(), + app_id: None, + } + } +} + +/// The options that can be configured for a window's titlebar +#[derive(Debug, Default)] +pub struct TitlebarOptions { + /// The initial title of the window + pub title: Option, + + /// Whether the titlebar should appear transparent + pub appears_transparent: bool, + + /// The position of the macOS traffic light buttons + pub traffic_light_position: Option>, +} + +/// The kind of window to create +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum WindowKind { + /// A normal application window + Normal, + + /// A window that appears above all other windows, usually used for alerts or popups + /// use sparingly! + PopUp, +} + +/// The appearance of the window, as defined by the operating system. +/// +/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance) +/// values. +#[derive(Copy, Clone, Debug)] +pub enum WindowAppearance { + /// A light appearance. + /// + /// On macOS, this corresponds to the `aqua` appearance. + Light, + + /// A light appearance with vibrant colors. + /// + /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance. + VibrantLight, + + /// A dark appearance. + /// + /// On macOS, this corresponds to the `darkAqua` appearance. + Dark, + + /// A dark appearance with vibrant colors. + /// + /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance. + VibrantDark, +} + +impl Default for WindowAppearance { + fn default() -> Self { + Self::Light + } +} + +/// The appearance of the background of the window itself, when there is +/// no content or the content is transparent. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub enum WindowBackgroundAppearance { + /// Opaque. + /// + /// This lets the window manager know that content behind this + /// window does not need to be drawn. + /// + /// Actual color depends on the system and themes should define a fully + /// opaque background color instead. + #[default] + Opaque, + /// Plain alpha transparency. + Transparent, + /// Transparency, but the contents behind the window are blurred. + /// + /// Not always supported. + Blurred, +} + +/// The options that can be configured for a file dialog prompt +#[derive(Copy, Clone, Debug)] +pub struct PathPromptOptions { + /// Should the prompt allow files to be selected? + pub files: bool, + /// Should the prompt allow directories to be selected? + pub directories: bool, + /// Should the prompt allow multiple files to be selected? + pub multiple: bool, +} + +/// What kind of prompt styling to show +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PromptLevel { + /// A prompt that is shown when the user should be notified of something + Info, + + /// A prompt that is shown when the user needs to be warned of a potential problem + Warning, + + /// A prompt that is shown when a critical problem has occurred + Critical, +} + +/// The style of the cursor (pointer) +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum CursorStyle { + /// The default cursor + Arrow, + + /// A text input cursor + /// corresponds to the CSS cursor value `text` + IBeam, + + /// A crosshair cursor + /// corresponds to the CSS cursor value `crosshair` + Crosshair, + + /// A closed hand cursor + /// corresponds to the CSS cursor value `grabbing` + ClosedHand, + + /// An open hand cursor + /// corresponds to the CSS cursor value `grab` + OpenHand, + + /// A pointing hand cursor + /// corresponds to the CSS cursor value `pointer` + PointingHand, + + /// A resize left cursor + /// corresponds to the CSS cursor value `w-resize` + ResizeLeft, + + /// A resize right cursor + /// corresponds to the CSS cursor value `e-resize` + ResizeRight, + + /// A resize cursor to the left and right + /// corresponds to the CSS cursor value `ew-resize` + ResizeLeftRight, + + /// A resize up cursor + /// corresponds to the CSS cursor value `n-resize` + ResizeUp, + + /// A resize down cursor + /// corresponds to the CSS cursor value `s-resize` + ResizeDown, + + /// A resize cursor directing up and down + /// corresponds to the CSS cursor value `ns-resize` + ResizeUpDown, + + /// A cursor indicating that the item/column can be resized horizontally. + /// corresponds to the CSS curosr value `col-resize` + ResizeColumn, + + /// A cursor indicating that the item/row can be resized vertically. + /// corresponds to the CSS curosr value `row-resize` + ResizeRow, + + /// A cursor indicating that something will disappear if moved here + /// Does not correspond to a CSS cursor value + DisappearingItem, + + /// A text input cursor for vertical layout + /// corresponds to the CSS cursor value `vertical-text` + IBeamCursorForVerticalLayout, + + /// A cursor indicating that the operation is not allowed + /// corresponds to the CSS cursor value `not-allowed` + OperationNotAllowed, + + /// A cursor indicating that the operation will result in a link + /// corresponds to the CSS cursor value `alias` + DragLink, + + /// A cursor indicating that the operation will result in a copy + /// corresponds to the CSS cursor value `copy` + DragCopy, + + /// A cursor indicating that the operation will result in a context menu + /// corresponds to the CSS cursor value `context-menu` + ContextualMenu, +} + +impl Default for CursorStyle { + fn default() -> Self { + Self::Arrow + } +} + +/// A clipboard item that should be copied to the clipboard +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardItem { + pub(crate) text: String, + pub(crate) metadata: Option, +} + +impl ClipboardItem { + /// Create a new clipboard item with the given text + pub fn new(text: String) -> Self { + Self { + text, + metadata: None, + } + } + + /// Create a new clipboard item with the given text and metadata + pub fn with_metadata(mut self, metadata: T) -> Self { + self.metadata = Some(serde_json::to_string(&metadata).unwrap()); + self + } + + /// Get the text of the clipboard item + pub fn text(&self) -> &String { + &self.text + } + + /// Get the metadata of the clipboard item + pub fn metadata(&self) -> Option + where + T: for<'a> Deserialize<'a>, + { + self.metadata + .as_ref() + .and_then(|m| serde_json::from_str(m).ok()) + } + + pub(crate) fn text_hash(text: &str) -> u64 { + let mut hasher = SeaHasher::new(); + text.hash(&mut hasher); + hasher.finish() + } +} diff --git a/crates/ming/src/platform/app_menu.rs b/crates/ming/src/platform/app_menu.rs new file mode 100644 index 0000000..91fe358 --- /dev/null +++ b/crates/ming/src/platform/app_menu.rs @@ -0,0 +1,115 @@ +use crate::{Action, AppContext, Platform}; +use util::ResultExt; + +/// A menu of the application, either a main menu or a submenu +pub struct Menu<'a> { + /// The name of the menu + pub name: &'a str, + + /// The items in the menu + pub items: Vec>, +} + +/// The different kinds of items that can be in a menu +pub enum MenuItem<'a> { + /// A separator between items + Separator, + + /// A submenu + Submenu(Menu<'a>), + + /// An action that can be performed + Action { + /// The name of this menu item + name: &'a str, + + /// the action to perform when this menu item is selected + action: Box, + + /// The OS Action that corresponds to this action, if any + /// See [`OsAction`] for more information + os_action: Option, + }, +} + +impl<'a> MenuItem<'a> { + /// Creates a new menu item that is a separator + pub fn separator() -> Self { + Self::Separator + } + + /// Creates a new menu item that is a submenu + pub fn submenu(menu: Menu<'a>) -> Self { + Self::Submenu(menu) + } + + /// Creates a new menu item that invokes an action + pub fn action(name: &'a str, action: impl Action) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: None, + } + } + + /// Creates a new menu item that invokes an action and has an OS action + pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: Some(os_action), + } + } +} + +// TODO: As part of the global selections refactor, these should +// be moved to GPUI-provided actions that make this association +// without leaking the platform details to GPUI users + +/// OS actions are actions that are recognized by the operating system +/// This allows the operating system to provide specialized behavior for +/// these actions +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum OsAction { + /// The 'cut' action + Cut, + + /// The 'copy' action + Copy, + + /// The 'paste' action + Paste, + + /// The 'select all' action + SelectAll, + + /// The 'undo' action + Undo, + + /// The 'redo' action + Redo, +} + +pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) { + platform.on_will_open_app_menu(Box::new({ + let cx = cx.to_async(); + move || { + cx.update(|cx| cx.clear_pending_keystrokes()).ok(); + } + })); + + platform.on_validate_app_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| cx.is_action_available(action)) + .unwrap_or(false) + } + })); + + platform.on_app_menu_action(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| cx.dispatch_action(action)).log_err(); + } + })); +} diff --git a/crates/ming/src/platform/blade.rs b/crates/ming/src/platform/blade.rs new file mode 100644 index 0000000..830f69d --- /dev/null +++ b/crates/ming/src/platform/blade.rs @@ -0,0 +1,8 @@ +mod blade_atlas; +mod blade_belt; +mod blade_renderer; + +pub(crate) use blade_atlas::*; +pub(crate) use blade_renderer::*; + +use blade_belt::*; diff --git a/crates/ming/src/platform/blade/blade_atlas.rs b/crates/ming/src/platform/blade/blade_atlas.rs new file mode 100644 index 0000000..22b2e4f --- /dev/null +++ b/crates/ming/src/platform/blade/blade_atlas.rs @@ -0,0 +1,377 @@ +use super::{BladeBelt, BladeBeltDescriptor}; +use crate::{ + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, + Point, Size, +}; +use anyhow::Result; +use blade_graphics as gpu; +use collections::FxHashMap; +use etagere::BucketedAtlasAllocator; +use parking_lot::Mutex; +use std::{borrow::Cow, ops, sync::Arc}; + +pub(crate) const PATH_TEXTURE_FORMAT: gpu::TextureFormat = gpu::TextureFormat::R16Float; + +pub(crate) struct BladeAtlas(Mutex); + +struct PendingUpload { + id: AtlasTextureId, + bounds: Bounds, + data: gpu::BufferPiece, +} + +struct BladeAtlasState { + gpu: Arc, + upload_belt: BladeBelt, + storage: BladeAtlasStorage, + tiles_by_key: FxHashMap, + initializations: Vec, + uploads: Vec, +} + +#[cfg(gles)] +unsafe impl Send for BladeAtlasState {} + +impl BladeAtlasState { + fn destroy(&mut self) { + self.storage.destroy(&self.gpu); + self.upload_belt.destroy(&self.gpu); + } +} + +pub struct BladeTextureInfo { + pub size: gpu::Extent, + pub raw_view: gpu::TextureView, +} + +impl BladeAtlas { + pub(crate) fn new(gpu: &Arc) -> Self { + BladeAtlas(Mutex::new(BladeAtlasState { + gpu: Arc::clone(gpu), + upload_belt: BladeBelt::new(BladeBeltDescriptor { + memory: gpu::Memory::Upload, + min_chunk_size: 0x10000, + alignment: 64, // Vulkan `optimalBufferCopyOffsetAlignment` on Intel XE + }), + storage: BladeAtlasStorage::default(), + tiles_by_key: Default::default(), + initializations: Vec::new(), + uploads: Vec::new(), + })) + } + + pub(crate) fn destroy(&self) { + self.0.lock().destroy(); + } + + pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { + let mut lock = self.0.lock(); + let textures = &mut lock.storage[texture_kind]; + for texture in textures { + texture.clear(); + } + } + + /// Allocate a rectangle and make it available for rendering immediately (without waiting for `before_frame`) + pub fn allocate_for_rendering( + &self, + size: Size, + texture_kind: AtlasTextureKind, + gpu_encoder: &mut gpu::CommandEncoder, + ) -> AtlasTile { + let mut lock = self.0.lock(); + let tile = lock.allocate(size, texture_kind); + lock.flush_initializations(gpu_encoder); + tile + } + + pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) { + let mut lock = self.0.lock(); + lock.flush(gpu_encoder); + } + + pub fn after_frame(&self, sync_point: &gpu::SyncPoint) { + let mut lock = self.0.lock(); + lock.upload_belt.flush(sync_point); + } + + pub fn get_texture_info(&self, id: AtlasTextureId) -> BladeTextureInfo { + let lock = self.0.lock(); + let texture = &lock.storage[id]; + let size = texture.allocator.size(); + BladeTextureInfo { + size: gpu::Extent { + width: size.width as u32, + height: size.height as u32, + depth: 1, + }, + raw_view: texture.raw_view, + } + } +} + +impl PlatformAtlas for BladeAtlas { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result<(Size, Cow<'a, [u8]>)>, + ) -> Result { + let mut lock = self.0.lock(); + if let Some(tile) = lock.tiles_by_key.get(key) { + Ok(tile.clone()) + } else { + profiling::scope!("new tile"); + let (size, bytes) = build()?; + let tile = lock.allocate(size, key.texture_kind()); + lock.upload_texture(tile.texture_id, tile.bounds, &bytes); + lock.tiles_by_key.insert(key.clone(), tile.clone()); + Ok(tile) + } + } +} + +impl BladeAtlasState { + fn allocate(&mut self, size: Size, texture_kind: AtlasTextureKind) -> AtlasTile { + let textures = &mut self.storage[texture_kind]; + textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + .unwrap_or_else(|| { + let texture = self.push_texture(size, texture_kind); + texture.allocate(size).unwrap() + }) + } + + fn push_texture( + &mut self, + min_size: Size, + kind: AtlasTextureKind, + ) -> &mut BladeAtlasTexture { + const DEFAULT_ATLAS_SIZE: Size = Size { + width: DevicePixels(1024), + height: DevicePixels(1024), + }; + + let size = min_size.max(&DEFAULT_ATLAS_SIZE); + let format; + let usage; + match kind { + AtlasTextureKind::Monochrome => { + format = gpu::TextureFormat::R8Unorm; + usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE; + } + AtlasTextureKind::Polychrome => { + format = gpu::TextureFormat::Bgra8UnormSrgb; + usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE; + } + AtlasTextureKind::Path => { + format = PATH_TEXTURE_FORMAT; + usage = gpu::TextureUsage::COPY + | gpu::TextureUsage::RESOURCE + | gpu::TextureUsage::TARGET; + } + } + + let raw = self.gpu.create_texture(gpu::TextureDesc { + name: "atlas", + format, + size: gpu::Extent { + width: size.width.into(), + height: size.height.into(), + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + dimension: gpu::TextureDimension::D2, + usage, + }); + let raw_view = self.gpu.create_texture_view(gpu::TextureViewDesc { + name: "", + texture: raw, + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }); + + let textures = &mut self.storage[kind]; + let atlas_texture = BladeAtlasTexture { + id: AtlasTextureId { + index: textures.len() as u32, + kind, + }, + allocator: etagere::BucketedAtlasAllocator::new(size.into()), + format, + raw, + raw_view, + }; + + self.initializations.push(atlas_texture.id); + textures.push(atlas_texture); + textures.last_mut().unwrap() + } + + fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds, bytes: &[u8]) { + let data = unsafe { self.upload_belt.alloc_data(bytes, &self.gpu) }; + self.uploads.push(PendingUpload { id, bounds, data }); + } + + fn flush_initializations(&mut self, encoder: &mut gpu::CommandEncoder) { + for id in self.initializations.drain(..) { + let texture = &self.storage[id]; + encoder.init_texture(texture.raw); + } + } + + fn flush(&mut self, encoder: &mut gpu::CommandEncoder) { + self.flush_initializations(encoder); + + let mut transfers = encoder.transfer(); + for upload in self.uploads.drain(..) { + let texture = &self.storage[upload.id]; + transfers.copy_buffer_to_texture( + upload.data, + upload.bounds.size.width.to_bytes(texture.bytes_per_pixel()), + gpu::TexturePiece { + texture: texture.raw, + mip_level: 0, + array_layer: 0, + origin: [ + upload.bounds.origin.x.into(), + upload.bounds.origin.y.into(), + 0, + ], + }, + gpu::Extent { + width: upload.bounds.size.width.into(), + height: upload.bounds.size.height.into(), + depth: 1, + }, + ); + } + } +} + +#[derive(Default)] +struct BladeAtlasStorage { + monochrome_textures: Vec, + polychrome_textures: Vec, + path_textures: Vec, +} + +impl ops::Index for BladeAtlasStorage { + type Output = Vec; + fn index(&self, kind: AtlasTextureKind) -> &Self::Output { + match kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, + } + } +} + +impl ops::IndexMut for BladeAtlasStorage { + fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output { + match kind { + crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + crate::AtlasTextureKind::Path => &mut self.path_textures, + } + } +} + +impl ops::Index for BladeAtlasStorage { + type Output = BladeAtlasTexture; + fn index(&self, id: AtlasTextureId) -> &Self::Output { + let textures = match id.kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, + }; + &textures[id.index as usize] + } +} + +impl BladeAtlasStorage { + fn destroy(&mut self, gpu: &gpu::Context) { + for mut texture in self.monochrome_textures.drain(..) { + texture.destroy(gpu); + } + for mut texture in self.polychrome_textures.drain(..) { + texture.destroy(gpu); + } + for mut texture in self.path_textures.drain(..) { + texture.destroy(gpu); + } + } +} + +struct BladeAtlasTexture { + id: AtlasTextureId, + allocator: BucketedAtlasAllocator, + raw: gpu::Texture, + raw_view: gpu::TextureView, + format: gpu::TextureFormat, +} + +impl BladeAtlasTexture { + fn clear(&mut self) { + self.allocator.clear(); + } + + fn allocate(&mut self, size: Size) -> Option { + let allocation = self.allocator.allocate(size.into())?; + let tile = AtlasTile { + texture_id: self.id, + tile_id: allocation.id.into(), + padding: 0, + bounds: Bounds { + origin: allocation.rectangle.min.into(), + size, + }, + }; + Some(tile) + } + + fn destroy(&mut self, gpu: &gpu::Context) { + gpu.destroy_texture(self.raw); + gpu.destroy_texture_view(self.raw_view); + } + + fn bytes_per_pixel(&self) -> u8 { + self.format.block_info().size + } +} + +impl From> for etagere::Size { + fn from(size: Size) -> Self { + etagere::Size::new(size.width.into(), size.height.into()) + } +} + +impl From for Point { + fn from(value: etagere::Point) -> Self { + Point { + x: DevicePixels::from(value.x), + y: DevicePixels::from(value.y), + } + } +} + +impl From for Size { + fn from(size: etagere::Size) -> Self { + Size { + width: DevicePixels::from(size.width), + height: DevicePixels::from(size.height), + } + } +} + +impl From for Bounds { + fn from(rectangle: etagere::Rectangle) -> Self { + Bounds { + origin: rectangle.min.into(), + size: rectangle.size().into(), + } + } +} diff --git a/crates/ming/src/platform/blade/blade_belt.rs b/crates/ming/src/platform/blade/blade_belt.rs new file mode 100644 index 0000000..322caaa --- /dev/null +++ b/crates/ming/src/platform/blade/blade_belt.rs @@ -0,0 +1,101 @@ +use blade_graphics as gpu; +use std::mem; + +struct ReusableBuffer { + raw: gpu::Buffer, + size: u64, +} + +pub struct BladeBeltDescriptor { + pub memory: gpu::Memory, + pub min_chunk_size: u64, + pub alignment: u64, +} + +/// A belt of buffers, used by the BladeAtlas to cheaply +/// find staging space for uploads. +pub struct BladeBelt { + desc: BladeBeltDescriptor, + buffers: Vec<(ReusableBuffer, gpu::SyncPoint)>, + active: Vec<(ReusableBuffer, u64)>, +} + +impl BladeBelt { + pub fn new(desc: BladeBeltDescriptor) -> Self { + assert_ne!(desc.alignment, 0); + Self { + desc, + buffers: Vec::new(), + active: Vec::new(), + } + } + + pub fn destroy(&mut self, gpu: &gpu::Context) { + for (buffer, _) in self.buffers.drain(..) { + gpu.destroy_buffer(buffer.raw); + } + for (buffer, _) in self.active.drain(..) { + gpu.destroy_buffer(buffer.raw); + } + } + + #[profiling::function] + pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece { + for &mut (ref rb, ref mut offset) in self.active.iter_mut() { + let aligned = offset.next_multiple_of(self.desc.alignment); + if aligned + size <= rb.size { + let piece = rb.raw.at(aligned); + *offset = aligned + size; + return piece; + } + } + + let index_maybe = self + .buffers + .iter() + .position(|(rb, sp)| size <= rb.size && gpu.wait_for(sp, 0)); + if let Some(index) = index_maybe { + let (rb, _) = self.buffers.remove(index); + let piece = rb.raw.into(); + self.active.push((rb, size)); + return piece; + } + + let chunk_index = self.buffers.len() + self.active.len(); + let chunk_size = size.max(self.desc.min_chunk_size); + let chunk = gpu.create_buffer(gpu::BufferDesc { + name: &format!("chunk-{}", chunk_index), + size: chunk_size, + memory: self.desc.memory, + }); + let rb = ReusableBuffer { + raw: chunk, + size: chunk_size, + }; + self.active.push((rb, size)); + chunk.into() + } + + // SAFETY: T should be zeroable and ordinary data, no references, pointers, cells or other complicated data type. + pub unsafe fn alloc_data(&mut self, data: &[T], gpu: &gpu::Context) -> gpu::BufferPiece { + assert!(!data.is_empty()); + let type_alignment = mem::align_of::() as u64; + debug_assert_eq!( + self.desc.alignment % type_alignment, + 0, + "Type alignment {} is too big", + type_alignment + ); + let total_bytes = std::mem::size_of_val(data); + let bp = self.alloc(total_bytes as u64, gpu); + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, bp.data(), total_bytes); + } + bp + } + + pub fn flush(&mut self, sp: &gpu::SyncPoint) { + self.buffers + .extend(self.active.drain(..).map(|(rb, _)| (rb, sp.clone()))); + } +} diff --git a/crates/ming/src/platform/blade/blade_renderer.rs b/crates/ming/src/platform/blade/blade_renderer.rs new file mode 100644 index 0000000..b245089 --- /dev/null +++ b/crates/ming/src/platform/blade/blade_renderer.rs @@ -0,0 +1,753 @@ +// Doing `if let` gives you nice scoping with passes/encoders +#![allow(irrefutable_let_patterns)] + +use super::{BladeAtlas, BladeBelt, BladeBeltDescriptor, PATH_TEXTURE_FORMAT}; +use crate::{ + AtlasTextureKind, AtlasTile, Bounds, ContentMask, Hsla, MonochromeSprite, Path, PathId, + PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, + Underline, +}; +use bytemuck::{Pod, Zeroable}; +use collections::HashMap; +#[cfg(target_os = "macos")] +use media::core_video::CVMetalTextureCache; +#[cfg(target_os = "macos")] +use std::{ffi::c_void, ptr::NonNull}; + +use blade_graphics as gpu; +use std::{mem, sync::Arc}; + +const MAX_FRAME_TIME_MS: u32 = 1000; + +pub type Context = (); +pub type Renderer = BladeRenderer; + +#[cfg(target_os = "macos")] +pub unsafe fn new_renderer( + _context: self::Context, + _native_window: *mut c_void, + native_view: *mut c_void, + bounds: crate::Size, + transparent: bool, +) -> Renderer { + use raw_window_handle as rwh; + struct RawWindow { + view: *mut c_void, + } + + impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result { + let view = NonNull::new(self.view).unwrap(); + let handle = rwh::AppKitWindowHandle::new(view); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } + } + impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result { + let handle = rwh::AppKitDisplayHandle::new(); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } + } + + let gpu = Arc::new( + gpu::Context::init_windowed( + &RawWindow { + view: native_view as *mut _, + }, + gpu::ContextDesc { + validation: cfg!(debug_assertions), + capture: false, + overlay: false, + }, + ) + .unwrap(), + ); + + BladeRenderer::new( + gpu, + BladeSurfaceConfig { + size: gpu::Extent { + width: bounds.width as u32, + height: bounds.height as u32, + depth: 1, + }, + transparent, + }, + ) +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct GlobalParams { + viewport_size: [f32; 2], + premultiplied_alpha: u32, + pad: u32, +} + +//Note: we can't use `Bounds` directly here because +// it doesn't implement Pod + Zeroable +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct PodBounds { + origin: [f32; 2], + size: [f32; 2], +} + +impl From> for PodBounds { + fn from(bounds: Bounds) -> Self { + Self { + origin: [bounds.origin.x.0, bounds.origin.y.0], + size: [bounds.size.width.0, bounds.size.height.0], + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct SurfaceParams { + bounds: PodBounds, + content_mask: PodBounds, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderQuadsData { + globals: GlobalParams, + b_quads: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderShadowsData { + globals: GlobalParams, + b_shadows: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPathRasterizationData { + globals: GlobalParams, + b_path_vertices: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPathsData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_path_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderUnderlinesData { + globals: GlobalParams, + b_underlines: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderMonoSpritesData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_mono_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPolySpritesData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_poly_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderSurfacesData { + globals: GlobalParams, + surface_locals: SurfaceParams, + t_y: gpu::TextureView, + t_cb_cr: gpu::TextureView, + s_surface: gpu::Sampler, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +struct PathSprite { + bounds: Bounds, + color: Hsla, + tile: AtlasTile, +} + +struct BladePipelines { + quads: gpu::RenderPipeline, + shadows: gpu::RenderPipeline, + path_rasterization: gpu::RenderPipeline, + paths: gpu::RenderPipeline, + underlines: gpu::RenderPipeline, + mono_sprites: gpu::RenderPipeline, + poly_sprites: gpu::RenderPipeline, + surfaces: gpu::RenderPipeline, +} + +impl BladePipelines { + fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo) -> Self { + use gpu::ShaderData as _; + + log::info!( + "Initializing Blade pipelines for surface {:?}", + surface_info + ); + let shader = gpu.create_shader(gpu::ShaderDesc { + source: include_str!("shaders.wgsl"), + }); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + assert_eq!( + mem::size_of::>(), + shader.get_struct_size("PathVertex") as usize, + ); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + + // See https://apoorvaj.io/alpha-compositing-opengl-blending-and-premultiplied-alpha/ + let blend_mode = match surface_info.alpha { + gpu::AlphaMode::Ignored => gpu::BlendState::ALPHA_BLENDING, + gpu::AlphaMode::PreMultiplied => gpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, + gpu::AlphaMode::PostMultiplied => gpu::BlendState::ALPHA_BLENDING, + }; + let color_targets = &[gpu::ColorTargetState { + format: surface_info.format, + blend: Some(blend_mode), + write_mask: gpu::ColorWrites::default(), + }]; + + Self { + quads: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "quads", + data_layouts: &[&ShaderQuadsData::layout()], + vertex: shader.at("vs_quad"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_quad"), + color_targets, + }), + shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "shadows", + data_layouts: &[&ShaderShadowsData::layout()], + vertex: shader.at("vs_shadow"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_shadow"), + color_targets, + }), + path_rasterization: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "path_rasterization", + data_layouts: &[&ShaderPathRasterizationData::layout()], + vertex: shader.at("vs_path_rasterization"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_path_rasterization"), + color_targets: &[gpu::ColorTargetState { + format: PATH_TEXTURE_FORMAT, + blend: Some(gpu::BlendState::ADDITIVE), + write_mask: gpu::ColorWrites::default(), + }], + }), + paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "paths", + data_layouts: &[&ShaderPathsData::layout()], + vertex: shader.at("vs_path"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_path"), + color_targets, + }), + underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "underlines", + data_layouts: &[&ShaderUnderlinesData::layout()], + vertex: shader.at("vs_underline"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_underline"), + color_targets, + }), + mono_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "mono-sprites", + data_layouts: &[&ShaderMonoSpritesData::layout()], + vertex: shader.at("vs_mono_sprite"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_mono_sprite"), + color_targets, + }), + poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "poly-sprites", + data_layouts: &[&ShaderPolySpritesData::layout()], + vertex: shader.at("vs_poly_sprite"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_poly_sprite"), + color_targets, + }), + surfaces: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "surfaces", + data_layouts: &[&ShaderSurfacesData::layout()], + vertex: shader.at("vs_surface"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_surface"), + color_targets, + }), + } + } +} + +pub struct BladeSurfaceConfig { + pub size: gpu::Extent, + pub transparent: bool, +} + +pub struct BladeRenderer { + gpu: Arc, + surface_config: gpu::SurfaceConfig, + alpha_mode: gpu::AlphaMode, + command_encoder: gpu::CommandEncoder, + last_sync_point: Option, + pipelines: BladePipelines, + instance_belt: BladeBelt, + path_tiles: HashMap, + atlas: Arc, + atlas_sampler: gpu::Sampler, + #[cfg(target_os = "macos")] + core_video_texture_cache: CVMetalTextureCache, +} + +impl BladeRenderer { + pub fn new(gpu: Arc, config: BladeSurfaceConfig) -> Self { + let surface_config = gpu::SurfaceConfig { + size: config.size, + usage: gpu::TextureUsage::TARGET, + display_sync: gpu::DisplaySync::Recent, + color_space: gpu::ColorSpace::Linear, + allow_exclusive_full_screen: false, + transparent: config.transparent, + }; + let surface_info = gpu.resize(surface_config); + + let command_encoder = gpu.create_command_encoder(gpu::CommandEncoderDesc { + name: "main", + buffer_count: 2, + }); + let pipelines = BladePipelines::new(&gpu, surface_info); + let instance_belt = BladeBelt::new(BladeBeltDescriptor { + memory: gpu::Memory::Shared, + min_chunk_size: 0x1000, + alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe + }); + let atlas = Arc::new(BladeAtlas::new(&gpu)); + let atlas_sampler = gpu.create_sampler(gpu::SamplerDesc { + name: "atlas", + mag_filter: gpu::FilterMode::Linear, + min_filter: gpu::FilterMode::Linear, + ..Default::default() + }); + + #[cfg(target_os = "macos")] + let core_video_texture_cache = unsafe { + use foreign_types::ForeignType as _; + CVMetalTextureCache::new(gpu.metal_device().as_ptr()).unwrap() + }; + + Self { + gpu, + surface_config, + alpha_mode: surface_info.alpha, + command_encoder, + last_sync_point: None, + pipelines, + instance_belt, + path_tiles: HashMap::default(), + atlas, + atlas_sampler, + #[cfg(target_os = "macos")] + core_video_texture_cache, + } + } + + fn wait_for_gpu(&mut self) { + if let Some(last_sp) = self.last_sync_point.take() { + if !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) { + panic!("GPU hung"); + } + } + } + + pub fn update_drawable_size(&mut self, size: Size) { + let gpu_size = gpu::Extent { + width: size.width as u32, + height: size.height as u32, + depth: 1, + }; + + if gpu_size != self.surface_config.size { + self.wait_for_gpu(); + self.surface_config.size = gpu_size; + self.gpu.resize(self.surface_config); + } + } + + pub fn update_transparency(&mut self, transparent: bool) { + if transparent != self.surface_config.transparent { + self.wait_for_gpu(); + self.surface_config.transparent = transparent; + let surface_info = self.gpu.resize(self.surface_config); + self.pipelines = BladePipelines::new(&self.gpu, surface_info); + self.alpha_mode = surface_info.alpha; + } + } + + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub fn viewport_size(&self) -> gpu::Extent { + self.surface_config.size + } + + pub fn sprite_atlas(&self) -> &Arc { + &self.atlas + } + + #[cfg(target_os = "macos")] + pub fn layer(&self) -> metal::MetalLayer { + self.gpu.metal_layer().unwrap() + } + + #[cfg(target_os = "macos")] + pub fn layer_ptr(&self) -> *mut metal::CAMetalLayer { + use metal::foreign_types::ForeignType as _; + self.gpu.metal_layer().unwrap().as_ptr() + } + + #[profiling::function] + fn rasterize_paths(&mut self, paths: &[Path]) { + self.path_tiles.clear(); + let mut vertices_by_texture_id = HashMap::default(); + + for path in paths { + let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds); + let tile = self.atlas.allocate_for_rendering( + clipped_bounds.size.map(Into::into), + AtlasTextureKind::Path, + &mut self.command_encoder, + ); + vertices_by_texture_id + .entry(tile.texture_id) + .or_insert(Vec::new()) + .extend(path.vertices.iter().map(|vertex| PathVertex { + xy_position: vertex.xy_position - clipped_bounds.origin + + tile.bounds.origin.map(Into::into), + st_position: vertex.st_position, + content_mask: ContentMask { + bounds: tile.bounds.map(Into::into), + }, + })); + self.path_tiles.insert(path.id, tile); + } + + for (texture_id, vertices) in vertices_by_texture_id { + let tex_info = self.atlas.get_texture_info(texture_id); + let globals = GlobalParams { + viewport_size: [tex_info.size.width as f32, tex_info.size.height as f32], + premultiplied_alpha: 0, + pad: 0, + }; + + let vertex_buf = unsafe { self.instance_belt.alloc_data(&vertices, &self.gpu) }; + let mut pass = self.command_encoder.render(gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: tex_info.raw_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }); + + let mut encoder = pass.with(&self.pipelines.path_rasterization); + encoder.bind( + 0, + &ShaderPathRasterizationData { + globals, + b_path_vertices: vertex_buf, + }, + ); + encoder.draw(0, vertices.len() as u32, 0, 1); + } + } + + pub fn destroy(&mut self) { + self.wait_for_gpu(); + self.atlas.destroy(); + self.instance_belt.destroy(&self.gpu); + self.gpu.destroy_command_encoder(&mut self.command_encoder); + } + + pub fn draw(&mut self, scene: &Scene) { + self.command_encoder.start(); + self.atlas.before_frame(&mut self.command_encoder); + self.rasterize_paths(scene.paths()); + + let frame = { + profiling::scope!("acquire frame"); + self.gpu.acquire_frame() + }; + self.command_encoder.init_texture(frame.texture()); + + let globals = GlobalParams { + viewport_size: [ + self.surface_config.size.width as f32, + self.surface_config.size.height as f32, + ], + premultiplied_alpha: match self.alpha_mode { + gpu::AlphaMode::Ignored | gpu::AlphaMode::PostMultiplied => 0, + gpu::AlphaMode::PreMultiplied => 1, + }, + pad: 0, + }; + + if let mut pass = self.command_encoder.render(gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }) { + profiling::scope!("render pass"); + for batch in scene.batches() { + match batch { + PrimitiveBatch::Quads(quads) => { + let instance_buf = + unsafe { self.instance_belt.alloc_data(quads, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.quads); + encoder.bind( + 0, + &ShaderQuadsData { + globals, + b_quads: instance_buf, + }, + ); + encoder.draw(0, 4, 0, quads.len() as u32); + } + PrimitiveBatch::Shadows(shadows) => { + let instance_buf = + unsafe { self.instance_belt.alloc_data(shadows, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.shadows); + encoder.bind( + 0, + &ShaderShadowsData { + globals, + b_shadows: instance_buf, + }, + ); + encoder.draw(0, 4, 0, shadows.len() as u32); + } + PrimitiveBatch::Paths(paths) => { + let mut encoder = pass.with(&self.pipelines.paths); + // todo(linux): group by texture ID + for path in paths { + let tile = &self.path_tiles[&path.id]; + let tex_info = self.atlas.get_texture_info(tile.texture_id); + let origin = path.bounds.intersect(&path.content_mask.bounds).origin; + let sprites = [PathSprite { + bounds: Bounds { + origin: origin.map(|p| p.floor()), + size: tile.bounds.size.map(Into::into), + }, + color: path.color, + tile: (*tile).clone(), + }]; + + let instance_buf = + unsafe { self.instance_belt.alloc_data(&sprites, &self.gpu) }; + encoder.bind( + 0, + &ShaderPathsData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_path_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + } + PrimitiveBatch::Underlines(underlines) => { + let instance_buf = + unsafe { self.instance_belt.alloc_data(underlines, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.underlines); + encoder.bind( + 0, + &ShaderUnderlinesData { + globals, + b_underlines: instance_buf, + }, + ); + encoder.draw(0, 4, 0, underlines.len() as u32); + } + PrimitiveBatch::MonochromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = + unsafe { self.instance_belt.alloc_data(sprites, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.mono_sprites); + encoder.bind( + 0, + &ShaderMonoSpritesData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_mono_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::PolychromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = + unsafe { self.instance_belt.alloc_data(sprites, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.poly_sprites); + encoder.bind( + 0, + &ShaderPolySpritesData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_poly_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::Surfaces(surfaces) => { + let mut _encoder = pass.with(&self.pipelines.surfaces); + + for surface in surfaces { + #[cfg(not(target_os = "macos"))] + { + let _ = surface; + continue; + }; + + #[cfg(target_os = "macos")] + { + let (t_y, t_cb_cr) = { + use core_foundation::base::TCFType as _; + use std::ptr; + + assert_eq!( + surface.image_buffer.pixel_format_type(), + media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ); + + let y_texture = unsafe { + self.core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + metal::MTLPixelFormat::R8Unorm, + surface.image_buffer.plane_width(0), + surface.image_buffer.plane_height(0), + 0, + ) + .unwrap() + }; + let cb_cr_texture = unsafe { + self.core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + metal::MTLPixelFormat::RG8Unorm, + surface.image_buffer.plane_width(1), + surface.image_buffer.plane_height(1), + 1, + ) + .unwrap() + }; + ( + gpu::TextureView::from_metal_texture( + y_texture.as_texture_ref(), + ), + gpu::TextureView::from_metal_texture( + cb_cr_texture.as_texture_ref(), + ), + ) + }; + + _encoder.bind( + 0, + &ShaderSurfacesData { + globals, + surface_locals: SurfaceParams { + bounds: surface.bounds.into(), + content_mask: surface.content_mask.bounds.into(), + }, + t_y, + t_cb_cr, + s_surface: self.atlas_sampler, + }, + ); + + _encoder.draw(0, 4, 0, 1); + } + } + } + } + } + } + + self.command_encoder.present(frame); + let sync_point = self.gpu.submit(&mut self.command_encoder); + + profiling::scope!("finish"); + self.instance_belt.flush(&sync_point); + self.atlas.after_frame(&sync_point); + self.atlas.clear_textures(AtlasTextureKind::Path); + + self.wait_for_gpu(); + self.last_sync_point = Some(sync_point); + } +} diff --git a/crates/ming/src/platform/blade/shaders.wgsl b/crates/ming/src/platform/blade/shaders.wgsl new file mode 100644 index 0000000..4a4d924 --- /dev/null +++ b/crates/ming/src/platform/blade/shaders.wgsl @@ -0,0 +1,647 @@ +struct GlobalParams { + viewport_size: vec2, + premultiplied_alpha: u32, + pad: u32, +} + +var globals: GlobalParams; +var t_sprite: texture_2d; +var s_sprite: sampler; + +const M_PI_F: f32 = 3.1415926; +const GRAYSCALE_FACTORS: vec3 = vec3(0.2126, 0.7152, 0.0722); + +struct Bounds { + origin: vec2, + size: vec2, +} +struct Corners { + top_left: f32, + top_right: f32, + bottom_right: f32, + bottom_left: f32, +} +struct Edges { + top: f32, + right: f32, + bottom: f32, + left: f32, +} +struct Hsla { + h: f32, + s: f32, + l: f32, + a: f32, +} + +struct AtlasTextureId { + index: u32, + kind: u32, +} + +struct AtlasBounds { + origin: vec2, + size: vec2, +} +struct AtlasTile { + texture_id: AtlasTextureId, + tile_id: u32, + padding: u32, + bounds: AtlasBounds, +} + +struct TransformationMatrix { + rotation_scale: mat2x2, + translation: vec2, +} + +fn to_device_position_impl(position: vec2) -> vec4 { + let device_position = position / globals.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0); + return vec4(device_position, 0.0, 1.0); +} + +fn to_device_position(unit_vertex: vec2, bounds: Bounds) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + return to_device_position_impl(position); +} + +fn to_device_position_transformed(unit_vertex: vec2, bounds: Bounds, transform: TransformationMatrix) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + //Note: Rust side stores it as row-major, so transposing here + let transformed = transpose(transform.rotation_scale) * position + transform.translation; + return to_device_position_impl(transformed); +} + +fn to_tile_position(unit_vertex: vec2, tile: AtlasTile) -> vec2 { + let atlas_size = vec2(textureDimensions(t_sprite, 0)); + return (vec2(tile.bounds.origin) + unit_vertex * vec2(tile.bounds.size)) / atlas_size; +} + +fn distance_from_clip_rect_impl(position: vec2, clip_bounds: Bounds) -> vec4 { + let tl = position - clip_bounds.origin; + let br = clip_bounds.origin + clip_bounds.size - position; + return vec4(tl.x, br.x, tl.y, br.y); +} + +fn distance_from_clip_rect(unit_vertex: vec2, bounds: Bounds, clip_bounds: Bounds) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + return distance_from_clip_rect_impl(position, clip_bounds); +} + +// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl +fn srgb_to_linear(srgb: vec3) -> vec3 { + let cutoff = srgb < vec3(0.04045); + let higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + let lower = srgb / vec3(12.92); + return select(higher, lower, cutoff); +} + +fn hsla_to_rgba(hsla: Hsla) -> vec4 { + let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range + let s = hsla.s; + let l = hsla.l; + let a = hsla.a; + + let c = (1.0 - abs(2.0 * l - 1.0)) * s; + let x = c * (1.0 - abs(h % 2.0 - 1.0)); + let m = l - c / 2.0; + var color = vec3(m); + + if (h >= 0.0 && h < 1.0) { + color.r += c; + color.g += x; + } else if (h >= 1.0 && h < 2.0) { + color.r += x; + color.g += c; + } else if (h >= 2.0 && h < 3.0) { + color.g += c; + color.b += x; + } else if (h >= 3.0 && h < 4.0) { + color.g += x; + color.b += c; + } else if (h >= 4.0 && h < 5.0) { + color.r += x; + color.b += c; + } else { + color.r += c; + color.b += x; + } + + // Input colors are assumed to be in sRGB space, + // but blending and rendering needs to happen in linear space. + // The output will be converted to sRGB by either the target + // texture format or the swapchain color space. + let linear = srgb_to_linear(color); + return vec4(linear, a); +} + +fn over(below: vec4, above: vec4) -> vec4 { + let alpha = above.a + below.a * (1.0 - above.a); + let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; + return vec4(color, alpha); +} + +// A standard gaussian function, used for weighting samples +fn gaussian(x: f32, sigma: f32) -> f32{ + return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +fn erf(v: vec2) -> vec2 { + let s = sign(v); + let a = abs(v); + let r1 = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + let r2 = r1 * r1; + return s - s / (r2 * r2); +} + +fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2) -> f32 { + let delta = min(half_size.y - corner - abs(y), 0.0); + let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); + let integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +fn pick_corner_radius(point: vec2, radii: Corners) -> f32 { + if (point.x < 0.0) { + if (point.y < 0.0) { + return radii.top_left; + } else { + return radii.bottom_left; + } + } else { + if (point.y < 0.0) { + return radii.top_right; + } else { + return radii.bottom_right; + } + } +} + +fn quad_sdf(point: vec2, bounds: Bounds, corner_radii: Corners) -> f32 { + let half_size = bounds.size / 2.0; + let center = bounds.origin + half_size; + let center_to_point = point - center; + let corner_radius = pick_corner_radius(center_to_point, corner_radii); + let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius; + return length(max(vec2(0.0), rounded_edge_to_point)) + + min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - + corner_radius; +} + +// Abstract away the final color transformation based on the +// target alpha compositing mode. +fn blend_color(color: vec4, alpha_factor: f32) -> vec4 { + let alpha = color.a * alpha_factor; + let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u); + return vec4(color.rgb * multiplier, alpha); +} + +// --- quads --- // + +struct Quad { + order: u32, + pad: u32, + bounds: Bounds, + content_mask: Bounds, + background: Hsla, + border_color: Hsla, + corner_radii: Corners, + border_widths: Edges, +} +var b_quads: array; + +struct QuadVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) background_color: vec4, + @location(1) @interpolate(flat) border_color: vec4, + @location(2) @interpolate(flat) quad_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let quad = b_quads[instance_id]; + + var out = QuadVarying(); + out.position = to_device_position(unit_vertex, quad.bounds); + out.background_color = hsla_to_rgba(quad.background); + out.border_color = hsla_to_rgba(quad.border_color); + out.quad_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); + return out; +} + +@fragment +fn fs_quad(input: QuadVarying) -> @location(0) vec4 { + // Alpha clip first, since we don't have `clip_distance`. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let quad = b_quads[input.quad_id]; + // Fast path when the quad is not rounded and doesn't have any border. + if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 && + quad.corner_radii.top_right == 0.0 && + quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 && + quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 && + quad.border_widths.bottom == 0.0) { + return blend_color(input.background_color, 1.0); + } + + let half_size = quad.bounds.size / 2.0; + let center = quad.bounds.origin + half_size; + let center_to_point = input.position.xy - center; + + let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); + + let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius; + let distance = + length(max(vec2(0.0), rounded_edge_to_point)) + + min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - + corner_radius; + + let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0); + let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0); + let inset_size = half_size - corner_radius - vec2(vertical_border, horizontal_border); + let point_to_inset_corner = abs(center_to_point) - inset_size; + + var border_width = 0.0; + if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) { + border_width = 0.0; + } else if (point_to_inset_corner.y > point_to_inset_corner.x) { + border_width = horizontal_border; + } else { + border_width = vertical_border; + } + + var color = input.background_color; + if (border_width > 0.0) { + let inset_distance = distance + border_width; + // Blend the border on top of the background and then linearly interpolate + // between the two as we slide inside the background. + let blended_border = over(input.background_color, input.border_color); + color = mix(blended_border, input.background_color, + saturate(0.5 - inset_distance)); + } + + return blend_color(color, saturate(0.5 - distance)); +} + +// --- shadows --- // + +struct Shadow { + order: u32, + blur_radius: f32, + bounds: Bounds, + corner_radii: Corners, + content_mask: Bounds, + color: Hsla, +} +var b_shadows: array; + +struct ShadowVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) color: vec4, + @location(1) @interpolate(flat) shadow_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + var shadow = b_shadows[instance_id]; + + let margin = 3.0 * shadow.blur_radius; + // Set the bounds of the shadow and adjust its size based on the shadow's + // spread radius to achieve the spreading effect + shadow.bounds.origin -= vec2(margin); + shadow.bounds.size += 2.0 * vec2(margin); + + var out = ShadowVarying(); + out.position = to_device_position(unit_vertex, shadow.bounds); + out.color = hsla_to_rgba(shadow.color); + out.shadow_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask); + return out; +} + +@fragment +fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { + // Alpha clip first, since we don't have `clip_distance`. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let shadow = b_shadows[input.shadow_id]; + let half_size = shadow.bounds.size / 2.0; + let center = shadow.bounds.origin + half_size; + let center_to_point = input.position.xy - center; + + let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii); + + // The signal is only non-zero in a limited range, so don't waste samples + let low = center_to_point.y - half_size.y; + let high = center_to_point.y + half_size.y; + let start = clamp(-3.0 * shadow.blur_radius, low, high); + let end = clamp(3.0 * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + let step = (end - start) / 4.0; + var y = start + step * 0.5; + var alpha = 0.0; + for (var i = 0; i < 4; i += 1) { + let blur = blur_along_x(center_to_point.x, center_to_point.y - y, + shadow.blur_radius, corner_radius, half_size); + alpha += blur * gaussian(y, shadow.blur_radius) * step; + y += step; + } + + return blend_color(input.color, alpha); +} + +// --- path rasterization --- // + +struct PathVertex { + xy_position: vec2, + st_position: vec2, + content_mask: Bounds, +} +var b_path_vertices: array; + +struct PathRasterizationVarying { + @builtin(position) position: vec4, + @location(0) st_position: vec2, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying { + let v = b_path_vertices[vertex_id]; + + var out = PathRasterizationVarying(); + out.position = to_device_position_impl(v.xy_position); + out.st_position = v.st_position; + out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask); + return out; +} + +@fragment +fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 { + let dx = dpdx(input.st_position); + let dy = dpdy(input.st_position); + if (any(input.clip_distances < vec4(0.0))) { + return 0.0; + } + + let gradient = 2.0 * input.st_position.xx * vec2(dx.x, dy.x) - vec2(dx.y, dy.y); + let f = input.st_position.x * input.st_position.x - input.st_position.y; + let distance = f / length(gradient); + return saturate(0.5 - distance); +} + +// --- paths --- // + +struct PathSprite { + bounds: Bounds, + color: Hsla, + tile: AtlasTile, +} +var b_path_sprites: array; + +struct PathVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) color: vec4, +} + +@vertex +fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_path_sprites[instance_id]; + // Don't apply content mask because it was already accounted for when rasterizing the path. + + var out = PathVarying(); + out.position = to_device_position(unit_vertex, sprite.bounds); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.color = hsla_to_rgba(sprite.color); + return out; +} + +@fragment +fn fs_path(input: PathVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; + let mask = 1.0 - abs(1.0 - sample % 2.0); + return blend_color(input.color, mask); +} + +// --- underlines --- // + +struct Underline { + order: u32, + pad: u32, + bounds: Bounds, + content_mask: Bounds, + color: Hsla, + thickness: f32, + wavy: u32, +} +var b_underlines: array; + +struct UnderlineVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) color: vec4, + @location(1) @interpolate(flat) underline_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let underline = b_underlines[instance_id]; + + var out = UnderlineVarying(); + out.position = to_device_position(unit_vertex, underline.bounds); + out.color = hsla_to_rgba(underline.color); + out.underline_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask); + return out; +} + +@fragment +fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { + // Alpha clip first, since we don't have `clip_distance`. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let underline = b_underlines[input.underline_id]; + if ((underline.wavy & 0xFFu) == 0u) + { + return vec4(0.0); + } + + let half_thickness = underline.thickness * 0.5; + let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2(0.0, 0.5); + let frequency = M_PI_F * 3.0 * underline.thickness / 8.0; + let amplitude = 1.0 / (2.0 * underline.thickness); + let sine = sin(st.x * frequency) * amplitude; + let dSine = cos(st.x * frequency) * amplitude * frequency; + let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine); + let distance_in_pixels = distance * underline.bounds.size.y; + let distance_from_top_border = distance_in_pixels - half_thickness; + let distance_from_bottom_border = distance_in_pixels + half_thickness; + let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border)); + return blend_color(input.color, alpha); +} + +// --- monochrome sprites --- // + +struct MonochromeSprite { + order: u32, + pad: u32, + bounds: Bounds, + content_mask: Bounds, + color: Hsla, + tile: AtlasTile, + transformation: TransformationMatrix, +} +var b_mono_sprites: array; + +struct MonoSpriteVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) color: vec4, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_mono_sprites[instance_id]; + + var out = MonoSpriteVarying(); + out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); + + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.color = hsla_to_rgba(sprite.color); + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + return out; +} + +@fragment +fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + return blend_color(input.color, sample); +} + +// --- polychrome sprites --- // + +struct PolychromeSprite { + order: u32, + grayscale: u32, + bounds: Bounds, + content_mask: Bounds, + corner_radii: Corners, + tile: AtlasTile, +} +var b_poly_sprites: array; + +struct PolySpriteVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) sprite_id: u32, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_poly_sprites[instance_id]; + + var out = PolySpriteVarying(); + out.position = to_device_position(unit_vertex, sprite.bounds); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.sprite_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + return out; +} + +@fragment +fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position); + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let sprite = b_poly_sprites[input.sprite_id]; + let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); + + var color = sample; + if ((sprite.grayscale & 0xFFu) != 0u) { + let grayscale = dot(color.rgb, GRAYSCALE_FACTORS); + color = vec4(vec3(grayscale), sample.a); + } + return blend_color(color, saturate(0.5 - distance)); +} + +// --- surfaces --- // + +struct SurfaceParams { + bounds: Bounds, + content_mask: Bounds, +} + +var surface_locals: SurfaceParams; +var t_y: texture_2d; +var t_cb_cr: texture_2d; +var s_surface: sampler; + +const ycbcr_to_RGB = mat4x4( + vec4( 1.0000f, 1.0000f, 1.0000f, 0.0), + vec4( 0.0000f, -0.3441f, 1.7720f, 0.0), + vec4( 1.4020f, -0.7141f, 0.0000f, 0.0), + vec4(-0.7010f, 0.5291f, -0.8860f, 1.0), +); + +struct SurfaceVarying { + @builtin(position) position: vec4, + @location(0) texture_position: vec2, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + + var out = SurfaceVarying(); + out.position = to_device_position(unit_vertex, surface_locals.bounds); + out.texture_position = unit_vertex; + out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask); + return out; +} + +@fragment +fn fs_surface(input: SurfaceVarying) -> @location(0) vec4 { + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let y_cb_cr = vec4( + textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r, + textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg, + 1.0); + + return ycbcr_to_RGB * y_cb_cr; +} diff --git a/crates/ming/src/platform/cosmic_text.rs b/crates/ming/src/platform/cosmic_text.rs new file mode 100644 index 0000000..f7a54b6 --- /dev/null +++ b/crates/ming/src/platform/cosmic_text.rs @@ -0,0 +1,3 @@ +mod text_system; + +pub(crate) use text_system::*; diff --git a/crates/ming/src/platform/cosmic_text/text_system.rs b/crates/ming/src/platform/cosmic_text/text_system.rs new file mode 100644 index 0000000..c1c5f5a --- /dev/null +++ b/crates/ming/src/platform/cosmic_text/text_system.rs @@ -0,0 +1,516 @@ +use crate::{ + point, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, + FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, + ShapedGlyph, SharedString, Size, +}; +use anyhow::{anyhow, Context, Ok, Result}; +use collections::HashMap; +use cosmic_text::{ + Attrs, AttrsList, BufferLine, CacheKey, Family, Font as CosmicTextFont, FontSystem, SwashCache, +}; + +use itertools::Itertools; +use parking_lot::RwLock; +use pathfinder_geometry::{ + rect::{RectF, RectI}, + vector::{Vector2F, Vector2I}, +}; +use smallvec::SmallVec; +use std::{borrow::Cow, sync::Arc}; + +pub(crate) struct CosmicTextSystem(RwLock); + +struct CosmicTextSystemState { + swash_cache: SwashCache, + font_system: FontSystem, + /// Contains all already loaded fonts, including all faces. Indexed by `FontId`. + loaded_fonts_store: Vec>, + /// Caches the `FontId`s associated with a specific family to avoid iterating the font database + /// for every font face in a family. + font_ids_by_family_cache: HashMap>, + /// The name of each font associated with the given font id + postscript_names: HashMap, +} + +impl CosmicTextSystem { + pub(crate) fn new() -> Self { + let mut font_system = FontSystem::new(); + + // todo(linux) make font loading non-blocking + font_system.db_mut().load_system_fonts(); + + Self(RwLock::new(CosmicTextSystemState { + font_system, + swash_cache: SwashCache::new(), + loaded_fonts_store: Vec::new(), + font_ids_by_family_cache: HashMap::default(), + postscript_names: HashMap::default(), + })) + } +} + +impl Default for CosmicTextSystem { + fn default() -> Self { + Self::new() + } +} + +impl PlatformTextSystem for CosmicTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } + + // todo(linux) ensure that this integrates with platform font loading + // do we need to do more than call load_system_fonts()? + fn all_font_names(&self) -> Vec { + self.0 + .read() + .font_system + .db() + .faces() + .map(|face| face.post_script_name.clone()) + .collect() + } + + fn all_font_families(&self) -> Vec { + self.0 + .read() + .font_system + .db() + .faces() + // todo(linux) this will list the same font family multiple times + .filter_map(|face| face.families.first().map(|family| family.0.clone())) + .collect_vec() + } + + fn font_id(&self, font: &Font) -> Result { + // todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit? + let mut state = self.0.write(); + + let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) { + font_ids.as_slice() + } else { + let font_ids = state.load_family(&font.family, &font.features)?; + state + .font_ids_by_family_cache + .insert(font.family.clone(), font_ids); + state.font_ids_by_family_cache[&font.family].as_ref() + }; + + // todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here + let candidate_properties = candidates + .iter() + .map(|font_id| { + let database_id = state.loaded_fonts_store[font_id.0].id(); + let face_info = state.font_system.db().face(database_id).expect(""); + face_info_into_properties(face_info) + }) + .collect::>(); + + let ix = + font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font)) + .context("requested font family contains no font matching the other parameters")?; + + Ok(candidates[ix]) + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + let metrics = self.0.read().loaded_fonts_store[font_id.0] + .as_swash() + .metrics(&[]); + + FontMetrics { + units_per_em: metrics.units_per_em as u32, + ascent: metrics.ascent, + descent: -metrics.descent, // todo(linux) confirm this is correct + line_gap: metrics.leading, + underline_position: metrics.underline_offset, + underline_thickness: metrics.stroke_size, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + // todo(linux): Compute this correctly + bounding_box: Bounds { + origin: point(0.0, 0.0), + size: size(metrics.max_width, metrics.ascent + metrics.descent), + }, + } + } + + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + let lock = self.0.read(); + let glyph_metrics = lock.loaded_fonts_store[font_id.0] + .as_swash() + .glyph_metrics(&[]); + let glyph_id = glyph_id.0 as u16; + // todo(linux): Compute this correctly + // see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620 + Ok(Bounds { + origin: point(0.0, 0.0), + size: size( + glyph_metrics.advance_width(glyph_id), + glyph_metrics.advance_height(glyph_id), + ), + }) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + self.0.read().advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + self.0.write().raster_bounds(params) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + self.0.write().rasterize_glyph(params, raster_bounds) + } + + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + self.0.write().layout_line(text, font_size, runs) + } +} + +impl CosmicTextSystemState { + #[profiling::function] + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + let db = self.font_system.db_mut(); + for bytes in fonts { + match bytes { + Cow::Borrowed(embedded_font) => { + db.load_font_data(embedded_font.to_vec()); + } + Cow::Owned(bytes) => { + db.load_font_data(bytes); + } + } + } + Ok(()) + } + + // todo(linux) handle `FontFeatures` + #[profiling::function] + fn load_family( + &mut self, + name: &str, + _features: &FontFeatures, + ) -> Result> { + // TODO: Determine the proper system UI font. + let name = if name == ".SystemUIFont" { + "Zed Sans" + } else { + name + }; + + let mut font_ids = SmallVec::new(); + let families = self + .font_system + .db() + .faces() + .filter(|face| face.families.iter().any(|family| *name == family.0)) + .map(|face| (face.id, face.post_script_name.clone())) + .collect::>(); + + for (font_id, postscript_name) in families { + let font = self + .font_system + .get_font(font_id) + .ok_or_else(|| anyhow!("Could not load font"))?; + + // HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback. + let allowed_bad_font_names = [ + "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent + "Segoe Fluent Icons", + ]; + + if font.as_swash().charmap().map('m') == 0 + && !allowed_bad_font_names.contains(&postscript_name.as_str()) + { + self.font_system.db_mut().remove_face(font.id()); + continue; + }; + + let font_id = FontId(self.loaded_fonts_store.len()); + font_ids.push(font_id); + self.loaded_fonts_store.push(font); + self.postscript_names.insert(font_id, postscript_name); + } + + Ok(font_ids) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + let width = self.loaded_fonts_store[font_id.0] + .as_swash() + .glyph_metrics(&[]) + .advance_width(glyph_id.0 as u16); + let height = self.loaded_fonts_store[font_id.0] + .as_swash() + .glyph_metrics(&[]) + .advance_height(glyph_id.0 as u16); + Ok(Size { width, height }) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + let glyph_id = self.loaded_fonts_store[font_id.0] + .as_swash() + .charmap() + .map(ch); + if glyph_id == 0 { + None + } else { + Some(GlyphId(glyph_id.into())) + } + } + + fn is_emoji(&self, font_id: FontId) -> bool { + // TODO: Include other common emoji fonts + self.postscript_names + .get(&font_id) + .map_or(false, |postscript_name| postscript_name == "NotoColorEmoji") + } + + fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result> { + let font = &self.loaded_fonts_store[params.font_id.0]; + let font_system = &mut self.font_system; + let image = self + .swash_cache + .get_image( + font_system, + CacheKey::new( + font.id(), + params.glyph_id.0 as u16, + (params.font_size * params.scale_factor).into(), + (0.0, 0.0), + cosmic_text::CacheKeyFlags::empty(), + ) + .0, + ) + .clone() + .unwrap(); + Ok(Bounds { + origin: point(image.placement.left.into(), (-image.placement.top).into()), + size: size(image.placement.width.into(), image.placement.height.into()), + }) + } + + #[profiling::function] + fn rasterize_glyph( + &mut self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { + if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { + Err(anyhow!("glyph bounds are empty")) + } else { + // todo(linux) handle subpixel variants + let bitmap_size = glyph_bounds.size; + let font = &self.loaded_fonts_store[params.font_id.0]; + let font_system = &mut self.font_system; + let image = self + .swash_cache + .get_image( + font_system, + CacheKey::new( + font.id(), + params.glyph_id.0 as u16, + (params.font_size * params.scale_factor).into(), + (0.0, 0.0), + cosmic_text::CacheKeyFlags::empty(), + ) + .0, + ) + .clone() + .unwrap(); + + Ok((bitmap_size, image.data)) + } + } + + fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId { + if let Some(ix) = self + .loaded_fonts_store + .iter() + .position(|font| font.id() == id) + { + FontId(ix) + } else { + // This matches the behavior of the mac text system + let font = self.font_system.get_font(id).unwrap(); + let face = self + .font_system + .db() + .faces() + .find(|info| info.id == id) + .unwrap(); + + let font_id = FontId(self.loaded_fonts_store.len()); + self.loaded_fonts_store.push(font); + self.postscript_names + .insert(font_id, face.post_script_name.clone()); + + font_id + } + } + + // todo(linux) This is all a quick first pass, maybe we should be using cosmic_text::Buffer + #[profiling::function] + fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + let mut attrs_list = AttrsList::new(Attrs::new()); + let mut offs = 0; + for run in font_runs { + // todo(linux) We need to check we are doing utf properly + let font = &self.loaded_fonts_store[run.font_id.0]; + let font = self.font_system.db().face(font.id()).unwrap(); + attrs_list.add_span( + offs..(offs + run.len), + Attrs::new() + .family(Family::Name(&font.families.first().unwrap().0)) + .stretch(font.stretch) + .style(font.style) + .weight(font.weight), + ); + offs += run.len; + } + let mut line = BufferLine::new(text, attrs_list, cosmic_text::Shaping::Advanced); + + let layout = line.layout( + &mut self.font_system, + font_size.0, + f32::MAX, // We do our own wrapping + cosmic_text::Wrap::None, + None, + ); + let mut runs = Vec::new(); + + let layout = layout.first().unwrap(); + for glyph in &layout.glyphs { + let font_id = glyph.font_id; + let font_id = self.font_id_for_cosmic_id(font_id); + let mut glyphs = SmallVec::new(); + // todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction + glyphs.push(ShapedGlyph { + id: GlyphId(glyph.glyph_id as u32), + position: point((glyph.x).into(), glyph.y.into()), + index: glyph.start, + is_emoji: self.is_emoji(font_id), + }); + + runs.push(crate::ShapedRun { font_id, glyphs }); + } + + LineLayout { + font_size, + width: layout.w.into(), + ascent: layout.max_ascent.into(), + descent: layout.max_descent.into(), + runs, + len: text.len(), + } + } +} + +impl From for Bounds { + fn from(rect: RectF) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())), + size: size(DevicePixels(rect.width()), DevicePixels(rect.height())), + } + } +} + +impl From for Size { + fn from(value: Vector2I) -> Self { + size(value.x().into(), value.y().into()) + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From> for Vector2I { + fn from(size: Point) -> Self { + Vector2I::new(size.x as i32, size.y as i32) + } +} + +impl From for Size { + fn from(vec: Vector2F) -> Self { + size(vec.x(), vec.y()) + } +} + +impl From for cosmic_text::Weight { + fn from(value: FontWeight) -> Self { + cosmic_text::Weight(value.0 as u16) + } +} + +impl From for cosmic_text::Style { + fn from(style: FontStyle) -> Self { + match style { + FontStyle::Normal => cosmic_text::Style::Normal, + FontStyle::Italic => cosmic_text::Style::Italic, + FontStyle::Oblique => cosmic_text::Style::Oblique, + } + } +} + +fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties { + font_kit::properties::Properties { + style: match font.style { + crate::FontStyle::Normal => font_kit::properties::Style::Normal, + crate::FontStyle::Italic => font_kit::properties::Style::Italic, + crate::FontStyle::Oblique => font_kit::properties::Style::Oblique, + }, + weight: font_kit::properties::Weight(font.weight.0), + stretch: Default::default(), + } +} + +fn face_info_into_properties( + face_info: &cosmic_text::fontdb::FaceInfo, +) -> font_kit::properties::Properties { + font_kit::properties::Properties { + style: match face_info.style { + cosmic_text::Style::Normal => font_kit::properties::Style::Normal, + cosmic_text::Style::Italic => font_kit::properties::Style::Italic, + cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique, + }, + // both libs use the same values for weight + weight: font_kit::properties::Weight(face_info.weight.0.into()), + stretch: match face_info.stretch { + cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED, + cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED, + cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED, + cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED, + cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL, + cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED, + cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED, + cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED, + cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED, + }, + } +} diff --git a/crates/ming/src/platform/keystroke.rs b/crates/ming/src/platform/keystroke.rs new file mode 100644 index 0000000..55f8658 --- /dev/null +++ b/crates/ming/src/platform/keystroke.rs @@ -0,0 +1,307 @@ +use anyhow::anyhow; +use serde::Deserialize; +use smallvec::SmallVec; +use std::fmt::Write; + +/// A keystroke and associated metadata generated by the platform +#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] +pub struct Keystroke { + /// the state of the modifier keys at the time the keystroke was generated + pub modifiers: Modifiers, + + /// key is the character printed on the key that was pressed + /// e.g. for option-s, key is "s" + pub key: String, + + /// ime_key is the character inserted by the IME engine when that key was pressed. + /// e.g. for option-s, ime_key is "ß" + pub ime_key: Option, +} + +impl Keystroke { + /// When matching a key we cannot know whether the user intended to type + /// the ime_key or the key itself. On some non-US keyboards keys we use in our + /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), + /// and on some keyboards the IME handler converts a sequence of keys into a + /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). + /// + /// This method generates a list of potential keystroke candidates that could be matched + /// against when resolving a keybinding. + pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { + let mut possibilities = SmallVec::new(); + match self.ime_key.as_ref() { + Some(ime_key) => { + if ime_key != &self.key { + possibilities.push(Keystroke { + modifiers: Modifiers { + control: self.modifiers.control, + alt: false, + shift: false, + platform: false, + function: false, + }, + key: ime_key.to_string(), + ime_key: None, + }); + } + possibilities.push(Keystroke { + ime_key: None, + ..self.clone() + }); + } + None => possibilities.push(self.clone()), + } + possibilities + } + + /// key syntax is: + /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key] + /// ime_key syntax is only used for generating test events, + /// when matching a key with an ime_key set will be matched without it. + pub fn parse(source: &str) -> anyhow::Result { + let mut control = false; + let mut alt = false; + let mut shift = false; + let mut platform = false; + let mut function = false; + let mut key = None; + let mut ime_key = None; + + let mut components = source.split('-').peekable(); + while let Some(component) = components.next() { + match component { + "ctrl" => control = true, + "alt" => alt = true, + "shift" => shift = true, + "fn" => function = true, + "cmd" | "super" | "win" => platform = true, + _ => { + if let Some(next) = components.peek() { + if next.is_empty() && source.ends_with('-') { + key = Some(String::from("-")); + break; + } else if next.len() > 1 && next.starts_with('>') { + key = Some(String::from(component)); + ime_key = Some(String::from(&next[1..])); + components.next(); + } else { + return Err(anyhow!("Invalid keystroke `{}`", source)); + } + } else { + key = Some(String::from(component)); + } + } + } + } + + let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?; + + Ok(Keystroke { + modifiers: Modifiers { + control, + alt, + shift, + platform, + function, + }, + key, + ime_key, + }) + } + + /// Returns a new keystroke with the ime_key filled. + /// This is used for dispatch_keystroke where we want users to + /// be able to simulate typing "space", etc. + pub fn with_simulated_ime(mut self) -> Self { + if self.ime_key.is_none() + && !self.modifiers.platform + && !self.modifiers.control + && !self.modifiers.function + && !self.modifiers.alt + { + self.ime_key = match self.key.as_str() { + "space" => Some(" ".into()), + "tab" => Some("\t".into()), + "enter" => Some("\n".into()), + "up" | "down" | "left" | "right" | "pageup" | "pagedown" | "home" | "end" + | "delete" | "escape" | "backspace" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6" + | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => None, + key => { + if self.modifiers.shift { + Some(key.to_uppercase()) + } else { + Some(key.into()) + } + } + } + } + self + } +} + +impl std::fmt::Display for Keystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.modifiers.control { + f.write_char('^')?; + } + if self.modifiers.alt { + f.write_char('⌥')?; + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(target_os = "linux")] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if self.modifiers.shift { + f.write_char('⇧')?; + } + let key = match self.key.as_str() { + "backspace" => '⌫', + "up" => '↑', + "down" => '↓', + "left" => '←', + "right" => '→', + "tab" => '⇥', + "escape" => '⎋', + key => { + if key.len() == 1 { + key.chars().next().unwrap().to_ascii_uppercase() + } else { + return f.write_str(key); + } + } + }; + f.write_char(key) + } +} + +/// The state of the modifier keys at some point in time +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] +pub struct Modifiers { + /// The control key + pub control: bool, + + /// The alt key + /// Sometimes also known as the 'meta' key + pub alt: bool, + + /// The shift key + pub shift: bool, + + /// The command key, on macos + /// the windows key, on windows + /// the super key, on linux + pub platform: bool, + + /// The function key + pub function: bool, +} + +impl Modifiers { + /// Returns true if any modifier key is pressed + pub fn modified(&self) -> bool { + self.control || self.alt || self.shift || self.platform || self.function + } + + /// Whether the semantically 'secondary' modifier key is pressed + /// On macos, this is the command key + /// On windows and linux, this is the control key + pub fn secondary(&self) -> bool { + #[cfg(target_os = "macos")] + { + return self.platform; + } + + #[cfg(not(target_os = "macos"))] + { + return self.control; + } + } + + /// helper method for Modifiers with no modifiers + pub fn none() -> Modifiers { + Default::default() + } + + /// helper method for Modifiers with just the command key + pub fn command() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// A helper method for Modifiers with just the secondary key pressed + pub fn secondary_key() -> Modifiers { + #[cfg(target_os = "macos")] + { + Modifiers { + platform: true, + ..Default::default() + } + } + + #[cfg(not(target_os = "macos"))] + { + Modifiers { + control: true, + ..Default::default() + } + } + } + + /// helper method for Modifiers with just the windows key + pub fn windows() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// helper method for Modifiers with just the super key + pub fn super_key() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// helper method for Modifiers with just control + pub fn control() -> Modifiers { + Modifiers { + control: true, + ..Default::default() + } + } + + /// helper method for Modifiers with just shift + pub fn shift() -> Modifiers { + Modifiers { + shift: true, + ..Default::default() + } + } + + /// helper method for Modifiers with command + shift + pub fn command_shift() -> Modifiers { + Modifiers { + shift: true, + platform: true, + ..Default::default() + } + } + + /// Checks if this Modifiers is a subset of another Modifiers + pub fn is_subset_of(&self, other: &Modifiers) -> bool { + (other.control || !self.control) + && (other.alt || !self.alt) + && (other.shift || !self.shift) + && (other.platform || !self.platform) + && (other.function || !self.function) + } +} diff --git a/crates/ming/src/platform/linux.rs b/crates/ming/src/platform/linux.rs new file mode 100644 index 0000000..1628e22 --- /dev/null +++ b/crates/ming/src/platform/linux.rs @@ -0,0 +1,14 @@ +// todo(linux): remove +#![allow(unused)] + +mod dispatcher; +mod headless; +mod platform; +mod wayland; +mod x11; + +pub(crate) use dispatcher::*; +pub(crate) use headless::*; +pub(crate) use platform::*; +pub(crate) use wayland::*; +pub(crate) use x11::*; diff --git a/crates/ming/src/platform/linux/dispatcher.rs b/crates/ming/src/platform/linux/dispatcher.rs new file mode 100644 index 0000000..8be712d --- /dev/null +++ b/crates/ming/src/platform/linux/dispatcher.rs @@ -0,0 +1,125 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// todo(linux): remove +#![allow(unused_variables)] + +use crate::{PlatformDispatcher, TaskLabel}; +use async_task::Runnable; +use calloop::{ + channel::{self, Sender}, + timer::TimeoutAction, + EventLoop, +}; +use parking::{Parker, Unparker}; +use parking_lot::Mutex; +use std::{thread, time::Duration}; +use util::ResultExt; + +struct TimerAfter { + duration: Duration, + runnable: Runnable, +} + +pub(crate) struct LinuxDispatcher { + parker: Mutex, + main_sender: Sender, + timer_sender: Sender, + background_sender: flume::Sender, + _background_threads: Vec>, + main_thread_id: thread::ThreadId, +} + +impl LinuxDispatcher { + pub fn new(main_sender: Sender) -> Self { + let (background_sender, background_receiver) = flume::unbounded::(); + let thread_count = std::thread::available_parallelism() + .map(|i| i.get()) + .unwrap_or(1); + + let mut background_threads = (0..thread_count) + .map(|_| { + let receiver = background_receiver.clone(); + std::thread::spawn(move || { + for runnable in receiver { + runnable.run(); + } + }) + }) + .collect::>(); + + let (timer_sender, timer_channel) = calloop::channel::channel::(); + let timer_thread = std::thread::spawn(|| { + let mut event_loop: EventLoop<()> = + EventLoop::try_new().expect("Failed to initialize timer loop!"); + + let handle = event_loop.handle(); + let timer_handle = event_loop.handle(); + handle + .insert_source(timer_channel, move |e, _, _| { + if let channel::Event::Msg(timer) = e { + // This has to be in an option to satisfy the borrow checker. The callback below should only be scheduled once. + let mut runnable = Some(timer.runnable); + timer_handle + .insert_source( + calloop::timer::Timer::from_duration(timer.duration), + move |e, _, _| { + if let Some(runnable) = runnable.take() { + runnable.run(); + } + TimeoutAction::Drop + }, + ) + .expect("Failed to start timer"); + } + }) + .expect("Failed to start timer thread"); + + event_loop.run(None, &mut (), |_| {}).log_err(); + }); + + background_threads.push(timer_thread); + + Self { + parker: Mutex::new(Parker::new()), + main_sender, + timer_sender, + background_sender, + _background_threads: background_threads, + main_thread_id: thread::current().id(), + } + } +} + +impl PlatformDispatcher for LinuxDispatcher { + fn is_main_thread(&self) -> bool { + thread::current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: Runnable, _: Option) { + self.background_sender.send(runnable).unwrap(); + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + self.main_sender.send(runnable).ok(); + } + + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + self.timer_sender + .send(TimerAfter { duration, runnable }) + .ok(); + } + + fn park(&self, timeout: Option) -> bool { + if let Some(timeout) = timeout { + self.parker.lock().park_timeout(timeout) + } else { + self.parker.lock().park(); + true + } + } + + fn unparker(&self) -> Unparker { + self.parker.lock().unparker() + } +} diff --git a/crates/ming/src/platform/linux/headless.rs b/crates/ming/src/platform/linux/headless.rs new file mode 100644 index 0000000..2237aeb --- /dev/null +++ b/crates/ming/src/platform/linux/headless.rs @@ -0,0 +1,3 @@ +mod client; + +pub(crate) use client::*; diff --git a/crates/ming/src/platform/linux/headless/client.rs b/crates/ming/src/platform/linux/headless/client.rs new file mode 100644 index 0000000..c02646a --- /dev/null +++ b/crates/ming/src/platform/linux/headless/client.rs @@ -0,0 +1,105 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; +use std::time::{Duration, Instant}; + +use calloop::{EventLoop, LoopHandle}; +use collections::HashMap; + +use util::ResultExt; + +use crate::platform::linux::LinuxClient; +use crate::platform::{LinuxCommon, PlatformWindow}; +use crate::{ + px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels, + PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, +}; + +use calloop::{ + generic::{FdWrapper, Generic}, + RegistrationToken, +}; + +pub struct HeadlessClientState { + pub(crate) loop_handle: LoopHandle<'static, HeadlessClient>, + pub(crate) event_loop: Option>, + pub(crate) common: LinuxCommon, +} + +#[derive(Clone)] +pub(crate) struct HeadlessClient(Rc>); + +impl HeadlessClient { + pub(crate) fn new() -> Self { + let event_loop = EventLoop::try_new().unwrap(); + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }); + + HeadlessClient(Rc::new(RefCell::new(HeadlessClientState { + event_loop: Some(event_loop), + loop_handle: handle, + common, + }))) + } +} + +impl LinuxClient for HeadlessClient { + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn displays(&self) -> Vec> { + vec![] + } + + fn primary_display(&self) -> Option> { + None + } + + fn display(&self, id: DisplayId) -> Option> { + None + } + + fn open_window( + &self, + _handle: AnyWindowHandle, + params: WindowParams, + ) -> Box { + unimplemented!() + } + + fn set_cursor_style(&self, _style: CursorStyle) {} + + fn open_uri(&self, _uri: &str) {} + + fn write_to_primary(&self, item: crate::ClipboardItem) {} + + fn write_to_clipboard(&self, item: crate::ClipboardItem) {} + + fn read_from_primary(&self) -> Option { + None + } + + fn read_from_clipboard(&self) -> Option { + None + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } +} diff --git a/crates/ming/src/platform/linux/platform.rs b/crates/ming/src/platform/linux/platform.rs new file mode 100644 index 0000000..159e6e9 --- /dev/null +++ b/crates/ming/src/platform/linux/platform.rs @@ -0,0 +1,703 @@ +#![allow(unused)] + +use std::any::{type_name, Any}; +use std::cell::{self, RefCell}; +use std::env; +use std::fs::File; +use std::io::Read; +use std::ops::{Deref, DerefMut}; +use std::os::fd::{AsRawFd, FromRawFd}; +use std::panic::Location; +use std::{ + path::{Path, PathBuf}, + process::Command, + rc::Rc, + sync::Arc, + time::Duration, +}; + +use anyhow::anyhow; +use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest}; +use async_task::Runnable; +use calloop::channel::Channel; +use calloop::{EventLoop, LoopHandle, LoopSignal}; +use copypasta::ClipboardProvider; +use filedescriptor::FileDescriptor; +use flume::{Receiver, Sender}; +use futures::channel::oneshot; +use parking_lot::Mutex; +use time::UtcOffset; +use wayland_client::Connection; +use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; +use xkbcommon::xkb::{self, Keycode, Keysym, State}; + +use crate::platform::linux::wayland::WaylandClient; +use crate::{ + px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle, + DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, Modifiers, + PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem, + PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, WindowAppearance, + WindowOptions, WindowParams, +}; + +use super::x11::X11Client; + +pub(crate) const SCROLL_LINES: f64 = 3.0; + +// Values match the defaults on GTK. +// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320 +pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400); +pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0); +pub(crate) const KEYRING_LABEL: &str = "zed-github-account"; + +pub trait LinuxClient { + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; + fn displays(&self) -> Vec>; + fn primary_display(&self) -> Option>; + fn display(&self, id: DisplayId) -> Option>; + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Box; + fn set_cursor_style(&self, style: CursorStyle); + fn open_uri(&self, uri: &str); + fn write_to_primary(&self, item: ClipboardItem); + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_primary(&self) -> Option; + fn read_from_clipboard(&self) -> Option; + fn run(&self); +} + +#[derive(Default)] +pub(crate) struct PlatformHandlers { + pub(crate) open_urls: Option)>>, + pub(crate) quit: Option>, + pub(crate) reopen: Option>, + pub(crate) app_menu_action: Option>, + pub(crate) will_open_app_menu: Option>, + pub(crate) validate_app_menu_command: Option bool>>, +} + +pub(crate) struct LinuxCommon { + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + pub(crate) text_system: Arc, + pub(crate) callbacks: PlatformHandlers, + pub(crate) signal: LoopSignal, +} + +impl LinuxCommon { + pub fn new(signal: LoopSignal) -> (Self, Channel) { + let (main_sender, main_receiver) = calloop::channel::channel::(); + let text_system = Arc::new(CosmicTextSystem::new()); + let callbacks = PlatformHandlers::default(); + + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); + + let common = LinuxCommon { + background_executor: BackgroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher.clone()), + text_system, + callbacks, + signal, + }; + + (common, main_receiver) + } +} + +impl Platform for P { + fn background_executor(&self) -> BackgroundExecutor { + self.with_common(|common| common.background_executor.clone()) + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.with_common(|common| common.foreground_executor.clone()) + } + + fn text_system(&self) -> Arc { + self.with_common(|common| common.text_system.clone()) + } + + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); + + LinuxClient::run(self); + + self.with_common(|common| { + if let Some(mut fun) = common.callbacks.quit.take() { + fun(); + } + }); + } + + fn quit(&self) { + self.with_common(|common| common.signal.stop()); + } + + fn restart(&self, binary_path: Option) { + use std::os::unix::process::CommandExt as _; + + // get the process id of the current process + let app_pid = std::process::id().to_string(); + // get the path to the executable + let app_path = if let Some(path) = binary_path { + path + } else { + match self.app_path() { + Ok(path) => path, + Err(err) => { + log::error!("Failed to get app path: {:?}", err); + return; + } + } + }; + + log::info!("Restarting process, using app path: {:?}", app_path); + + // Script to wait for the current process to exit and then restart the app. + // We also wait for possibly open TCP sockets by the process to be closed, + // since on Linux it's not guaranteed that a process' resources have been + // cleaned up when `kill -0` returns. + let script = format!( + r#" + while kill -O {pid} 2>/dev/null; do + sleep 0.1 + done + + while lsof -nP -iTCP -a -p {pid} 2>/dev/null; do + sleep 0.1 + done + + {app_path} + "#, + pid = app_pid, + app_path = app_path.display() + ); + + // execute the script using /bin/bash + let restart_process = Command::new("/bin/bash") + .arg("-c") + .arg(script) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + // todo(linux) + fn activate(&self, ignoring_other_apps: bool) {} + + // todo(linux) + fn hide(&self) {} + + fn hide_other_apps(&self) { + log::warn!("hide_other_apps is not implemented on Linux, ignoring the call") + } + + // todo(linux) + fn unhide_other_apps(&self) {} + + fn primary_display(&self) -> Option> { + self.primary_display() + } + + fn displays(&self) -> Vec> { + self.displays() + } + + // todo(linux) + fn active_window(&self) -> Option { + None + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Box { + self.open_window(handle, options) + } + + fn open_url(&self, url: &str) { + self.open_uri(url); + } + + fn on_open_urls(&self, callback: Box)>) { + self.with_common(|common| common.callbacks.open_urls = Some(callback)); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + let (done_tx, done_rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + let title = if options.multiple { + if !options.files { + "Open folders" + } else { + "Open files" + } + } else { + if !options.files { + "Open folder" + } else { + "Open file" + } + }; + + let result = OpenFileRequest::default() + .modal(true) + .title(title) + .accept_label("Select") + .multiple(options.multiple) + .directory(options.directories) + .send() + .await + .ok() + .and_then(|request| request.response().ok()) + .and_then(|response| { + response + .uris() + .iter() + .map(|uri| uri.to_file_path().ok()) + .collect() + }); + + done_tx.send(result); + }) + .detach(); + done_rx + } + + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + let (done_tx, done_rx) = oneshot::channel(); + let directory = directory.to_owned(); + self.foreground_executor() + .spawn(async move { + let result = SaveFileRequest::default() + .modal(true) + .title("Select new path") + .accept_label("Accept") + .send() + .await + .ok() + .and_then(|request| request.response().ok()) + .and_then(|response| { + response + .uris() + .first() + .and_then(|uri| uri.to_file_path().ok()) + }); + + done_tx.send(result); + }) + .detach(); + + done_rx + } + + fn reveal_path(&self, path: &Path) { + if path.is_dir() { + open::that(path); + return; + } + // If `path` is a file, the system may try to open it in a text editor + let dir = path.parent().unwrap_or(Path::new("")); + open::that(dir); + } + + fn on_quit(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.quit = Some(callback); + }); + } + + fn on_reopen(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.reopen = Some(callback); + }); + } + + fn on_app_menu_action(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.app_menu_action = Some(callback); + }); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.with_common(|common| { + common.callbacks.will_open_app_menu = Some(callback); + }); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.with_common(|common| { + common.callbacks.validate_app_menu_command = Some(callback); + }); + } + + fn os_name(&self) -> &'static str { + "Linux" + } + + fn os_version(&self) -> Result { + Ok(SemanticVersion::new(1, 0, 0)) + } + + fn app_version(&self) -> Result { + const VERSION: Option<&str> = option_env!("RELEASE_VERSION"); + if let Some(version) = VERSION { + version.parse() + } else { + Ok(SemanticVersion::new(1, 0, 0)) + } + } + + fn app_path(&self) -> Result { + // get the path of the executable of the current process + let exe_path = std::env::current_exe()?; + Ok(exe_path) + } + + // todo(linux) + fn set_menus(&self, menus: Vec, keymap: &Keymap) {} + + fn local_timezone(&self) -> UtcOffset { + UtcOffset::UTC + } + + //todo(linux) + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + Err(anyhow::Error::msg( + "Platform::path_for_auxiliary_executable is not implemented yet", + )) + } + + fn set_cursor_style(&self, style: CursorStyle) { + self.set_cursor_style(style) + } + + // todo(linux) + fn should_auto_hide_scrollbars(&self) -> bool { + false + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + keyring + .create_item( + KEYRING_LABEL, + &vec![("url", &url), ("username", &username)], + password, + true, + ) + .await?; + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + let attributes = item.attributes().await?; + let username = attributes + .get("username") + .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?; + let secret = item.secret().await?; + + // we lose the zeroizing capabilities at this boundary, + // a current limitation GPUI's credentials api + return Ok(Some((username.to_string(), secret.to_vec()))); + } else { + continue; + } + } + Ok(None) + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + item.delete().await?; + return Ok(()); + } + } + + Ok(()) + }) + } + + fn window_appearance(&self) -> crate::WindowAppearance { + crate::WindowAppearance::Light + } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } + + fn write_to_primary(&self, item: ClipboardItem) { + self.write_to_primary(item) + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + self.write_to_clipboard(item) + } + + fn read_from_primary(&self) -> Option { + self.read_from_primary() + } + + fn read_from_clipboard(&self) -> Option { + self.read_from_clipboard() + } +} + +pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) { + let mut last_err = None; + for mut command in open::commands(uri) { + if let Some(token) = activation_token { + command.env("XDG_ACTIVATION_TOKEN", token); + } + match command.status() { + Ok(_) => return, + Err(err) => last_err = Some(err), + } + } + log::error!("failed to open uri: {uri:?}, last error: {last_err:?}"); +} + +pub(super) fn is_within_click_distance(a: Point, b: Point) -> bool { + let diff = a - b; + diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE +} + +pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result { + let mut file = File::from_raw_fd(fd.as_raw_fd()); + + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + + // Normalize the text to unix line endings, otherwise + // copying from eg: firefox inserts a lot of blank + // lines, and that is super annoying. + let result = buffer.replace("\r\n", "\n"); + Ok(result) +} + +impl CursorStyle { + pub(super) fn to_shape(&self) -> Shape { + match self { + CursorStyle::Arrow => Shape::Default, + CursorStyle::IBeam => Shape::Text, + CursorStyle::Crosshair => Shape::Crosshair, + CursorStyle::ClosedHand => Shape::Grabbing, + CursorStyle::OpenHand => Shape::Grab, + CursorStyle::PointingHand => Shape::Pointer, + CursorStyle::ResizeLeft => Shape::WResize, + CursorStyle::ResizeRight => Shape::EResize, + CursorStyle::ResizeLeftRight => Shape::EwResize, + CursorStyle::ResizeUp => Shape::NResize, + CursorStyle::ResizeDown => Shape::SResize, + CursorStyle::ResizeUpDown => Shape::NsResize, + CursorStyle::ResizeColumn => Shape::ColResize, + CursorStyle::ResizeRow => Shape::RowResize, + CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux + CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText, + CursorStyle::OperationNotAllowed => Shape::NotAllowed, + CursorStyle::DragLink => Shape::Alias, + CursorStyle::DragCopy => Shape::Copy, + CursorStyle::ContextualMenu => Shape::ContextMenu, + } + } + + pub(super) fn to_icon_name(&self) -> String { + // Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME) + // and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from + // Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values + match self { + CursorStyle::Arrow => "arrow", + CursorStyle::IBeam => "text", + CursorStyle::Crosshair => "crosshair", + CursorStyle::ClosedHand => "grabbing", + CursorStyle::OpenHand => "grab", + CursorStyle::PointingHand => "pointer", + CursorStyle::ResizeLeft => "w-resize", + CursorStyle::ResizeRight => "e-resize", + CursorStyle::ResizeLeftRight => "ew-resize", + CursorStyle::ResizeUp => "n-resize", + CursorStyle::ResizeDown => "s-resize", + CursorStyle::ResizeUpDown => "ns-resize", + CursorStyle::ResizeColumn => "col-resize", + CursorStyle::ResizeRow => "row-resize", + CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux + CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", + CursorStyle::OperationNotAllowed => "not-allowed", + CursorStyle::DragLink => "alias", + CursorStyle::DragCopy => "copy", + CursorStyle::ContextualMenu => "context-menu", + } + .to_string() + } +} + +impl Keystroke { + pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self { + let mut modifiers = modifiers; + + let key_utf32 = state.key_get_utf32(keycode); + let key_utf8 = state.key_get_utf8(keycode); + let key_sym = state.key_get_one_sym(keycode); + + // The logic here tries to replicate the logic in `../mac/events.rs` + // "Consumed" modifiers are modifiers that have been used to translate a key, for example + // pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift. + // Notes: + // - macOS gets the key character directly ("."), xkb gives us the key name ("period") + // - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{" + // - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A" + + let mut handle_consumed_modifiers = true; + let key = match key_sym { + Keysym::Return => "enter".to_owned(), + Keysym::Prior => "pageup".to_owned(), + Keysym::Next => "pagedown".to_owned(), + + Keysym::comma => ",".to_owned(), + Keysym::period => ".".to_owned(), + Keysym::less => "<".to_owned(), + Keysym::greater => ">".to_owned(), + Keysym::slash => "/".to_owned(), + Keysym::question => "?".to_owned(), + + Keysym::semicolon => ";".to_owned(), + Keysym::colon => ":".to_owned(), + Keysym::apostrophe => "'".to_owned(), + Keysym::quotedbl => "\"".to_owned(), + + Keysym::bracketleft => "[".to_owned(), + Keysym::braceleft => "{".to_owned(), + Keysym::bracketright => "]".to_owned(), + Keysym::braceright => "}".to_owned(), + Keysym::backslash => "\\".to_owned(), + Keysym::bar => "|".to_owned(), + + Keysym::grave => "`".to_owned(), + Keysym::asciitilde => "~".to_owned(), + Keysym::exclam => "!".to_owned(), + Keysym::at => "@".to_owned(), + Keysym::numbersign => "#".to_owned(), + Keysym::dollar => "$".to_owned(), + Keysym::percent => "%".to_owned(), + Keysym::asciicircum => "^".to_owned(), + Keysym::ampersand => "&".to_owned(), + Keysym::asterisk => "*".to_owned(), + Keysym::parenleft => "(".to_owned(), + Keysym::parenright => ")".to_owned(), + Keysym::minus => "-".to_owned(), + Keysym::underscore => "_".to_owned(), + Keysym::equal => "=".to_owned(), + Keysym::plus => "+".to_owned(), + + Keysym::ISO_Left_Tab => { + handle_consumed_modifiers = false; + "tab".to_owned() + } + + _ => { + handle_consumed_modifiers = false; + xkb::keysym_get_name(key_sym).to_lowercase() + } + }; + + // Ignore control characters (and DEL) for the purposes of ime_key + let ime_key = + (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8); + + if handle_consumed_modifiers { + let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT); + let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index); + + if modifiers.shift && is_shift_consumed { + modifiers.shift = false; + } + } + + Keystroke { + modifiers, + key, + ime_key, + } + } +} + +impl Modifiers { + pub(super) fn from_xkb(keymap_state: &State) -> Self { + let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); + let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); + let control = + keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); + let platform = + keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); + Modifiers { + shift, + alt, + control, + platform, + function: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{px, Point}; + + #[test] + fn test_is_within_click_distance() { + let zero = Point::new(px(0.0), px(0.0)); + assert_eq!( + is_within_click_distance(zero, Point::new(px(5.0), px(5.0))), + true + ); + assert_eq!( + is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))), + true + ); + assert_eq!( + is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))), + true + ); + assert_eq!( + is_within_click_distance(zero, Point::new(px(5.0), px(5.1))), + false + ); + } +} diff --git a/crates/ming/src/platform/linux/wayland.rs b/crates/ming/src/platform/linux/wayland.rs new file mode 100644 index 0000000..61eb0bf --- /dev/null +++ b/crates/ming/src/platform/linux/wayland.rs @@ -0,0 +1,7 @@ +mod client; +mod cursor; +mod display; +mod serial; +mod window; + +pub(crate) use client::*; diff --git a/crates/ming/src/platform/linux/wayland/client.rs b/crates/ming/src/platform/linux/wayland/client.rs new file mode 100644 index 0000000..b5aed4e --- /dev/null +++ b/crates/ming/src/platform/linux/wayland/client.rs @@ -0,0 +1,1427 @@ +use core::hash; +use std::cell::{RefCell, RefMut}; +use std::os::fd::{AsRawFd, BorrowedFd}; +use std::path::PathBuf; +use std::rc::{Rc, Weak}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use async_task::Runnable; +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{EventLoop, LoopHandle}; +use calloop_wayland_source::WaylandSource; +use collections::HashMap; +use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary}; +use copypasta::ClipboardProvider; +use filedescriptor::Pipe; +use smallvec::SmallVec; +use util::ResultExt; +use wayland_backend::client::ObjectId; +use wayland_backend::protocol::WEnum; +use wayland_client::event_created_child; +use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents}; +use wayland_client::protocol::wl_callback::{self, WlCallback}; +use wayland_client::protocol::wl_data_device_manager::DndAction; +use wayland_client::protocol::wl_pointer::{AxisRelativeDirection, AxisSource}; +use wayland_client::protocol::{ + wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region, +}; +use wayland_client::{ + delegate_noop, + protocol::{ + wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, + wl_shm_pool, wl_surface, + }, + Connection, Dispatch, Proxy, QueueHandle, +}; +use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; +use wayland_protocols::wp::cursor_shape::v1::client::{ + wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1, +}; +use wayland_protocols::wp::fractional_scale::v1::client::{ + wp_fractional_scale_manager_v1, wp_fractional_scale_v1, +}; +use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter}; +use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1}; +use wayland_protocols::xdg::decoration::zv1::client::{ + zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, +}; +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; +use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; +use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; +use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS}; + +use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL}; +use super::window::{WaylandWindowState, WaylandWindowStatePtr}; +use crate::platform::linux::is_within_click_distance; +use crate::platform::linux::wayland::cursor::Cursor; +use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker}; +use crate::platform::linux::wayland::window::WaylandWindow; +use crate::platform::linux::LinuxClient; +use crate::platform::PlatformWindow; +use crate::{point, px, FileDropEvent, ForegroundExecutor, MouseExitEvent, SCROLL_LINES}; +use crate::{ + AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, + ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta, + ScrollWheelEvent, TouchPhase, +}; +use crate::{LinuxCommon, WindowParams}; + +/// Used to convert evdev scancode to xkb scancode +const MIN_KEYCODE: u32 = 8; + +#[derive(Clone)] +pub struct Globals { + pub qh: QueueHandle, + pub activation: Option, + pub compositor: wl_compositor::WlCompositor, + pub cursor_shape_manager: Option, + pub data_device_manager: Option, + pub wm_base: xdg_wm_base::XdgWmBase, + pub shm: wl_shm::WlShm, + pub viewporter: Option, + pub fractional_scale_manager: + Option, + pub decoration_manager: Option, + pub blur_manager: Option, + pub executor: ForegroundExecutor, +} + +impl Globals { + fn new( + globals: GlobalList, + executor: ForegroundExecutor, + qh: QueueHandle, + ) -> Self { + Globals { + activation: globals.bind(&qh, 1..=1, ()).ok(), + compositor: globals + .bind( + &qh, + wl_surface::REQ_SET_BUFFER_SCALE_SINCE + ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, + (), + ) + .unwrap(), + cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(), + data_device_manager: globals + .bind( + &qh, + WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, + (), + ) + .ok(), + shm: globals.bind(&qh, 1..=1, ()).unwrap(), + wm_base: globals.bind(&qh, 1..=1, ()).unwrap(), + viewporter: globals.bind(&qh, 1..=1, ()).ok(), + fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), + decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), + blur_manager: globals.bind(&qh, 1..=1, ()).ok(), + executor, + qh, + } + } +} + +pub(crate) struct WaylandClientState { + serial_tracker: SerialTracker, + globals: Globals, + wl_seat: wl_seat::WlSeat, // todo(linux): multi-seat support + wl_pointer: Option, + cursor_shape_device: Option, + data_device: Option, + // Surface to Window mapping + windows: HashMap, + // Output to scale mapping + output_scales: HashMap, + keymap_state: Option, + drag: DragState, + click: ClickState, + repeat: KeyRepeat, + modifiers: Modifiers, + axis_source: AxisSource, + mouse_location: Option>, + continuous_scroll_delta: Option>, + discrete_scroll_delta: Option>, + vertical_modifier: f32, + horizontal_modifier: f32, + scroll_event_received: bool, + enter_token: Option<()>, + button_pressed: Option, + mouse_focused_window: Option, + keyboard_focused_window: Option, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + cursor_style: Option, + cursor: Cursor, + clipboard: Option, + primary: Option, + event_loop: Option>, + common: LinuxCommon, + + pending_open_uri: Option, +} + +pub struct DragState { + data_offer: Option, + window: Option, + position: Point, +} + +pub struct ClickState { + last_click: Instant, + last_location: Point, + current_count: usize, +} + +pub(crate) struct KeyRepeat { + characters_per_second: u32, + delay: Duration, + current_id: u64, + current_keycode: Option, +} + +/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the +/// window to GPUI. +#[derive(Clone)] +pub struct WaylandClientStatePtr(Weak>); + +impl WaylandClientStatePtr { + fn get_client(&self) -> Rc> { + self.0 + .upgrade() + .expect("The pointer should always be valid when dispatching in wayland") + } + + pub fn drop_window(&self, surface_id: &ObjectId) { + let mut client = self.get_client(); + let mut state = client.borrow_mut(); + let closed_window = state.windows.remove(surface_id).unwrap(); + if let Some(window) = state.mouse_focused_window.take() { + if !window.ptr_eq(&closed_window) { + state.mouse_focused_window = Some(window); + } + } + if let Some(window) = state.keyboard_focused_window.take() { + if !window.ptr_eq(&closed_window) { + state.keyboard_focused_window = Some(window); + } + } + if state.windows.is_empty() { + state.common.signal.stop(); + } + } +} + +#[derive(Clone)] +pub struct WaylandClient(Rc>); + +impl Drop for WaylandClient { + fn drop(&mut self) { + let mut state = self.0.borrow_mut(); + state.windows.clear(); + + // Drop the clipboard to prevent a seg fault after we've closed all Wayland connections. + state.primary = None; + state.clipboard = None; + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.destroy(); + } + if let Some(data_device) = &state.data_device { + data_device.release(); + } + } +} + +const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3; +const WL_OUTPUT_VERSION: u32 = 2; + +fn wl_seat_version(version: u32) -> u32 { + // We rely on the wl_pointer.frame event + const WL_SEAT_MIN_VERSION: u32 = 5; + const WL_SEAT_MAX_VERSION: u32 = 9; + + if version < WL_SEAT_MIN_VERSION { + panic!( + "wl_seat below required version: {} < {}", + version, WL_SEAT_MIN_VERSION + ); + } + + version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION) +} + +impl WaylandClient { + pub(crate) fn new() -> Self { + let conn = Connection::connect_to_env().unwrap(); + + let (globals, mut event_queue) = + registry_queue_init::(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut seat: Option = None; + let mut outputs = HashMap::default(); + globals.contents().with_list(|list| { + for global in list { + match &global.interface[..] { + "wl_seat" => { + seat = Some(globals.registry().bind::( + global.name, + wl_seat_version(global.version), + &qh, + (), + )); + } + "wl_output" => { + let output = globals.registry().bind::( + global.name, + WL_OUTPUT_VERSION, + &qh, + (), + ); + outputs.insert(output.id(), 1); + } + _ => {} + } + } + }); + + let display = conn.backend().display_ptr() as *mut std::ffi::c_void; + + let event_loop = EventLoop::::try_new().unwrap(); + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + handle.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }); + + let seat = seat.unwrap(); + let globals = Globals::new(globals, common.foreground_executor.clone(), qh.clone()); + + let data_device = globals + .data_device_manager + .as_ref() + .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ())); + + let (primary, clipboard) = unsafe { create_clipboards_from_external(display) }; + + let cursor = Cursor::new(&conn, &globals, 24); + + let mut state = Rc::new(RefCell::new(WaylandClientState { + serial_tracker: SerialTracker::new(), + globals, + wl_seat: seat, + wl_pointer: None, + cursor_shape_device: None, + data_device, + output_scales: outputs, + windows: HashMap::default(), + common, + keymap_state: None, + drag: DragState { + data_offer: None, + window: None, + position: Point::default(), + }, + click: ClickState { + last_click: Instant::now(), + last_location: Point::default(), + current_count: 0, + }, + repeat: KeyRepeat { + characters_per_second: 16, + delay: Duration::from_millis(500), + current_id: 0, + current_keycode: None, + }, + modifiers: Modifiers { + shift: false, + control: false, + alt: false, + function: false, + platform: false, + }, + scroll_event_received: false, + axis_source: AxisSource::Wheel, + mouse_location: None, + continuous_scroll_delta: None, + discrete_scroll_delta: None, + vertical_modifier: -1.0, + horizontal_modifier: -1.0, + button_pressed: None, + mouse_focused_window: None, + keyboard_focused_window: None, + loop_handle: handle.clone(), + enter_token: None, + cursor_style: None, + cursor, + clipboard: Some(clipboard), + primary: Some(primary), + event_loop: Some(event_loop), + + pending_open_uri: None, + })); + + WaylandSource::new(conn, event_queue).insert(handle); + + Self(state) + } +} + +impl LinuxClient for WaylandClient { + fn displays(&self) -> Vec> { + Vec::new() + } + + fn display(&self, id: DisplayId) -> Option> { + unimplemented!() + } + + fn primary_display(&self) -> Option> { + None + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> Box { + let mut state = self.0.borrow_mut(); + + let (window, surface_id) = WaylandWindow::new( + state.globals.clone(), + WaylandClientStatePtr(Rc::downgrade(&self.0)), + params, + ); + state.windows.insert(surface_id, window.0.clone()); + + Box::new(window) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); + + let need_update = state + .cursor_style + .map_or(true, |current_style| current_style != style); + + if need_update { + let serial = state.serial_tracker.get(SerialKind::MouseEnter); + state.cursor_style = Some(style); + + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, style.to_shape()); + } else if state.mouse_focused_window.is_some() { + // cursor-shape-v1 isn't supported, set the cursor using a surface. + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + state + .cursor + .set_icon(&wl_pointer, serial, &style.to_icon_name()); + } + } + } + + fn open_uri(&self, uri: &str) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_open_uri = Some(uri.to_owned()); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial, &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + open_uri_internal(uri, None); + } + } + + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop + .run( + None, + &mut WaylandClientStatePtr(Rc::downgrade(&self.0)), + |_| {}, + ) + .log_err(); + } + + fn write_to_primary(&self, item: crate::ClipboardItem) { + self.0 + .borrow_mut() + .primary + .as_mut() + .unwrap() + .set_contents(item.text); + } + + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + self.0 + .borrow_mut() + .clipboard + .as_mut() + .unwrap() + .set_contents(item.text); + } + + fn read_from_primary(&self) -> Option { + self.0 + .borrow_mut() + .primary + .as_mut() + .unwrap() + .get_contents() + .ok() + .map(|s| crate::ClipboardItem { + text: s, + metadata: None, + }) + } + + fn read_from_clipboard(&self) -> Option { + self.0 + .borrow_mut() + .clipboard + .as_mut() + .unwrap() + .get_contents() + .ok() + .map(|s| crate::ClipboardItem { + text: s, + metadata: None, + }) + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + qh: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_registry::Event::Global { + name, + interface, + version, + } => match &interface[..] { + "wl_seat" => { + state.wl_pointer = None; + registry.bind::(name, wl_seat_version(version), qh, ()); + } + "wl_output" => { + let output = + registry.bind::(name, WL_OUTPUT_VERSION, qh, ()); + + state.output_scales.insert(output.id(), 1); + } + _ => {} + }, + wl_registry::Event::GlobalRemove { name: _ } => {} + _ => {} + } + } +} + +delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor); +delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1); +delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager); +delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm); +delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool); +delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer); +delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion); +delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); +delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter); +delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport); + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut WaylandClientStatePtr, + _: &wl_callback::WlCallback, + event: wl_callback::Event, + surface_id: &ObjectId, + _: &Connection, + qh: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + + match event { + wl_callback::Event::Done { callback_data } => { + window.frame(true); + } + _ => {} + } + } +} + +fn get_window( + mut state: &mut RefMut, + surface_id: &ObjectId, +) -> Option { + state.windows.get(surface_id).cloned() +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + surface: &wl_surface::WlSurface, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = get_window(&mut state, &surface.id()) else { + return; + }; + let scales = state.output_scales.clone(); + drop(state); + + window.handle_surface_event(event, scales); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + output: &wl_output::WlOutput, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(mut output_scale) = state.output_scales.get_mut(&output.id()) else { + return; + }; + + match event { + wl_output::Event::Scale { factor } => { + *output_scale = factor; + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut Self, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + window.handle_xdg_surface_event(event); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + xdg_toplevel: &xdg_toplevel::XdgToplevel, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + let should_close = window.handle_toplevel_event(event); + + if should_close { + this.drop_window(surface_id); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + _: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + token: &xdg_activation_token_v1::XdgActivationTokenV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + if let xdg_activation_token_v1::Event::Done { token } = event { + if let Some(uri) = state.pending_open_uri.take() { + open_uri_internal(&uri, Some(&token)); + } else { + log::error!("called while pending_open_uri is None"); + } + } + token.destroy(); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + data: &(), + conn: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + if capabilities.contains(wl_seat::Capability::Keyboard) { + seat.get_keyboard(qh, ()); + } + if capabilities.contains(wl_seat::Capability::Pointer) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let pointer = seat.get_pointer(qh, ()); + state.cursor_shape_device = state + .globals + .cursor_shape_manager + .as_ref() + .map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ())); + state.wl_pointer = Some(pointer); + } + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + keyboard: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + data: &(), + conn: &Connection, + qh: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + match event { + wl_keyboard::Event::RepeatInfo { rate, delay } => { + state.repeat.characters_per_second = rate as u32; + state.repeat.delay = Duration::from_millis(delay as u64); + } + wl_keyboard::Event::Keymap { + format: WEnum::Value(format), + fd, + size, + .. + } => { + assert_eq!( + format, + wl_keyboard::KeymapFormat::XkbV1, + "Unsupported keymap format" + ); + let keymap = unsafe { + xkb::Keymap::new_from_fd( + &xkb::Context::new(xkb::CONTEXT_NO_FLAGS), + fd, + size as usize, + XKB_KEYMAP_FORMAT_TEXT_V1, + KEYMAP_COMPILE_NO_FLAGS, + ) + .log_err() + .flatten() + .expect("Failed to create keymap") + }; + state.keymap_state = Some(xkb::State::new(&keymap)); + } + wl_keyboard::Event::Enter { surface, .. } => { + state.keyboard_focused_window = get_window(&mut state, &surface.id()); + state.enter_token = Some(()); + + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + window.set_focused(true); + } + } + wl_keyboard::Event::Leave { surface, .. } => { + let keyboard_focused_window = get_window(&mut state, &surface.id()); + state.keyboard_focused_window = None; + state.enter_token.take(); + + if let Some(window) = keyboard_focused_window { + drop(state); + window.set_focused(false); + } + } + wl_keyboard::Event::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + } => { + let focused_window = state.keyboard_focused_window.clone(); + let Some(focused_window) = focused_window else { + return; + }; + + let keymap_state = state.keymap_state.as_mut().unwrap(); + keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); + state.modifiers = Modifiers::from_xkb(keymap_state); + + let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: state.modifiers, + }); + + drop(state); + focused_window.handle_input(input); + } + wl_keyboard::Event::Key { + serial, + key, + state: WEnum::Value(key_state), + .. + } => { + state.serial_tracker.update(SerialKind::KeyPress, serial); + + let focused_window = state.keyboard_focused_window.clone(); + let Some(focused_window) = focused_window else { + return; + }; + let focused_window = focused_window.clone(); + + let keymap_state = state.keymap_state.as_ref().unwrap(); + let keycode = Keycode::from(key + MIN_KEYCODE); + let keysym = keymap_state.key_get_one_sym(keycode); + + match key_state { + wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => { + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), + is_held: false, // todo(linux) + }); + + state.repeat.current_id += 1; + state.repeat.current_keycode = Some(keycode); + + let rate = state.repeat.characters_per_second; + let id = state.repeat.current_id; + state + .loop_handle + .insert_source(Timer::from_duration(state.repeat.delay), { + let input = input.clone(); + move |event, _metadata, this| { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + let is_repeating = id == state.repeat.current_id + && state.repeat.current_keycode.is_some() + && state.keyboard_focused_window.is_some(); + + if !is_repeating { + return TimeoutAction::Drop; + } + + let focused_window = + state.keyboard_focused_window.as_ref().unwrap().clone(); + + drop(state); + focused_window.handle_input(input.clone()); + + TimeoutAction::ToDuration(Duration::from_secs(1) / rate) + } + }) + .unwrap(); + + drop(state); + focused_window.handle_input(input); + } + wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { + let input = PlatformInput::KeyUp(KeyUpEvent { + keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), + }); + + if state.repeat.current_keycode == Some(keycode) { + state.repeat.current_keycode = None; + } + + drop(state); + focused_window.handle_input(input); + } + _ => {} + } + } + _ => {} + } + } +} + +fn linux_button_to_gpui(button: u32) -> Option { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + Some(match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back), + BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward), + _ => return None, + }) +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + wl_pointer: &wl_pointer::WlPointer, + event: wl_pointer::Event, + data: &(), + conn: &Connection, + qh: &QueueHandle, + ) { + let mut client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_pointer::Event::Enter { + serial, + surface, + surface_x, + surface_y, + .. + } => { + state.serial_tracker.update(SerialKind::MouseEnter, serial); + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + + if let Some(window) = get_window(&mut state, &surface.id()) { + state.mouse_focused_window = Some(window.clone()); + if state.enter_token.is_some() { + state.enter_token = None; + } + if let Some(style) = state.cursor_style { + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, style.to_shape()); + } else { + state + .cursor + .set_icon(&wl_pointer, serial, &style.to_icon_name()); + } + } + drop(state); + window.set_focused(true); + } + } + wl_pointer::Event::Leave { surface, .. } => { + if let Some(focused_window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseExited(MouseExitEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + state.mouse_focused_window = None; + state.mouse_location = None; + + drop(state); + focused_window.handle_input(input); + focused_window.set_focused(false); + } + } + wl_pointer::Event::Motion { + time, + surface_x, + surface_y, + .. + } => { + if state.mouse_focused_window.is_none() { + return; + } + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + + if let Some(window) = state.mouse_focused_window.clone() { + if state + .keyboard_focused_window + .as_ref() + .map_or(false, |keyboard_window| window.ptr_eq(&keyboard_window)) + { + state.enter_token = None; + } + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + drop(state); + window.handle_input(input); + } + } + wl_pointer::Event::Button { + serial, + button, + state: WEnum::Value(button_state), + .. + } => { + state.serial_tracker.update(SerialKind::MousePress, serial); + let button = linux_button_to_gpui(button); + let Some(button) = button else { return }; + if state.mouse_focused_window.is_none() { + return; + } + match button_state { + wl_pointer::ButtonState::Pressed => { + let click_elapsed = state.click.last_click.elapsed(); + + if click_elapsed < DOUBLE_CLICK_INTERVAL + && is_within_click_distance( + state.click.last_location, + state.mouse_location.unwrap(), + ) + { + state.click.current_count += 1; + } else { + state.click.current_count = 1; + } + + state.click.last_click = Instant::now(); + state.click.last_location = state.mouse_location.unwrap(); + + state.button_pressed = Some(button); + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + first_mouse: state.enter_token.take().is_some(), + }); + drop(state); + window.handle_input(input); + } + } + wl_pointer::ButtonState::Released => { + state.button_pressed = None; + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + }); + drop(state); + window.handle_input(input); + } + } + _ => {} + } + } + + // Axis Events + wl_pointer::Event::AxisSource { + axis_source: WEnum::Value(axis_source), + } => { + state.axis_source = axis_source; + } + wl_pointer::Event::Axis { + time, + axis: WEnum::Value(axis), + value, + .. + } => { + if state.axis_source == AxisSource::Wheel { + return; + } + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; + let supports_relative_direction = + wl_pointer.version() >= wl_pointer::EVT_AXIS_RELATIVE_DIRECTION_SINCE; + state.scroll_event_received = true; + let scroll_delta = state + .continuous_scroll_delta + .get_or_insert(point(px(0.0), px(0.0))); + // TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings + let modifier = 3.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += px(value as f32 * modifier * axis_modifier); + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += px(value as f32 * modifier * axis_modifier); + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisDiscrete { + axis: WEnum::Value(axis), + discrete, + } => { + state.scroll_event_received = true; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; + + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES as f32; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES as f32; + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisRelativeDirection { + axis: WEnum::Value(axis), + direction: WEnum::Value(direction), + } => match (axis, direction) { + (wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Identical) => { + state.vertical_modifier = -1.0 + } + (wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Inverted) => { + state.vertical_modifier = 1.0 + } + (wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Identical) => { + state.horizontal_modifier = -1.0 + } + (wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Inverted) => { + state.horizontal_modifier = 1.0 + } + _ => unreachable!(), + }, + wl_pointer::Event::AxisValue120 { + axis: WEnum::Value(axis), + value120, + } => { + state.scroll_event_received = true; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => unreachable!(), + }; + + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + let wheel_percent = value120 as f32 / 120.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES as f32; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES as f32; + } + _ => unreachable!(), + } + } + wl_pointer::Event::Frame => { + if state.scroll_event_received { + state.scroll_event_received = false; + let continuous = state.continuous_scroll_delta.take(); + let discrete = state.discrete_scroll_delta.take(); + if let Some(continuous) = continuous { + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Pixels(continuous), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + } else if let Some(discrete) = discrete { + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Lines(discrete), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + } + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wp_fractional_scale_v1::WpFractionalScaleV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + window.handle_fractional_scale_event(event); + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + event: zxdg_toplevel_decoration_v1::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + window.handle_toplevel_decoration_event(event); + } +} + +const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wl_data_device::WlDataDevice, + event: wl_data_device::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_data_device::Event::Enter { + serial, + surface, + x, + y, + id: data_offer, + } => { + state.serial_tracker.update(SerialKind::DataDevice, serial); + if let Some(data_offer) = data_offer { + let Some(drag_window) = get_window(&mut state, &surface.id()) else { + return; + }; + + const ACTIONS: DndAction = DndAction::Copy; + data_offer.set_actions(ACTIONS, ACTIONS); + + let pipe = Pipe::new().unwrap(); + data_offer.receive(FILE_LIST_MIME_TYPE.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); + + let read_task = state + .common + .background_executor + .spawn(async { unsafe { read_fd(fd) } }); + + let this = this.clone(); + state + .common + .foreground_executor + .spawn(async move { + let file_list = match read_task.await { + Ok(list) => list, + Err(err) => { + log::error!("error reading drag and drop pipe: {err:?}"); + return; + } + }; + + let paths: SmallVec<[_; 2]> = file_list + .lines() + .map(|path| PathBuf::from(path.replace("file://", ""))) + .collect(); + let position = Point::new(x.into(), y.into()); + + // Prevent dropping text from other programs. + if paths.is_empty() { + data_offer.finish(); + data_offer.destroy(); + return; + } + + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position, + paths: crate::ExternalPaths(paths), + }); + + let client = this.get_client(); + let mut state = client.borrow_mut(); + state.drag.data_offer = Some(data_offer); + state.drag.window = Some(drag_window.clone()); + state.drag.position = position; + + drop(state); + drag_window.handle_input(input); + }) + .detach(); + } + } + wl_data_device::Event::Motion { x, y, .. } => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let position = Point::new(x.into(), y.into()); + state.drag.position = position; + + let input = PlatformInput::FileDrop(FileDropEvent::Pending { position }); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Leave => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.destroy(); + + state.drag.data_offer = None; + state.drag.window = None; + + let input = PlatformInput::FileDrop(FileDropEvent::Exited {}); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Drop => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.finish(); + data_offer.destroy(); + + state.drag.data_offer = None; + state.drag.window = None; + + let input = PlatformInput::FileDrop(FileDropEvent::Submit { + position: state.drag.position, + }); + drop(state); + drag_window.handle_input(input); + } + _ => {} + } + } + + event_created_child!(WaylandClientStatePtr, wl_data_device::WlDataDevice, [ + wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ()), + ]); +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_offer: &wl_data_offer::WlDataOffer, + event: wl_data_offer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_data_offer::Event::Offer { mime_type } => { + if mime_type == FILE_LIST_MIME_TYPE { + let serial = state.serial_tracker.get(SerialKind::DataDevice); + data_offer.accept(serial, Some(mime_type)); + } + } + _ => {} + } + } +} diff --git a/crates/ming/src/platform/linux/wayland/cursor.rs b/crates/ming/src/platform/linux/wayland/cursor.rs new file mode 100644 index 0000000..4ca4d47 --- /dev/null +++ b/crates/ming/src/platform/linux/wayland/cursor.rs @@ -0,0 +1,60 @@ +use crate::Globals; +use util::ResultExt; + +use wayland_client::protocol::wl_pointer::WlPointer; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::Connection; +use wayland_cursor::{CursorImageBuffer, CursorTheme}; + +pub(crate) struct Cursor { + theme: Option, + surface: WlSurface, +} + +impl Drop for Cursor { + fn drop(&mut self) { + self.theme.take(); + self.surface.destroy(); + } +} + +impl Cursor { + pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self { + Self { + theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(), + surface: globals.compositor.create_surface(&globals.qh, ()), + } + } + + pub fn set_icon(&mut self, wl_pointer: &WlPointer, serial_id: u32, mut cursor_icon_name: &str) { + if let Some(theme) = &mut self.theme { + let mut buffer: Option<&CursorImageBuffer>; + + if let Some(cursor) = theme.get_cursor(&cursor_icon_name) { + buffer = Some(&cursor[0]); + } else if let Some(cursor) = theme.get_cursor("default") { + buffer = Some(&cursor[0]); + cursor_icon_name = "default"; + log::warn!( + "Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon", + cursor_icon_name + ); + } else { + buffer = None; + log::warn!("Linux: Wayland: Unable to get default cursor too!"); + } + + if let Some(buffer) = &mut buffer { + let (width, height) = buffer.dimensions(); + let (hot_x, hot_y) = buffer.hotspot(); + + wl_pointer.set_cursor(serial_id, Some(&self.surface), hot_x as i32, hot_y as i32); + self.surface.attach(Some(&buffer), 0, 0); + self.surface.damage(0, 0, width as i32, height as i32); + self.surface.commit(); + } + } else { + log::warn!("Linux: Wayland: Unable to load cursor themes"); + } + } +} diff --git a/crates/ming/src/platform/linux/wayland/display.rs b/crates/ming/src/platform/linux/wayland/display.rs new file mode 100644 index 0000000..2e61770 --- /dev/null +++ b/crates/ming/src/platform/linux/wayland/display.rs @@ -0,0 +1,31 @@ +use std::fmt::Debug; + +use uuid::Uuid; + +use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Size}; + +#[derive(Debug)] +pub(crate) struct WaylandDisplay {} + +impl PlatformDisplay for WaylandDisplay { + // todo(linux) + fn id(&self) -> DisplayId { + DisplayId(123) // return some fake data so it doesn't panic + } + + // todo(linux) + fn uuid(&self) -> anyhow::Result { + Ok(Uuid::from_bytes([0; 16])) // return some fake data so it doesn't panic + } + + // todo(linux) + fn bounds(&self) -> Bounds { + Bounds { + origin: Default::default(), + size: Size { + width: DevicePixels(1000), + height: DevicePixels(500), + }, + } // return some fake data so it doesn't panic + } +} diff --git a/crates/ming/src/platform/linux/wayland/serial.rs b/crates/ming/src/platform/linux/wayland/serial.rs new file mode 100644 index 0000000..14d2dea --- /dev/null +++ b/crates/ming/src/platform/linux/wayland/serial.rs @@ -0,0 +1,91 @@ +use std::time::Instant; + +use collections::HashMap; + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) enum SerialKind { + DataDevice, + MouseEnter, + MousePress, + KeyPress, +} + +#[derive(Debug)] +struct SerialData { + serial: u32, + time: Instant, +} + +impl SerialData { + fn new(value: u32) -> Self { + Self { + serial: value, + time: Instant::now(), + } + } +} + +#[derive(Debug)] +/// Helper for tracking of different serial kinds. +pub(crate) struct SerialTracker { + serials: HashMap, +} + +impl SerialTracker { + pub fn new() -> Self { + Self { + serials: HashMap::default(), + } + } + + pub fn update(&mut self, kind: SerialKind, value: u32) { + self.serials.insert(kind, SerialData::new(value)); + } + + /// Returns the latest tracked serial of the provided [`SerialKind`] + /// + /// Will return 0 if not tracked. + pub fn get(&self, kind: SerialKind) -> u32 { + self.serials + .get(&kind) + .map(|serial_data| serial_data.serial) + .unwrap_or(0) + } + + /// Returns the newest serial of any of the provided [`SerialKind`] + pub fn get_newest_of(&self, kinds: &[SerialKind]) -> u32 { + kinds + .iter() + .filter_map(|kind| self.serials.get(&kind)) + .max_by_key(|serial_data| serial_data.time) + .map(|serial_data| serial_data.serial) + .unwrap_or(0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serial_tracker() { + let mut tracker = SerialTracker::new(); + + tracker.update(SerialKind::KeyPress, 100); + tracker.update(SerialKind::MousePress, 50); + tracker.update(SerialKind::MouseEnter, 300); + + assert_eq!( + tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]), + 50 + ); + assert_eq!(tracker.get(SerialKind::DataDevice), 0); + + tracker.update(SerialKind::KeyPress, 2000); + assert_eq!(tracker.get(SerialKind::KeyPress), 2000); + assert_eq!( + tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]), + 2000 + ); + } +} diff --git a/crates/ming/src/platform/linux/wayland/window.rs b/crates/ming/src/platform/linux/wayland/window.rs new file mode 100644 index 0000000..58e4641 --- /dev/null +++ b/crates/ming/src/platform/linux/wayland/window.rs @@ -0,0 +1,765 @@ +use std::any::Any; +use std::cell::{Ref, RefCell, RefMut}; +use std::ffi::c_void; +use std::num::NonZeroU32; +use std::ptr::NonNull; +use std::rc::{Rc, Weak}; +use std::sync::Arc; + +use blade_graphics as gpu; +use collections::{HashMap, HashSet}; +use futures::channel::oneshot::Receiver; +use raw_window_handle as rwh; +use wayland_backend::client::ObjectId; +use wayland_client::protocol::wl_region::WlRegion; +use wayland_client::WEnum; +use wayland_client::{protocol::wl_surface, Proxy}; +use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1; +use wayland_protocols::wp::viewporter::client::wp_viewport; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; +use wayland_protocols::xdg::shell::client::xdg_surface; +use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities}; +use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; + +use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig}; +use crate::platform::linux::wayland::display::WaylandDisplay; +use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow}; +use crate::scene::Scene; +use crate::{ + px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput, + Point, PromptLevel, Size, WaylandClientState, WaylandClientStatePtr, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowParams, +}; + +#[derive(Default)] +pub(crate) struct Callbacks { + request_frame: Option>, + input: Option crate::DispatchEventResult>>, + active_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, +} + +struct RawWindow { + window: *mut c_void, + display: *mut c_void, +} + +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let window = NonNull::new(self.window).unwrap(); + let handle = rwh::WaylandWindowHandle::new(window); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } +} +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = NonNull::new(self.display).unwrap(); + let handle = rwh::WaylandDisplayHandle::new(display); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } +} + +pub struct WaylandWindowState { + xdg_surface: xdg_surface::XdgSurface, + acknowledged_first_configure: bool, + pub surface: wl_surface::WlSurface, + decoration: Option, + blur: Option, + toplevel: xdg_toplevel::XdgToplevel, + viewport: Option, + outputs: HashSet, + globals: Globals, + renderer: BladeRenderer, + bounds: Bounds, + scale: f32, + input_handler: Option, + decoration_state: WaylandDecorationState, + fullscreen: bool, + restore_bounds: Bounds, + maximized: bool, + client: WaylandClientStatePtr, + callbacks: Callbacks, +} + +#[derive(Clone)] +pub struct WaylandWindowStatePtr { + state: Rc>, + callbacks: Rc>, +} + +impl WaylandWindowState { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + surface: wl_surface::WlSurface, + xdg_surface: xdg_surface::XdgSurface, + toplevel: xdg_toplevel::XdgToplevel, + decoration: Option, + viewport: Option, + client: WaylandClientStatePtr, + globals: Globals, + options: WindowParams, + ) -> Self { + let bounds = options.bounds.map(|p| p.0 as u32); + + let raw = RawWindow { + window: surface.id().as_ptr().cast::(), + display: surface + .backend() + .upgrade() + .unwrap() + .display_ptr() + .cast::(), + }; + let gpu = Arc::new( + unsafe { + gpu::Context::init_windowed( + &raw, + gpu::ContextDesc { + validation: false, + capture: false, + overlay: false, + }, + ) + } + .unwrap(), + ); + let config = BladeSurfaceConfig { + size: gpu::Extent { + width: bounds.size.width, + height: bounds.size.height, + depth: 1, + }, + transparent: options.window_background != WindowBackgroundAppearance::Opaque, + }; + + Self { + xdg_surface, + acknowledged_first_configure: false, + surface, + decoration, + blur: None, + toplevel, + viewport, + globals, + outputs: HashSet::default(), + renderer: BladeRenderer::new(gpu, config), + bounds, + scale: 1.0, + input_handler: None, + decoration_state: WaylandDecorationState::Client, + fullscreen: false, + restore_bounds: Bounds::default(), + maximized: false, + callbacks: Callbacks::default(), + client, + } + } +} + +pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); + +impl Drop for WaylandWindow { + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); + let surface_id = state.surface.id(); + let client = state.client.clone(); + + state.renderer.destroy(); + if let Some(decoration) = &state.decoration { + decoration.destroy(); + } + if let Some(blur) = &state.blur { + blur.release(); + } + state.toplevel.destroy(); + if let Some(viewport) = &state.viewport { + viewport.destroy(); + } + state.xdg_surface.destroy(); + state.surface.destroy(); + + let state_ptr = self.0.clone(); + state + .globals + .executor + .spawn(async move { + state_ptr.close(); + client.drop_window(&surface_id) + }) + .detach(); + drop(state); + } +} + +impl WaylandWindow { + fn borrow(&self) -> Ref { + self.0.state.borrow() + } + + fn borrow_mut(&self) -> RefMut { + self.0.state.borrow_mut() + } + + pub fn new( + globals: Globals, + client: WaylandClientStatePtr, + params: WindowParams, + ) -> (Self, ObjectId) { + let surface = globals.compositor.create_surface(&globals.qh, ()); + let xdg_surface = globals + .wm_base + .get_xdg_surface(&surface, &globals.qh, surface.id()); + let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); + + if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { + fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); + } + + // Attempt to set up window decorations based on the requested configuration + let decoration = globals + .decoration_manager + .as_ref() + .map(|decoration_manager| { + let decoration = decoration_manager.get_toplevel_decoration( + &toplevel, + &globals.qh, + surface.id(), + ); + decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide); + decoration + }); + + let viewport = globals + .viewporter + .as_ref() + .map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ())); + + let this = Self(WaylandWindowStatePtr { + state: Rc::new(RefCell::new(WaylandWindowState::new( + surface.clone(), + xdg_surface, + toplevel, + decoration, + viewport, + client, + globals, + params, + ))), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + }); + + // Kick things off + surface.commit(); + + (this, surface.id()) + } +} + +impl WaylandWindowStatePtr { + pub fn surface(&self) -> wl_surface::WlSurface { + self.state.borrow().surface.clone() + } + + pub fn ptr_eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } + + pub fn frame(&self, request_frame_callback: bool) { + if request_frame_callback { + let state = self.state.borrow_mut(); + state.surface.frame(&state.globals.qh, state.surface.id()); + drop(state); + } + let mut cb = self.callbacks.borrow_mut(); + if let Some(fun) = cb.request_frame.as_mut() { + fun(); + } + } + + pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { + match event { + xdg_surface::Event::Configure { serial } => { + let mut state = self.state.borrow_mut(); + state.xdg_surface.ack_configure(serial); + let request_frame_callback = !state.acknowledged_first_configure; + state.acknowledged_first_configure = true; + drop(state); + self.frame(request_frame_callback); + } + _ => {} + } + } + + pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { + match event { + zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { + self.set_decoration_state(WaylandDecorationState::Server) + } + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { + self.set_decoration_state(WaylandDecorationState::Client) + } + WEnum::Value(_) => { + log::warn!("Unknown decoration mode"); + } + WEnum::Unknown(v) => { + log::warn!("Unknown decoration mode: {}", v); + } + }, + _ => {} + } + } + + pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { + match event { + wp_fractional_scale_v1::Event::PreferredScale { scale } => { + self.rescale(scale as f32 / 120.0); + } + _ => {} + } + } + + pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool { + match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => { + let width = NonZeroU32::new(width as u32); + let height = NonZeroU32::new(height as u32); + let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8)); + let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8)); + let mut state = self.state.borrow_mut(); + state.maximized = maximized; + state.fullscreen = fullscreen; + if fullscreen || maximized { + state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32)); + } + drop(state); + self.resize(width, height); + self.set_fullscreen(fullscreen); + + false + } + xdg_toplevel::Event::Close => { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + if result { + drop(cb); + self.close(); + } + result + } else { + true + } + } + _ => false, + } + } + + pub fn handle_surface_event( + &self, + event: wl_surface::Event, + output_scales: HashMap, + ) { + let mut state = self.state.borrow_mut(); + + // We use `WpFractionalScale` instead to set the scale if it's available + if state.globals.fractional_scale_manager.is_some() { + return; + } + + match event { + wl_surface::Event::Enter { output } => { + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + return; + } + + state.outputs.insert(output.id()); + + let mut scale = 1; + for output in state.outputs.iter() { + if let Some(s) = output_scales.get(output) { + scale = scale.max(*s) + } + } + + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + wl_surface::Event::Leave { output } => { + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + return; + } + + state.outputs.remove(&output.id()); + + let mut scale = 1; + for output in state.outputs.iter() { + if let Some(s) = output_scales.get(output) { + scale = scale.max(*s) + } + } + + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + wl_surface::Event::PreferredBufferScale { factor } => { + state.surface.set_buffer_scale(factor); + drop(state); + self.rescale(factor as f32); + } + _ => {} + } + } + + pub fn set_size_and_scale( + &self, + width: Option, + height: Option, + scale: Option, + ) { + let (width, height, scale) = { + let mut state = self.state.borrow_mut(); + if width.map_or(true, |width| width.get() == state.bounds.size.width) + && height.map_or(true, |height| height.get() == state.bounds.size.height) + && scale.map_or(true, |scale| scale == state.scale) + { + return; + } + if let Some(width) = width { + state.bounds.size.width = width.get(); + } + if let Some(height) = height { + state.bounds.size.height = height.get(); + } + if let Some(scale) = scale { + state.scale = scale; + } + let width = state.bounds.size.width; + let height = state.bounds.size.height; + let scale = state.scale; + state.renderer.update_drawable_size(size( + width as f64 * scale as f64, + height as f64 * scale as f64, + )); + (width, height, scale) + }; + + if let Some(ref mut fun) = self.callbacks.borrow_mut().resize { + fun( + Size { + width: px(width as f32), + height: px(height as f32), + }, + scale, + ); + } + + { + let state = self.state.borrow(); + if let Some(viewport) = &state.viewport { + viewport.set_destination(width as i32, height as i32); + } + } + } + + pub fn resize(&self, width: Option, height: Option) { + self.set_size_and_scale(width, height, None); + } + + pub fn rescale(&self, scale: f32) { + self.set_size_and_scale(None, None, Some(scale)); + } + + pub fn set_fullscreen(&self, fullscreen: bool) { + let mut state = self.state.borrow_mut(); + state.fullscreen = fullscreen; + } + + /// Notifies the window of the state of the decorations. + /// + /// # Note + /// + /// This API is indirectly called by the wayland compositor and + /// not meant to be called by a user who wishes to change the state + /// of the decorations. This is because the state of the decorations + /// is managed by the compositor and not the client. + pub fn set_decoration_state(&self, state: WaylandDecorationState) { + self.state.borrow_mut().decoration_state = state; + } + + pub fn close(&self) { + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } + + pub fn handle_input(&self, input: PlatformInput) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().input { + if !fun(input.clone()).propagate { + return; + } + } + if let PlatformInput::KeyDown(event) = input { + if let Some(ime_key) = &event.keystroke.ime_key { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, ime_key); + self.state.borrow_mut().input_handler = Some(input_handler); + } + } + } + } + + pub fn set_focused(&self, focus: bool) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change { + fun(focus); + } + } +} + +impl rwh::HasWindowHandle for WaylandWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + unimplemented!() + } +} +impl rwh::HasDisplayHandle for WaylandWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + unimplemented!() + } +} + +impl PlatformWindow for WaylandWindow { + fn bounds(&self) -> Bounds { + self.borrow().bounds.map(|p| DevicePixels(p as i32)) + } + + fn is_maximized(&self) -> bool { + self.borrow().maximized + } + + // todo(linux) + // check if it is right + fn window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.restore_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.restore_bounds) + } else { + WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32))) + } + } + + fn content_size(&self) -> Size { + let state = self.borrow(); + Size { + width: Pixels(state.bounds.size.width as f32), + height: Pixels(state.bounds.size.height as f32), + } + } + + fn scale_factor(&self) -> f32 { + self.borrow().scale + } + + // todo(linux) + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } + + // todo(linux) + fn display(&self) -> Rc { + Rc::new(WaylandDisplay {}) + } + + // todo(linux) + fn mouse_position(&self) -> Point { + Point::default() + } + + // todo(linux) + fn modifiers(&self) -> Modifiers { + crate::Modifiers::default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[&str], + ) -> Option> { + None + } + + fn activate(&self) { + // todo(linux) + } + + // todo(linux) + fn is_active(&self) -> bool { + false + } + + fn set_title(&mut self, title: &str) { + self.borrow().toplevel.set_title(title.to_string()); + } + + fn set_app_id(&mut self, app_id: &str) { + self.borrow().toplevel.set_app_id(app_id.to_owned()); + } + + fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + let opaque = background_appearance == WindowBackgroundAppearance::Opaque; + let mut state = self.borrow_mut(); + state.renderer.update_transparency(!opaque); + + let region = state + .globals + .compositor + .create_region(&state.globals.qh, ()); + region.add(0, 0, i32::MAX, i32::MAX); + + if opaque { + // Promise the compositor that this region of the window surface + // contains no transparent pixels. This allows the compositor to + // do skip whatever is behind the surface for better performance. + state.surface.set_opaque_region(Some(®ion)); + } else { + state.surface.set_opaque_region(None); + } + + if let Some(ref blur_manager) = state.globals.blur_manager { + if (background_appearance == WindowBackgroundAppearance::Blurred) { + if (state.blur.is_none()) { + let blur = blur_manager.create(&state.surface, &state.globals.qh, ()); + blur.set_region(Some(®ion)); + state.blur = Some(blur); + } + state.blur.as_ref().unwrap().commit(); + } else { + // It probably doesn't hurt to clear the blur for opaque windows + blur_manager.unset(&state.surface); + if let Some(b) = state.blur.take() { + b.release() + } + } + } + + region.destroy(); + } + + fn set_edited(&mut self, edited: bool) { + // todo(linux) + } + + fn show_character_palette(&self) { + // todo(linux) + } + + fn minimize(&self) { + self.borrow().toplevel.set_minimized(); + } + + fn zoom(&self) { + let state = self.borrow(); + if !state.maximized { + state.toplevel.set_maximized(); + } else { + state.toplevel.unset_maximized(); + } + } + + fn toggle_fullscreen(&self) { + let mut state = self.borrow_mut(); + state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32)); + if !state.fullscreen { + state.toplevel.set_fullscreen(None); + } else { + state.toplevel.unset_fullscreen(); + } + } + + fn is_fullscreen(&self) -> bool { + self.borrow().fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box crate::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + // todo(linux) + } + + fn draw(&self, scene: &Scene) { + let mut state = self.borrow_mut(); + state.renderer.draw(scene); + } + + fn completed_frame(&self) { + let mut state = self.borrow_mut(); + state.surface.commit(); + } + + fn sprite_atlas(&self) -> Arc { + let state = self.borrow(); + state.renderer.sprite_atlas().clone() + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum WaylandDecorationState { + /// Decorations are to be provided by the client + Client, + + /// Decorations are provided by the server + Server, +} diff --git a/crates/ming/src/platform/linux/x11.rs b/crates/ming/src/platform/linux/x11.rs new file mode 100644 index 0000000..958da04 --- /dev/null +++ b/crates/ming/src/platform/linux/x11.rs @@ -0,0 +1,9 @@ +mod client; +mod display; +mod event; +mod window; + +pub(crate) use client::*; +pub(crate) use display::*; +pub(crate) use event::*; +pub(crate) use window::*; diff --git a/crates/ming/src/platform/linux/x11/client.rs b/crates/ming/src/platform/linux/x11/client.rs new file mode 100644 index 0000000..1c3a21c --- /dev/null +++ b/crates/ming/src/platform/linux/x11/client.rs @@ -0,0 +1,777 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::{Rc, Weak}; +use std::time::{Duration, Instant}; + +use calloop::generic::{FdWrapper, Generic}; +use calloop::{EventLoop, LoopHandle, RegistrationToken}; +use collections::HashMap; +use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext}; +use copypasta::ClipboardProvider; + +use util::ResultExt; +use x11rb::connection::{Connection, RequestConnection}; +use x11rb::cursor; +use x11rb::errors::ConnectionError; +use x11rb::protocol::randr::ConnectionExt as _; +use x11rb::protocol::xinput::{ConnectionExt, ScrollClass}; +use x11rb::protocol::xkb::ConnectionExt as _; +use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _}; +use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event}; +use x11rb::resource_manager::Database; +use x11rb::xcb_ffi::XCBConnection; +use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; +use xkbcommon::xkb as xkbc; + +use crate::platform::linux::LinuxClient; +use crate::platform::{LinuxCommon, PlatformWindow}; +use crate::{ + modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, + Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta, + Size, TouchPhase, WindowParams, X11Window, +}; + +use super::{ + super::{open_uri_internal, SCROLL_LINES}, + X11Display, X11WindowStatePtr, XcbAtoms, +}; +use super::{button_from_mask, button_of_key, modifiers_from_state}; +use crate::platform::linux::is_within_click_distance; +use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; + +pub(crate) struct WindowRef { + window: X11WindowStatePtr, + refresh_event_token: RegistrationToken, +} + +impl Deref for WindowRef { + type Target = X11WindowStatePtr; + + fn deref(&self) -> &Self::Target { + &self.window + } +} + +pub struct X11ClientState { + pub(crate) loop_handle: LoopHandle<'static, X11Client>, + pub(crate) event_loop: Option>, + + pub(crate) last_click: Instant, + pub(crate) last_location: Point, + pub(crate) current_count: usize, + + pub(crate) scale_factor: f32, + + pub(crate) xcb_connection: Rc, + pub(crate) x_root_index: usize, + pub(crate) resource_database: Database, + pub(crate) atoms: XcbAtoms, + pub(crate) windows: HashMap, + pub(crate) focused_window: Option, + pub(crate) xkb: xkbc::State, + + pub(crate) cursor_handle: cursor::Handle, + pub(crate) cursor_styles: HashMap, + pub(crate) cursor_cache: HashMap, + + pub(crate) scroll_class_data: Vec, + pub(crate) scroll_x: Option, + pub(crate) scroll_y: Option, + + pub(crate) common: LinuxCommon, + pub(crate) clipboard: X11ClipboardContext, + pub(crate) primary: X11ClipboardContext, +} + +#[derive(Clone)] +pub struct X11ClientStatePtr(pub Weak>); + +impl X11ClientStatePtr { + pub fn drop_window(&self, x_window: u32) { + let client = X11Client(self.0.upgrade().expect("client already dropped")); + let mut state = client.0.borrow_mut(); + + if let Some(window_ref) = state.windows.remove(&x_window) { + state.loop_handle.remove(window_ref.refresh_event_token); + } + + state.cursor_styles.remove(&x_window); + + if state.windows.is_empty() { + state.common.signal.stop(); + } + } +} + +#[derive(Clone)] +pub(crate) struct X11Client(Rc>); + +impl X11Client { + pub(crate) fn new() -> Self { + let event_loop = EventLoop::try_new().unwrap(); + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle.insert_source(main_receiver, |event, _, _: &mut X11Client| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }); + + let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap(); + xcb_connection + .prefetch_extension_information(xkb::X11_EXTENSION_NAME) + .unwrap(); + xcb_connection + .prefetch_extension_information(randr::X11_EXTENSION_NAME) + .unwrap(); + xcb_connection + .prefetch_extension_information(render::X11_EXTENSION_NAME) + .unwrap(); + xcb_connection + .prefetch_extension_information(xinput::X11_EXTENSION_NAME) + .unwrap(); + + let xinput_version = xcb_connection + .xinput_xi_query_version(2, 0) + .unwrap() + .reply() + .unwrap(); + assert!( + xinput_version.major_version >= 2, + "XInput Extension v2 not supported." + ); + + let master_device_query = xcb_connection + .xinput_xi_query_device(1_u16) + .unwrap() + .reply() + .unwrap(); + let scroll_class_data = master_device_query + .infos + .iter() + .find(|info| info.type_ == xinput::DeviceType::MASTER_POINTER) + .unwrap() + .classes + .iter() + .filter_map(|class| class.data.as_scroll()) + .map(|class| *class) + .collect::>(); + + let atoms = XcbAtoms::new(&xcb_connection).unwrap(); + let xkb = xcb_connection + .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION) + .unwrap(); + + let atoms = atoms.reply().unwrap(); + let xkb = xkb.reply().unwrap(); + let events = xkb::EventType::STATE_NOTIFY; + xcb_connection + .xkb_select_events( + xkb::ID::USE_CORE_KBD.into(), + 0u8.into(), + events, + 0u8.into(), + 0u8.into(), + &xkb::SelectEventsAux::new(), + ) + .unwrap(); + assert!(xkb.supported); + + let xkb_state = { + let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); + let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection); + let xkb_keymap = xkbc::x11::keymap_new_from_device( + &xkb_context, + &xcb_connection, + xkb_device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) + }; + + let screen = xcb_connection.setup().roots.get(x_root_index).unwrap(); + + // Values from `Database::GET_RESOURCE_DATABASE` + let resource_manager = xcb_connection + .get_property( + false, + screen.root, + xproto::AtomEnum::RESOURCE_MANAGER, + xproto::AtomEnum::STRING, + 0, + 100_000_000, + ) + .unwrap(); + let resource_manager = resource_manager.reply().unwrap(); + + // todo(linux): read hostname + let resource_database = Database::new_from_default(&resource_manager, "HOSTNAME".into()); + + let scale_factor = resource_database + .get_value("Xft.dpi", "Xft.dpi") + .ok() + .flatten() + .map(|dpi: f32| dpi / 96.0) + .unwrap_or(1.0); + + let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) + .unwrap() + .reply() + .unwrap(); + + let clipboard = X11ClipboardContext::::new().unwrap(); + let primary = X11ClipboardContext::::new().unwrap(); + + let xcb_connection = Rc::new(xcb_connection); + + // Safety: Safe if xcb::Connection always returns a valid fd + let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; + + handle + .insert_source( + Generic::new_with_error::( + fd, + calloop::Interest::READ, + calloop::Mode::Level, + ), + { + let xcb_connection = xcb_connection.clone(); + move |_readiness, _, client| { + while let Some(event) = xcb_connection.poll_for_event()? { + client.handle_event(event); + } + Ok(calloop::PostAction::Continue) + } + }, + ) + .expect("Failed to initialize x11 event source"); + + X11Client(Rc::new(RefCell::new(X11ClientState { + event_loop: Some(event_loop), + loop_handle: handle, + common, + last_click: Instant::now(), + last_location: Point::new(px(0.0), px(0.0)), + current_count: 0, + scale_factor, + + xcb_connection, + x_root_index, + resource_database, + atoms, + windows: HashMap::default(), + focused_window: None, + xkb: xkb_state, + + cursor_handle, + cursor_styles: HashMap::default(), + cursor_cache: HashMap::default(), + + scroll_class_data, + scroll_x: None, + scroll_y: None, + + clipboard, + primary, + }))) + } + + fn get_window(&self, win: xproto::Window) -> Option { + let state = self.0.borrow(); + state + .windows + .get(&win) + .map(|window_reference| window_reference.window.clone()) + } + + fn handle_event(&self, event: Event) -> Option<()> { + match event { + Event::ClientMessage(event) => { + let window = self.get_window(event.window)?; + let [atom, ..] = event.data.as_data32(); + let mut state = self.0.borrow_mut(); + + if atom == state.atoms.WM_DELETE_WINDOW { + // window "x" button clicked by user + if window.should_close() { + let window_ref = state.windows.remove(&event.window)?; + state.loop_handle.remove(window_ref.refresh_event_token); + // Rest of the close logic is handled in drop_window() + } + } + } + Event::ConfigureNotify(event) => { + let bounds = Bounds { + origin: Point { + x: event.x.into(), + y: event.y.into(), + }, + size: Size { + width: event.width.into(), + height: event.height.into(), + }, + }; + let window = self.get_window(event.window)?; + window.configure(bounds); + } + Event::Expose(event) => { + let window = self.get_window(event.window)?; + window.refresh(); + } + Event::FocusIn(event) => { + let window = self.get_window(event.event)?; + window.set_focused(true); + self.0.borrow_mut().focused_window = Some(event.event); + } + Event::FocusOut(event) => { + let window = self.get_window(event.event)?; + window.set_focused(false); + self.0.borrow_mut().focused_window = None; + } + Event::XkbStateNotify(event) => { + let mut state = self.0.borrow_mut(); + state.xkb.update_mask( + event.base_mods.into(), + event.latched_mods.into(), + event.locked_mods.into(), + 0, + 0, + event.locked_group.into(), + ); + let modifiers = Modifiers::from_xkb(&state.xkb); + let focused_window_id = state.focused_window?; + drop(state); + + let focused_window = self.get_window(focused_window_id)?; + focused_window.handle_input(PlatformInput::ModifiersChanged( + ModifiersChangedEvent { modifiers }, + )); + } + Event::KeyPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + let keystroke = { + let code = event.detail.into(); + let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + state.xkb.update_key(code, xkbc::KeyDirection::Down); + let keysym = state.xkb.key_get_one_sym(code); + if keysym.is_modifier_key() { + return Some(()); + } + keystroke + }; + + drop(state); + window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { + keystroke, + is_held: false, + })); + } + Event::KeyRelease(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + let keystroke = { + let code = event.detail.into(); + let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + state.xkb.update_key(code, xkbc::KeyDirection::Up); + let keysym = state.xkb.key_get_one_sym(code); + if keysym.is_modifier_key() { + return Some(()); + } + keystroke + }; + drop(state); + window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke })); + } + Event::XinputButtonPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_xinput_info(event.mods); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + if let Some(button) = button_of_key(event.detail.try_into().unwrap()) { + let click_elapsed = state.last_click.elapsed(); + + if click_elapsed < DOUBLE_CLICK_INTERVAL + && is_within_click_distance(state.last_location, position) + { + state.current_count += 1; + } else { + state.current_count = 1; + } + + state.last_click = Instant::now(); + state.last_location = position; + let current_count = state.current_count; + + drop(state); + window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent { + button, + position, + modifiers, + click_count: current_count, + first_mouse: false, + })); + } else { + log::warn!("Unknown button press: {event:?}"); + } + } + Event::XinputButtonRelease(event) => { + let window = self.get_window(event.event)?; + let state = self.0.borrow(); + let modifiers = modifiers_from_xinput_info(event.mods); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + if let Some(button) = button_of_key(event.detail.try_into().unwrap()) { + let click_count = state.current_count; + drop(state); + window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent { + button, + position, + modifiers, + click_count, + })); + } + } + Event::XinputMotion(event) => { + let window = self.get_window(event.event)?; + let state = self.0.borrow(); + let pressed_button = button_from_mask(event.button_mask[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + drop(state); + let modifiers = modifiers_from_xinput_info(event.mods); + + let axisvalues = event + .axisvalues + .iter() + .map(|axisvalue| fp3232_to_f32(*axisvalue)) + .collect::>(); + + if event.valuator_mask[0] & 3 != 0 { + window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent { + position, + pressed_button, + modifiers, + })); + } + + let mut valuator_idx = 0; + let scroll_class_data = self.0.borrow().scroll_class_data.clone(); + for shift in 0..32 { + if (event.valuator_mask[0] >> shift) & 1 == 0 { + continue; + } + + for scroll_class in &scroll_class_data { + if scroll_class.scroll_type == xinput::ScrollType::HORIZONTAL + && scroll_class.number == shift + { + let new_scroll = axisvalues[valuator_idx] + / fp3232_to_f32(scroll_class.increment) + * SCROLL_LINES as f32; + let old_scroll = self.0.borrow().scroll_x; + self.0.borrow_mut().scroll_x = Some(new_scroll); + + if let Some(old_scroll) = old_scroll { + let delta_scroll = old_scroll - new_scroll; + window.handle_input(PlatformInput::ScrollWheel( + crate::ScrollWheelEvent { + position, + delta: ScrollDelta::Lines(Point::new(delta_scroll, 0.0)), + modifiers, + touch_phase: TouchPhase::default(), + }, + )); + } + } else if scroll_class.scroll_type == xinput::ScrollType::VERTICAL + && scroll_class.number == shift + { + // the `increment` is the valuator delta equivalent to one positive unit of scrolling. Here that means SCROLL_LINES lines. + let new_scroll = axisvalues[valuator_idx] + / fp3232_to_f32(scroll_class.increment) + * SCROLL_LINES as f32; + let old_scroll = self.0.borrow().scroll_y; + self.0.borrow_mut().scroll_y = Some(new_scroll); + + if let Some(old_scroll) = old_scroll { + let delta_scroll = old_scroll - new_scroll; + window.handle_input(PlatformInput::ScrollWheel( + crate::ScrollWheelEvent { + position, + delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)), + modifiers, + touch_phase: TouchPhase::default(), + }, + )); + } + } + } + + valuator_idx += 1; + } + } + Event::XinputLeave(event) => { + self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) + self.0.borrow_mut().scroll_y = None; + + let window = self.get_window(event.event)?; + let state = self.0.borrow(); + let pressed_button = button_from_mask(event.buttons[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + let modifiers = modifiers_from_xinput_info(event.mods); + drop(state); + + window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent { + pressed_button, + position, + modifiers, + })); + } + _ => {} + }; + + Some(()) + } +} + +impl LinuxClient for X11Client { + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn displays(&self) -> Vec> { + let state = self.0.borrow(); + let setup = state.xcb_connection.setup(); + setup + .roots + .iter() + .enumerate() + .filter_map(|(root_id, _)| { + Some(Rc::new(X11Display::new(&state.xcb_connection, root_id)?) + as Rc) + }) + .collect() + } + + fn primary_display(&self) -> Option> { + let state = self.0.borrow(); + + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.x_root_index) + .expect("There should always be a root index"), + )) + } + + fn display(&self, id: DisplayId) -> Option> { + let state = self.0.borrow(); + + Some(Rc::new(X11Display::new( + &state.xcb_connection, + id.0 as usize, + )?)) + } + + fn open_window( + &self, + _handle: AnyWindowHandle, + params: WindowParams, + ) -> Box { + let mut state = self.0.borrow_mut(); + let x_window = state.xcb_connection.generate_id().unwrap(); + + let window = X11Window::new( + X11ClientStatePtr(Rc::downgrade(&self.0)), + state.common.foreground_executor.clone(), + params, + &state.xcb_connection, + state.x_root_index, + x_window, + &state.atoms, + state.scale_factor, + ); + + let screen_resources = state + .xcb_connection + .randr_get_screen_resources(x_window) + .unwrap() + .reply() + .expect("Could not find available screens"); + + let mode = screen_resources + .crtcs + .iter() + .find_map(|crtc| { + let crtc_info = state + .xcb_connection + .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) + .ok()? + .reply() + .ok()?; + + screen_resources + .modes + .iter() + .find(|m| m.id == crtc_info.mode) + }) + .expect("Unable to find screen refresh rate"); + + let refresh_event_token = state + .loop_handle + .insert_source(calloop::timer::Timer::immediate(), { + let refresh_duration = mode_refresh_rate(mode); + move |mut instant, (), client| { + let state = client.0.borrow_mut(); + state + .xcb_connection + .send_event( + false, + x_window, + xproto::EventMask::EXPOSURE, + xproto::ExposeEvent { + response_type: xproto::EXPOSE_EVENT, + sequence: 0, + window: x_window, + x: 0, + y: 0, + width: 0, + height: 0, + count: 1, + }, + ) + .unwrap(); + let _ = state.xcb_connection.flush().unwrap(); + // Take into account that some frames have been skipped + let now = time::Instant::now(); + while instant < now { + instant += refresh_duration; + } + calloop::timer::TimeoutAction::ToInstant(instant) + } + }) + .expect("Failed to initialize refresh timer"); + + let window_ref = WindowRef { + window: window.0.clone(), + refresh_event_token, + }; + + state.windows.insert(x_window, window_ref); + Box::new(window) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); + let Some(focused_window) = state.focused_window else { + return; + }; + let current_style = state + .cursor_styles + .get(&focused_window) + .unwrap_or(&CursorStyle::Arrow); + if *current_style == style { + return; + } + + let cursor = match state.cursor_cache.get(&style) { + Some(cursor) => *cursor, + None => { + let cursor = state + .cursor_handle + .load_cursor(&state.xcb_connection, &style.to_icon_name()) + .expect("failed to load cursor"); + state.cursor_cache.insert(style, cursor); + cursor + } + }; + + state.cursor_styles.insert(focused_window, style); + state + .xcb_connection + .change_window_attributes( + focused_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ) + .expect("failed to change window cursor"); + } + + fn open_uri(&self, uri: &str) { + open_uri_internal(uri, None); + } + + fn write_to_primary(&self, item: crate::ClipboardItem) { + self.0.borrow_mut().primary.set_contents(item.text); + } + + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + self.0.borrow_mut().clipboard.set_contents(item.text); + } + + fn read_from_primary(&self) -> Option { + self.0 + .borrow_mut() + .primary + .get_contents() + .ok() + .map(|text| crate::ClipboardItem { + text, + metadata: None, + }) + } + + fn read_from_clipboard(&self) -> Option { + self.0 + .borrow_mut() + .clipboard + .get_contents() + .ok() + .map(|text| crate::ClipboardItem { + text, + metadata: None, + }) + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } +} + +// Adatpted from: +// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 +pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { + let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); + let micros = 1_000_000_000 / millihertz; + log::info!("Refreshing at {} micros", micros); + Duration::from_micros(micros) +} + +fn fp3232_to_f32(value: xinput::Fp3232) -> f32 { + value.integral as f32 + value.frac as f32 / u32::MAX as f32 +} diff --git a/crates/ming/src/platform/linux/x11/display.rs b/crates/ming/src/platform/linux/x11/display.rs new file mode 100644 index 0000000..f68b219 --- /dev/null +++ b/crates/ming/src/platform/linux/x11/display.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use uuid::Uuid; +use x11rb::{connection::Connection as _, xcb_ffi::XCBConnection}; + +use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Size}; + +#[derive(Debug)] +pub(crate) struct X11Display { + x_screen_index: usize, + bounds: Bounds, + uuid: Uuid, +} + +impl X11Display { + pub(crate) fn new(xc: &XCBConnection, x_screen_index: usize) -> Option { + let screen = xc.setup().roots.get(x_screen_index).unwrap(); + Some(Self { + x_screen_index: x_screen_index, + bounds: Bounds { + origin: Default::default(), + size: Size { + width: DevicePixels(screen.width_in_pixels as i32), + height: DevicePixels(screen.height_in_pixels as i32), + }, + }, + uuid: Uuid::from_bytes([0; 16]), + }) + } +} + +impl PlatformDisplay for X11Display { + fn id(&self) -> DisplayId { + DisplayId(self.x_screen_index as u32) + } + + fn uuid(&self) -> Result { + Ok(self.uuid) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} diff --git a/crates/ming/src/platform/linux/x11/event.rs b/crates/ming/src/platform/linux/x11/event.rs new file mode 100644 index 0000000..fb16a85 --- /dev/null +++ b/crates/ming/src/platform/linux/x11/event.rs @@ -0,0 +1,50 @@ +use x11rb::protocol::{ + xinput, + xproto::{self, ModMask}, +}; + +use crate::{Modifiers, MouseButton, NavigationDirection}; + +pub(crate) fn button_of_key(detail: xproto::Button) -> Option { + Some(match detail { + 1 => MouseButton::Left, + 2 => MouseButton::Middle, + 3 => MouseButton::Right, + 8 => MouseButton::Navigate(NavigationDirection::Back), + 9 => MouseButton::Navigate(NavigationDirection::Forward), + _ => return None, + }) +} + +pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers { + Modifiers { + control: state.contains(xproto::KeyButMask::CONTROL), + alt: state.contains(xproto::KeyButMask::MOD1), + shift: state.contains(xproto::KeyButMask::SHIFT), + platform: state.contains(xproto::KeyButMask::MOD4), + function: false, + } +} + +pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers { + Modifiers { + control: modifier_info.effective as u16 & ModMask::CONTROL.bits() + == ModMask::CONTROL.bits(), + alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(), + shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(), + platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(), + function: false, + } +} + +pub(crate) fn button_from_mask(button_mask: u32) -> Option { + Some(if button_mask & 2 == 2 { + MouseButton::Left + } else if button_mask & 4 == 4 { + MouseButton::Middle + } else if button_mask & 8 == 8 { + MouseButton::Right + } else { + return None; + }) +} diff --git a/crates/ming/src/platform/linux/x11/window.rs b/crates/ming/src/platform/linux/x11/window.rs new file mode 100644 index 0000000..77c050a --- /dev/null +++ b/crates/ming/src/platform/linux/x11/window.rs @@ -0,0 +1,722 @@ +// todo(linux): remove +#![allow(unused)] + +use crate::{ + platform::blade::{BladeRenderer, BladeSurfaceConfig}, + size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, Platform, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel, + Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, + WindowParams, X11Client, X11ClientState, X11ClientStatePtr, +}; +use blade_graphics as gpu; +use parking_lot::Mutex; +use raw_window_handle as rwh; +use util::ResultExt; +use x11rb::{ + connection::{Connection as _, RequestConnection as _}, + protocol::{ + render::{self, ConnectionExt as _}, + xinput::{self, ConnectionExt as _}, + xproto::{self, ConnectionExt as _, CreateWindowAux}, + }, + resource_manager::Database, + wrapper::ConnectionExt as _, + xcb_ffi::XCBConnection, +}; + +use std::{ + cell::{Ref, RefCell, RefMut}, + collections::HashMap, + ffi::c_void, + iter::Zip, + mem, + num::NonZeroU32, + ops::Div, + ptr::NonNull, + rc::Rc, + sync::{self, Arc}, +}; + +use super::X11Display; + +x11rb::atom_manager! { + pub XcbAtoms: AtomsCookie { + UTF8_STRING, + WM_PROTOCOLS, + WM_DELETE_WINDOW, + _NET_WM_NAME, + _NET_WM_STATE, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_STATE_MAXIMIZED_HORZ, + } +} + +fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window) -> gpu::Extent { + let reply = xcb_connection + .get_geometry(x_window) + .unwrap() + .reply() + .unwrap(); + gpu::Extent { + width: reply.width as u32, + height: reply.height as u32, + depth: 1, + } +} + +#[derive(Debug)] +struct Visual { + id: xproto::Visualid, + colormap: u32, + depth: u8, +} + +struct VisualSet { + inherit: Visual, + opaque: Option, + transparent: Option, + root: u32, + black_pixel: u32, +} + +fn find_visuals(xcb_connection: &XCBConnection, screen_index: usize) -> VisualSet { + let screen = &xcb_connection.setup().roots[screen_index]; + let mut set = VisualSet { + inherit: Visual { + id: screen.root_visual, + colormap: screen.default_colormap, + depth: screen.root_depth, + }, + opaque: None, + transparent: None, + root: screen.root, + black_pixel: screen.black_pixel, + }; + + for depth_info in screen.allowed_depths.iter() { + for visual_type in depth_info.visuals.iter() { + let visual = Visual { + id: visual_type.visual_id, + colormap: 0, + depth: depth_info.depth, + }; + log::debug!("Visual id: {}, class: {:?}, depth: {}, bits_per_value: {}, masks: 0x{:x} 0x{:x} 0x{:x}", + visual_type.visual_id, + visual_type.class, + depth_info.depth, + visual_type.bits_per_rgb_value, + visual_type.red_mask, visual_type.green_mask, visual_type.blue_mask, + ); + + if ( + visual_type.red_mask, + visual_type.green_mask, + visual_type.blue_mask, + ) != (0xFF0000, 0xFF00, 0xFF) + { + continue; + } + let color_mask = visual_type.red_mask | visual_type.green_mask | visual_type.blue_mask; + let alpha_mask = color_mask as usize ^ ((1usize << depth_info.depth) - 1); + + if alpha_mask == 0 { + if set.opaque.is_none() { + set.opaque = Some(visual); + } + } else { + if set.transparent.is_none() { + set.transparent = Some(visual); + } + } + } + } + + set +} + +struct RawWindow { + connection: *mut c_void, + screen_id: usize, + window_id: u32, + visual_id: u32, +} + +#[derive(Default)] +pub struct Callbacks { + request_frame: Option>, + input: Option crate::DispatchEventResult>>, + active_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, +} + +pub(crate) struct X11WindowState { + client: X11ClientStatePtr, + executor: ForegroundExecutor, + atoms: XcbAtoms, + raw: RawWindow, + bounds: Bounds, + scale_factor: f32, + renderer: BladeRenderer, + display: Rc, + input_handler: Option, +} + +#[derive(Clone)] +pub(crate) struct X11WindowStatePtr { + pub(crate) state: Rc>, + pub(crate) callbacks: Rc>, + xcb_connection: Rc, + x_window: xproto::Window, +} + +// todo(linux): Remove other RawWindowHandle implementation +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result { + let non_zero = NonZeroU32::new(self.window_id).unwrap(); + let mut handle = rwh::XcbWindowHandle::new(non_zero); + handle.visual_id = NonZeroU32::new(self.visual_id); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } +} +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result { + let non_zero = NonNull::new(self.connection).unwrap(); + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } +} + +impl rwh::HasWindowHandle for X11Window { + fn window_handle(&self) -> Result { + unimplemented!() + } +} +impl rwh::HasDisplayHandle for X11Window { + fn display_handle(&self) -> Result { + unimplemented!() + } +} + +impl X11WindowState { + #[allow(clippy::too_many_arguments)] + pub fn new( + client: X11ClientStatePtr, + executor: ForegroundExecutor, + params: WindowParams, + xcb_connection: &Rc, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + ) -> Self { + let x_screen_index = params + .display_id + .map_or(x_main_screen_index, |did| did.0 as usize); + + let visual_set = find_visuals(&xcb_connection, x_screen_index); + let visual_maybe = match params.window_background { + WindowBackgroundAppearance::Opaque => visual_set.opaque, + WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => { + visual_set.transparent + } + }; + let visual = match visual_maybe { + Some(visual) => visual, + None => { + log::warn!( + "Unable to find a matching visual for {:?}", + params.window_background + ); + visual_set.inherit + } + }; + log::info!("Using {:?}", visual); + + let colormap = if visual.colormap != 0 { + visual.colormap + } else { + let id = xcb_connection.generate_id().unwrap(); + log::info!("Creating colormap {}", id); + xcb_connection + .create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id) + .unwrap() + .check() + .unwrap(); + id + }; + + let win_aux = xproto::CreateWindowAux::new() + .background_pixel(x11rb::NONE) + // https://stackoverflow.com/questions/43218127/x11-xlib-xcb-creating-a-window-requires-border-pixel-if-specifying-colormap-wh + .border_pixel(visual_set.black_pixel) + .colormap(colormap) + .event_mask( + xproto::EventMask::EXPOSURE + | xproto::EventMask::STRUCTURE_NOTIFY + | xproto::EventMask::ENTER_WINDOW + | xproto::EventMask::LEAVE_WINDOW + | xproto::EventMask::FOCUS_CHANGE + | xproto::EventMask::KEY_PRESS + | xproto::EventMask::KEY_RELEASE, + ); + + xcb_connection + .create_window( + visual.depth, + x_window, + visual_set.root, + params.bounds.origin.x.0 as i16, + params.bounds.origin.y.0 as i16, + params.bounds.size.width.0 as u16, + params.bounds.size.height.0 as u16, + 0, + xproto::WindowClass::INPUT_OUTPUT, + visual.id, + &win_aux, + ) + .unwrap() + .check() + .unwrap(); + + if let Some(titlebar) = params.titlebar { + if let Some(title) = titlebar.title { + xcb_connection + .change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ) + .unwrap(); + } + } + + xcb_connection + .change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms.WM_PROTOCOLS, + xproto::AtomEnum::ATOM, + &[atoms.WM_DELETE_WINDOW], + ) + .unwrap(); + + xcb_connection + .xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: 1, + mask: vec![ + xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::LEAVE, + ], + }], + ) + .unwrap(); + + xcb_connection.map_window(x_window).unwrap(); + xcb_connection.flush().unwrap(); + + let raw = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( + xcb_connection, + ) as *mut _, + screen_id: x_screen_index, + window_id: x_window, + visual_id: visual.id, + }; + let gpu = Arc::new( + unsafe { + gpu::Context::init_windowed( + &raw, + gpu::ContextDesc { + validation: false, + capture: false, + overlay: false, + }, + ) + } + .unwrap(), + ); + + let config = BladeSurfaceConfig { + // Note: this has to be done after the GPU init, or otherwise + // the sizes are immediately invalidated. + size: query_render_extent(xcb_connection, x_window), + transparent: params.window_background != WindowBackgroundAppearance::Opaque, + }; + + Self { + client, + executor, + display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()), + raw, + bounds: params.bounds.map(|v| v.0), + scale_factor, + renderer: BladeRenderer::new(gpu, config), + atoms: *atoms, + input_handler: None, + } + } + + fn content_size(&self) -> Size { + let size = self.renderer.viewport_size(); + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +pub(crate) struct X11Window(pub X11WindowStatePtr); + +impl Drop for X11Window { + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); + state.renderer.destroy(); + + self.0.xcb_connection.unmap_window(self.0.x_window).unwrap(); + self.0 + .xcb_connection + .destroy_window(self.0.x_window) + .unwrap(); + self.0.xcb_connection.flush().unwrap(); + + let this_ptr = self.0.clone(); + let client_ptr = state.client.clone(); + state + .executor + .spawn(async move { + this_ptr.close(); + client_ptr.drop_window(this_ptr.x_window); + }) + .detach(); + drop(state); + } +} + +impl X11Window { + #[allow(clippy::too_many_arguments)] + pub fn new( + client: X11ClientStatePtr, + executor: ForegroundExecutor, + params: WindowParams, + xcb_connection: &Rc, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + ) -> Self { + Self(X11WindowStatePtr { + state: Rc::new(RefCell::new(X11WindowState::new( + client, + executor, + params, + xcb_connection, + x_main_screen_index, + x_window, + atoms, + scale_factor, + ))), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + xcb_connection: xcb_connection.clone(), + x_window, + }) + } +} + +impl X11WindowStatePtr { + pub fn should_close(&self) -> bool { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + result + } else { + true + } + } + + pub fn close(&self) { + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } + + pub fn refresh(&self) { + let mut cb = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = cb.request_frame { + fun(); + } + } + + pub fn handle_input(&self, input: PlatformInput) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().input { + if !fun(input.clone()).propagate { + return; + } + } + if let PlatformInput::KeyDown(event) = input { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + if let Some(ime_key) = &event.keystroke.ime_key { + drop(state); + input_handler.replace_text_in_range(None, ime_key); + state = self.state.borrow_mut(); + } + state.input_handler = Some(input_handler); + } + } + } + + pub fn configure(&self, bounds: Bounds) { + let mut resize_args = None; + let do_move; + { + let mut state = self.state.borrow_mut(); + let old_bounds = mem::replace(&mut state.bounds, bounds); + do_move = old_bounds.origin != bounds.origin; + // todo(linux): use normal GPUI types here, refactor out the double + // viewport check and extra casts ( ) + let gpu_size = query_render_extent(&self.xcb_connection, self.x_window); + if state.renderer.viewport_size() != gpu_size { + state + .renderer + .update_drawable_size(size(gpu_size.width as f64, gpu_size.height as f64)); + resize_args = Some((state.content_size(), state.scale_factor)); + } + } + + let mut callbacks = self.callbacks.borrow_mut(); + if let Some((content_size, scale_factor)) = resize_args { + if let Some(ref mut fun) = callbacks.resize { + fun(content_size, scale_factor) + } + } + if do_move { + if let Some(ref mut fun) = callbacks.moved { + fun() + } + } + } + + pub fn set_focused(&self, focus: bool) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change { + fun(focus); + } + } +} + +impl PlatformWindow for X11Window { + fn bounds(&self) -> Bounds { + self.0.state.borrow().bounds.map(|v| v.into()) + } + + // todo(linux) + fn is_maximized(&self) -> bool { + false + } + + // todo(linux) + fn window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p))) + } + + fn content_size(&self) -> Size { + // We divide by the scale factor here because this value is queried to determine how much to draw, + // but it will be multiplied later by the scale to adjust for scaling. + let state = self.0.state.borrow(); + state + .content_size() + .map(|size| size.div(state.scale_factor)) + } + + fn scale_factor(&self) -> f32 { + self.0.state.borrow().scale_factor + } + + // todo(linux) + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } + + fn display(&self) -> Rc { + self.0.state.borrow().display.clone() + } + + fn mouse_position(&self) -> Point { + let reply = self + .0 + .xcb_connection + .query_pointer(self.0.x_window) + .unwrap() + .reply() + .unwrap(); + Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into()) + } + + // todo(linux) + fn modifiers(&self) -> Modifiers { + Modifiers::default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.state.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.state.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[&str], + ) -> Option> { + None + } + + fn activate(&self) { + let win_aux = xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE); + self.0 + .xcb_connection + .configure_window(self.0.x_window, &win_aux) + .log_err(); + } + + // todo(linux) + fn is_active(&self) -> bool { + false + } + + fn set_title(&mut self, title: &str) { + self.0 + .xcb_connection + .change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ) + .unwrap(); + + self.0 + .xcb_connection + .change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + self.0.state.borrow().atoms._NET_WM_NAME, + self.0.state.borrow().atoms.UTF8_STRING, + title.as_bytes(), + ) + .unwrap(); + } + + fn set_app_id(&mut self, app_id: &str) { + let mut data = Vec::with_capacity(app_id.len() * 2 + 1); + data.extend(app_id.bytes()); // instance https://unix.stackexchange.com/a/494170 + data.push(b'\0'); + data.extend(app_id.bytes()); // class + + self.0.xcb_connection.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_CLASS, + xproto::AtomEnum::STRING, + &data, + ); + } + + // todo(linux) + fn set_edited(&mut self, edited: bool) {} + + fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + let mut inner = self.0.state.borrow_mut(); + let transparent = background_appearance != WindowBackgroundAppearance::Opaque; + inner.renderer.update_transparency(transparent); + } + + // todo(linux), this corresponds to `orderFrontCharacterPalette` on macOS, + // but it looks like the equivalent for Linux is GTK specific: + // + // https://docs.gtk.org/gtk3/signal.Entry.insert-emoji.html + // + // This API might need to change, or we might need to build an emoji picker into GPUI + fn show_character_palette(&self) { + unimplemented!() + } + + // todo(linux) + fn minimize(&self) { + unimplemented!() + } + + // todo(linux) + fn zoom(&self) { + unimplemented!() + } + + // todo(linux) + fn toggle_fullscreen(&self) { + unimplemented!() + } + + // todo(linux) + fn is_fullscreen(&self) -> bool { + false + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box crate::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let mut inner = self.0.state.borrow_mut(); + inner.renderer.draw(scene); + } + + fn sprite_atlas(&self) -> sync::Arc { + let inner = self.0.state.borrow(); + inner.renderer.sprite_atlas().clone() + } +} diff --git a/crates/ming/src/platform/mac.rs b/crates/ming/src/platform/mac.rs new file mode 100644 index 0000000..62cca0d --- /dev/null +++ b/crates/ming/src/platform/mac.rs @@ -0,0 +1,130 @@ +//! Macos screen have a y axis that goings up from the bottom of the screen and +//! an origin at the bottom left of the main display. +mod dispatcher; +mod display; +mod display_link; +mod events; + +#[cfg(not(feature = "macos-blade"))] +mod metal_atlas; +#[cfg(not(feature = "macos-blade"))] +pub mod metal_renderer; + +#[cfg(not(feature = "macos-blade"))] +use metal_renderer as renderer; + +#[cfg(feature = "macos-blade")] +use crate::platform::blade as renderer; + +mod open_type; +mod platform; +mod text_system; +mod window; +mod window_appearance; + +use crate::{px, size, DevicePixels, Pixels, Size}; +use cocoa::{ + base::{id, nil}, + foundation::{NSAutoreleasePool, NSNotFound, NSRect, NSSize, NSString, NSUInteger}, +}; + +use objc::runtime::{BOOL, NO, YES}; +use std::ops::Range; + +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use display_link::*; +pub(crate) use platform::*; +pub(crate) use text_system::*; +pub(crate) use window::*; + +trait BoolExt { + fn to_objc(self) -> BOOL; +} + +impl BoolExt for bool { + fn to_objc(self) -> BOOL { + if self { + YES + } else { + NO + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct NSRange { + pub location: NSUInteger, + pub length: NSUInteger, +} + +impl NSRange { + fn invalid() -> Self { + Self { + location: NSNotFound as NSUInteger, + length: 0, + } + } + + fn is_valid(&self) -> bool { + self.location != NSNotFound as NSUInteger + } + + fn to_range(self) -> Option> { + if self.is_valid() { + let start = self.location as usize; + let end = start + self.length as usize; + Some(start..end) + } else { + None + } + } +} + +impl From> for NSRange { + fn from(range: Range) -> Self { + NSRange { + location: range.start as NSUInteger, + length: range.len() as NSUInteger, + } + } +} + +unsafe impl objc::Encode for NSRange { + fn encode() -> objc::Encoding { + let encoding = format!( + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str() + ); + unsafe { objc::Encoding::from_str(&encoding) } + } +} + +unsafe fn ns_string(string: &str) -> id { + NSString::alloc(nil).init_str(string).autorelease() +} + +impl From for Size { + fn from(value: NSSize) -> Self { + Size { + width: px(value.width as f32), + height: px(value.height as f32), + } + } +} + +impl From for Size { + fn from(rect: NSRect) -> Self { + let NSSize { width, height } = rect.size; + size(width.into(), height.into()) + } +} + +impl From for Size { + fn from(rect: NSRect) -> Self { + let NSSize { width, height } = rect.size; + size(DevicePixels(width as i32), DevicePixels(height as i32)) + } +} diff --git a/crates/ming/src/platform/mac/dispatch.h b/crates/ming/src/platform/mac/dispatch.h new file mode 100644 index 0000000..54f3818 --- /dev/null +++ b/crates/ming/src/platform/mac/dispatch.h @@ -0,0 +1,2 @@ +#include +#include diff --git a/crates/ming/src/platform/mac/dispatcher.rs b/crates/ming/src/platform/mac/dispatcher.rs new file mode 100644 index 0000000..776ca4f --- /dev/null +++ b/crates/ming/src/platform/mac/dispatcher.rs @@ -0,0 +1,107 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::{PlatformDispatcher, TaskLabel}; +use async_task::Runnable; +use objc::{ + class, msg_send, + runtime::{BOOL, YES}, + sel, sel_impl, +}; +use parking::{Parker, Unparker}; +use parking_lot::Mutex; +use std::{ + ffi::c_void, + ptr::{addr_of, NonNull}, + sync::Arc, + time::Duration, +}; + +/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent +/// these pub items from leaking into public API. +pub(crate) mod dispatch_sys { + include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); +} + +use dispatch_sys::*; +pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t { + unsafe { addr_of!(_dispatch_main_q) as *const _ as dispatch_queue_t } +} + +pub(crate) struct MacDispatcher { + parker: Arc>, +} + +impl Default for MacDispatcher { + fn default() -> Self { + Self::new() + } +} + +impl MacDispatcher { + pub fn new() -> Self { + MacDispatcher { + parker: Arc::new(Mutex::new(Parker::new())), + } + } +} + +impl PlatformDispatcher for MacDispatcher { + fn is_main_thread(&self) -> bool { + let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] }; + is_main_thread == YES + } + + fn dispatch(&self, runnable: Runnable, _: Option) { + unsafe { + dispatch_async_f( + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0), + runnable.into_raw().as_ptr() as *mut c_void, + Some(trampoline), + ); + } + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + runnable.into_raw().as_ptr() as *mut c_void, + Some(trampoline), + ); + } + } + + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + unsafe { + let queue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0); + let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64); + dispatch_after_f( + when, + queue, + runnable.into_raw().as_ptr() as *mut c_void, + Some(trampoline), + ); + } + } + + fn park(&self, timeout: Option) -> bool { + if let Some(timeout) = timeout { + self.parker.lock().park_timeout(timeout) + } else { + self.parker.lock().park(); + true + } + } + + fn unparker(&self) -> Unparker { + self.parker.lock().unparker() + } +} + +extern "C" fn trampoline(runnable: *mut c_void) { + let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) }; + task.run(); +} diff --git a/crates/ming/src/platform/mac/display.rs b/crates/ming/src/platform/mac/display.rs new file mode 100644 index 0000000..ce3e5ef --- /dev/null +++ b/crates/ming/src/platform/mac/display.rs @@ -0,0 +1,120 @@ +use crate::{point, size, Bounds, DevicePixels, DisplayId, PlatformDisplay}; +use anyhow::Result; +use cocoa::{ + appkit::NSScreen, + base::{id, nil}, + foundation::{NSDictionary, NSString}, +}; +use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}; +use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList}; +use objc::{msg_send, sel, sel_impl}; +use uuid::Uuid; + +#[derive(Debug)] +pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID); + +unsafe impl Send for MacDisplay {} + +impl MacDisplay { + /// Get the screen with the given [`DisplayId`]. + pub fn find_by_id(id: DisplayId) -> Option { + Self::all().find(|screen| screen.id() == id) + } + + /// Get the primary screen - the one with the menu bar, and whose bottom left + /// corner is at the origin of the AppKit coordinate system. + pub fn primary() -> Self { + // Instead of iterating through all active systems displays via `all()` we use the first + // NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList` + // will always return a list of active displays (machine might be sleeping). + // + // The following is what Chromium does too: + // + // https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56 + unsafe { + let screens = NSScreen::screens(nil); + let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0); + let device_description = NSScreen::deviceDescription(screen); + let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber"); + let screen_number = device_description.objectForKey_(screen_number_key); + let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue]; + Self(screen_number) + } + } + + /// Obtains an iterator over all currently active system displays. + pub fn all() -> impl Iterator { + unsafe { + // We're assuming there aren't more than 32 displays connected to the system. + let mut displays = Vec::with_capacity(32); + let mut display_count = 0; + let result = CGGetActiveDisplayList( + displays.capacity() as u32, + displays.as_mut_ptr(), + &mut display_count, + ); + + if result == 0 { + displays.set_len(display_count as usize); + displays.into_iter().map(MacDisplay) + } else { + panic!("Failed to get active display list. Result: {result}"); + } + } + } +} + +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} + +impl PlatformDisplay for MacDisplay { + fn id(&self) -> DisplayId { + DisplayId(self.0) + } + + fn uuid(&self) -> Result { + let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) }; + anyhow::ensure!( + !cfuuid.is_null(), + "AppKit returned a null from CGDisplayCreateUUIDFromDisplayID" + ); + + let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) }; + Ok(Uuid::from_bytes([ + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15, + ])) + } + + fn bounds(&self) -> Bounds { + unsafe { + // CGDisplayBounds is in "global display" coordinates, where 0 is + // the top left of the primary display. + let bounds = CGDisplayBounds(self.0); + + Bounds { + origin: point(DevicePixels(0), DevicePixels(0)), + size: size( + DevicePixels(bounds.size.width as i32), + DevicePixels(bounds.size.height as i32), + ), + } + } + } +} diff --git a/crates/ming/src/platform/mac/display_link.rs b/crates/ming/src/platform/mac/display_link.rs new file mode 100644 index 0000000..782c344 --- /dev/null +++ b/crates/ming/src/platform/mac/display_link.rs @@ -0,0 +1,267 @@ +use crate::{ + dispatch_get_main_queue, + dispatch_sys::{ + _dispatch_source_type_data_add, dispatch_resume, dispatch_set_context, + dispatch_source_cancel, dispatch_source_create, dispatch_source_merge_data, + dispatch_source_set_event_handler_f, dispatch_source_t, dispatch_suspend, + }, +}; +use anyhow::Result; +use core_graphics::display::CGDirectDisplayID; +use std::ffi::c_void; +use util::ResultExt; + +pub struct DisplayLink { + display_link: sys::DisplayLink, + frame_requests: dispatch_source_t, +} + +impl DisplayLink { + pub fn new( + display_id: CGDirectDisplayID, + data: *mut c_void, + callback: unsafe extern "C" fn(*mut c_void), + ) -> Result { + unsafe extern "C" fn display_link_callback( + _display_link_out: *mut sys::CVDisplayLink, + _current_time: *const sys::CVTimeStamp, + _output_time: *const sys::CVTimeStamp, + _flags_in: i64, + _flags_out: *mut i64, + frame_requests: *mut c_void, + ) -> i32 { + let frame_requests = frame_requests as dispatch_source_t; + dispatch_source_merge_data(frame_requests, 1); + 0 + } + + unsafe { + let frame_requests = dispatch_source_create( + &_dispatch_source_type_data_add, + 0, + 0, + dispatch_get_main_queue(), + ); + dispatch_set_context( + crate::dispatch_sys::dispatch_object_t { + _ds: frame_requests, + }, + data, + ); + dispatch_source_set_event_handler_f(frame_requests, Some(callback)); + + let display_link = sys::DisplayLink::new( + display_id, + display_link_callback, + frame_requests as *mut c_void, + )?; + + Ok(Self { + display_link, + frame_requests, + }) + } + } + + pub fn start(&mut self) -> Result<()> { + unsafe { + dispatch_resume(crate::dispatch_sys::dispatch_object_t { + _ds: self.frame_requests, + }); + self.display_link.start()?; + } + Ok(()) + } + + pub fn stop(&mut self) -> Result<()> { + unsafe { + dispatch_suspend(crate::dispatch_sys::dispatch_object_t { + _ds: self.frame_requests, + }); + self.display_link.stop()?; + } + Ok(()) + } +} + +impl Drop for DisplayLink { + fn drop(&mut self) { + self.stop().log_err(); + unsafe { + dispatch_source_cancel(self.frame_requests); + } + } +} + +mod sys { + //! Derived from display-link crate under the following license: + //! + //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc) + #![allow(dead_code, non_upper_case_globals)] + + use anyhow::Result; + use core_graphics::display::CGDirectDisplayID; + use foreign_types::{foreign_type, ForeignType}; + use std::{ + ffi::c_void, + fmt::{self, Debug, Formatter}, + }; + + #[derive(Debug)] + pub enum CVDisplayLink {} + + foreign_type! { + pub unsafe type DisplayLink { + type CType = CVDisplayLink; + fn drop = CVDisplayLinkRelease; + fn clone = CVDisplayLinkRetain; + } + } + + impl Debug for DisplayLink { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + formatter + .debug_tuple("DisplayLink") + .field(&self.as_ptr()) + .finish() + } + } + + #[repr(C)] + #[derive(Clone, Copy)] + pub(crate) struct CVTimeStamp { + pub version: u32, + pub video_time_scale: i32, + pub video_time: i64, + pub host_time: u64, + pub rate_scalar: f64, + pub video_refresh_period: i64, + pub smpte_time: CVSMPTETime, + pub flags: u64, + pub reserved: u64, + } + + pub type CVTimeStampFlags = u64; + + pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0; + pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1; + pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2; + pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3; + pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4; + pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16; + pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17; + pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags = + kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid; + pub const kCVTimeStampIsInterlaced: CVTimeStampFlags = + kCVTimeStampTopField | kCVTimeStampBottomField; + + #[repr(C)] + #[derive(Clone, Copy, Default)] + pub(crate) struct CVSMPTETime { + pub subframes: i16, + pub subframe_divisor: i16, + pub counter: u32, + pub time_type: u32, + pub flags: u32, + pub hours: i16, + pub minutes: i16, + pub seconds: i16, + pub frames: i16, + } + + pub type CVSMPTETimeType = u32; + + pub const kCVSMPTETimeType24: CVSMPTETimeType = 0; + pub const kCVSMPTETimeType25: CVSMPTETimeType = 1; + pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2; + pub const kCVSMPTETimeType30: CVSMPTETimeType = 3; + pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4; + pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5; + pub const kCVSMPTETimeType60: CVSMPTETimeType = 6; + pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7; + + pub type CVSMPTETimeFlags = u32; + + pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0; + pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1; + + pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn( + display_link_out: *mut CVDisplayLink, + // A pointer to the current timestamp. This represents the timestamp when the callback is called. + current_time: *const CVTimeStamp, + // A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed. + output_time: *const CVTimeStamp, + // Unused + flags_in: i64, + // Unused + flags_out: *mut i64, + // A pointer to app-defined data. + display_link_context: *mut c_void, + ) -> i32; + + #[link(name = "CoreFoundation", kind = "framework")] + #[link(name = "CoreVideo", kind = "framework")] + #[allow(improper_ctypes)] + extern "C" { + pub fn CVDisplayLinkCreateWithActiveCGDisplays( + display_link_out: *mut *mut CVDisplayLink, + ) -> i32; + pub fn CVDisplayLinkSetCurrentCGDisplay( + display_link: &mut DisplayLinkRef, + display_id: u32, + ) -> i32; + pub fn CVDisplayLinkSetOutputCallback( + display_link: &mut DisplayLinkRef, + callback: CVDisplayLinkOutputCallback, + user_info: *mut c_void, + ) -> i32; + pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32; + pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32; + pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink); + pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink; + } + + impl DisplayLink { + /// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc) + pub unsafe fn new( + display_id: CGDirectDisplayID, + callback: CVDisplayLinkOutputCallback, + user_info: *mut c_void, + ) -> Result { + let mut display_link: *mut CVDisplayLink = 0 as _; + + let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link); + anyhow::ensure!(code == 0, "could not create display link, code: {}", code); + + let mut display_link = DisplayLink::from_ptr(display_link); + + let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info); + anyhow::ensure!(code == 0, "could not set output callback, code: {}", code); + + let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id); + anyhow::ensure!( + code == 0, + "could not assign display to display link, code: {}", + code + ); + + Ok(display_link) + } + } + + impl DisplayLinkRef { + /// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc) + pub unsafe fn start(&mut self) -> Result<()> { + let code = CVDisplayLinkStart(self); + anyhow::ensure!(code == 0, "could not start display link, code: {}", code); + Ok(()) + } + + /// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc) + pub unsafe fn stop(&mut self) -> Result<()> { + let code = CVDisplayLinkStop(self); + anyhow::ensure!(code == 0, "could not stop display link, code: {}", code); + Ok(()) + } + } +} diff --git a/crates/ming/src/platform/mac/events.rs b/crates/ming/src/platform/mac/events.rs new file mode 100644 index 0000000..e500acc --- /dev/null +++ b/crates/ming/src/platform/mac/events.rs @@ -0,0 +1,359 @@ +use crate::{ + point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, + PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase, +}; +use cocoa::{ + appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, + base::{id, YES}, + foundation::NSString as _, +}; +use core_graphics::{ + event::{CGEvent, CGEventFlags, CGKeyCode}, + event_source::{CGEventSource, CGEventSourceStateID}, +}; +use ctor::ctor; +use metal::foreign_types::ForeignType as _; +use objc::{class, msg_send, sel, sel_impl}; +use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr}; + +const BACKSPACE_KEY: u16 = 0x7f; +const SPACE_KEY: u16 = b' ' as u16; +const ENTER_KEY: u16 = 0x0d; +const NUMPAD_ENTER_KEY: u16 = 0x03; +const ESCAPE_KEY: u16 = 0x1b; +const TAB_KEY: u16 = 0x09; +const SHIFT_TAB_KEY: u16 = 0x19; + +static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut(); + +#[ctor] +unsafe fn build_event_source() { + let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap(); + EVENT_SOURCE = source.as_ptr(); + mem::forget(source); +} + +pub fn key_to_native(key: &str) -> Cow { + use cocoa::appkit::*; + let code = match key { + "space" => SPACE_KEY, + "backspace" => BACKSPACE_KEY, + "up" => NSUpArrowFunctionKey, + "down" => NSDownArrowFunctionKey, + "left" => NSLeftArrowFunctionKey, + "right" => NSRightArrowFunctionKey, + "pageup" => NSPageUpFunctionKey, + "pagedown" => NSPageDownFunctionKey, + "home" => NSHomeFunctionKey, + "end" => NSEndFunctionKey, + "delete" => NSDeleteFunctionKey, + "f1" => NSF1FunctionKey, + "f2" => NSF2FunctionKey, + "f3" => NSF3FunctionKey, + "f4" => NSF4FunctionKey, + "f5" => NSF5FunctionKey, + "f6" => NSF6FunctionKey, + "f7" => NSF7FunctionKey, + "f8" => NSF8FunctionKey, + "f9" => NSF9FunctionKey, + "f10" => NSF10FunctionKey, + "f11" => NSF11FunctionKey, + "f12" => NSF12FunctionKey, + _ => return Cow::Borrowed(key), + }; + Cow::Owned(String::from_utf16(&[code]).unwrap()) +} + +unsafe fn read_modifiers(native_event: id) -> Modifiers { + let modifiers = native_event.modifierFlags(); + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); + + Modifiers { + control, + alt, + shift, + platform: command, + function, + } +} + +impl PlatformInput { + pub(crate) unsafe fn from_native( + native_event: id, + window_height: Option, + ) -> Option { + let event_type = native_event.eventType(); + + // Filter out event types that aren't in the NSEventType enum. + // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details. + match event_type as u64 { + 0 | 21 | 32 | 33 | 35 | 36 | 37 => { + return None; + } + _ => {} + } + + match event_type { + NSEventType::NSFlagsChanged => Some(Self::ModifiersChanged(ModifiersChangedEvent { + modifiers: read_modifiers(native_event), + })), + NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent { + keystroke: parse_keystroke(native_event), + is_held: native_event.isARepeat() == YES, + })), + NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent { + keystroke: parse_keystroke(native_event), + })), + NSEventType::NSLeftMouseDown + | NSEventType::NSRightMouseDown + | NSEventType::NSOtherMouseDown => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + window_height.map(|window_height| { + Self::MouseDown(MouseDownEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + // MacOS screen coordinates are relative to bottom left + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + first_mouse: false, + }) + }) + } + NSEventType::NSLeftMouseUp + | NSEventType::NSRightMouseUp + | NSEventType::NSOtherMouseUp => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + + window_height.map(|window_height| { + Self::MouseUp(MouseUpEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + }) + }) + } + NSEventType::NSScrollWheel => window_height.map(|window_height| { + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + TouchPhase::Started + } + NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; + + let raw_data = point( + native_event.scrollingDeltaX() as f32, + native_event.scrollingDeltaY() as f32, + ); + + let delta = if native_event.hasPreciseScrollingDeltas() == YES { + ScrollDelta::Pixels(raw_data.map(px)) + } else { + ScrollDelta::Lines(raw_data) + }; + + Self::ScrollWheel(ScrollWheelEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + delta, + touch_phase: phase, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSLeftMouseDragged + | NSEventType::NSRightMouseDragged + | NSEventType::NSOtherMouseDragged => { + let pressed_button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + + window_height.map(|window_height| { + Self::MouseMove(MouseMoveEvent { + pressed_button: Some(pressed_button), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + }) + }) + } + NSEventType::NSMouseMoved => window_height.map(|window_height| { + Self::MouseMove(MouseMoveEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSMouseExited => window_height.map(|window_height| { + Self::MouseExited(MouseExitEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + _ => None, + } + } +} + +unsafe fn parse_keystroke(native_event: id) -> Keystroke { + use cocoa::appkit::*; + + let mut chars_ignoring_modifiers = + CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) + .to_str() + .unwrap() + .to_string(); + let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); + let modifiers = native_event.modifierFlags(); + + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) + && first_char.map_or(true, |ch| { + !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch) + }); + + #[allow(non_upper_case_globals)] + let key = match first_char { + Some(SPACE_KEY) => "space".to_string(), + Some(BACKSPACE_KEY) => "backspace".to_string(), + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(), + Some(ESCAPE_KEY) => "escape".to_string(), + Some(TAB_KEY) => "tab".to_string(), + Some(SHIFT_TAB_KEY) => "tab".to_string(), + Some(NSUpArrowFunctionKey) => "up".to_string(), + Some(NSDownArrowFunctionKey) => "down".to_string(), + Some(NSLeftArrowFunctionKey) => "left".to_string(), + Some(NSRightArrowFunctionKey) => "right".to_string(), + Some(NSPageUpFunctionKey) => "pageup".to_string(), + Some(NSPageDownFunctionKey) => "pagedown".to_string(), + Some(NSHomeFunctionKey) => "home".to_string(), + Some(NSEndFunctionKey) => "end".to_string(), + Some(NSDeleteFunctionKey) => "delete".to_string(), + Some(NSF1FunctionKey) => "f1".to_string(), + Some(NSF2FunctionKey) => "f2".to_string(), + Some(NSF3FunctionKey) => "f3".to_string(), + Some(NSF4FunctionKey) => "f4".to_string(), + Some(NSF5FunctionKey) => "f5".to_string(), + Some(NSF6FunctionKey) => "f6".to_string(), + Some(NSF7FunctionKey) => "f7".to_string(), + Some(NSF8FunctionKey) => "f8".to_string(), + Some(NSF9FunctionKey) => "f9".to_string(), + Some(NSF10FunctionKey) => "f10".to_string(), + Some(NSF11FunctionKey) => "f11".to_string(), + Some(NSF12FunctionKey) => "f12".to_string(), + _ => { + let mut chars_ignoring_modifiers_and_shift = + chars_for_modified_key(native_event.keyCode(), false, false); + + // Honor ⌘ when Dvorak-QWERTY is used. + let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false); + if command && chars_ignoring_modifiers_and_shift != chars_with_cmd { + chars_ignoring_modifiers = + chars_for_modified_key(native_event.keyCode(), true, shift); + chars_ignoring_modifiers_and_shift = chars_with_cmd; + } + + if shift { + if chars_ignoring_modifiers_and_shift + == chars_ignoring_modifiers.to_ascii_lowercase() + { + chars_ignoring_modifiers_and_shift + } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers { + shift = false; + chars_ignoring_modifiers + } else { + chars_ignoring_modifiers + } + } else { + chars_ignoring_modifiers + } + } + }; + + Keystroke { + modifiers: Modifiers { + control, + alt, + shift, + platform: command, + function, + }, + key, + ime_key: None, + } +} + +fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String { + // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that + // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing + // an event with the given flags instead lets us access `characters`, which always + // returns a valid string. + let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) }; + let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap(); + mem::forget(source); + + let mut flags = CGEventFlags::empty(); + if cmd { + flags |= CGEventFlags::CGEventFlagCommand; + } + if shift { + flags |= CGEventFlags::CGEventFlagShift; + } + event.set_flags(flags); + + unsafe { + let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event]; + CStr::from_ptr(event.characters().UTF8String()) + .to_str() + .unwrap() + .to_string() + } +} diff --git a/crates/ming/src/platform/mac/metal_atlas.rs b/crates/ming/src/platform/mac/metal_atlas.rs new file mode 100644 index 0000000..49dd38c --- /dev/null +++ b/crates/ming/src/platform/mac/metal_atlas.rs @@ -0,0 +1,254 @@ +use crate::{ + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, + Point, Size, +}; +use anyhow::{anyhow, Result}; +use collections::FxHashMap; +use derive_more::{Deref, DerefMut}; +use etagere::BucketedAtlasAllocator; +use metal::Device; +use parking_lot::Mutex; +use std::borrow::Cow; + +pub(crate) struct MetalAtlas(Mutex); + +impl MetalAtlas { + pub(crate) fn new(device: Device) -> Self { + MetalAtlas(Mutex::new(MetalAtlasState { + device: AssertSend(device), + monochrome_textures: Default::default(), + polychrome_textures: Default::default(), + path_textures: Default::default(), + tiles_by_key: Default::default(), + })) + } + + pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture { + self.0.lock().texture(id).metal_texture.clone() + } + + pub(crate) fn allocate( + &self, + size: Size, + texture_kind: AtlasTextureKind, + ) -> Option { + self.0.lock().allocate(size, texture_kind) + } + + pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { + let mut lock = self.0.lock(); + let textures = match texture_kind { + AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, + AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + AtlasTextureKind::Path => &mut lock.path_textures, + }; + for texture in textures { + texture.clear(); + } + } +} + +struct MetalAtlasState { + device: AssertSend, + monochrome_textures: Vec, + polychrome_textures: Vec, + path_textures: Vec, + tiles_by_key: FxHashMap, +} + +impl PlatformAtlas for MetalAtlas { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result<(Size, Cow<'a, [u8]>)>, + ) -> Result { + let mut lock = self.0.lock(); + if let Some(tile) = lock.tiles_by_key.get(key) { + Ok(tile.clone()) + } else { + let (size, bytes) = build()?; + let tile = lock + .allocate(size, key.texture_kind()) + .ok_or_else(|| anyhow!("failed to allocate"))?; + let texture = lock.texture(tile.texture_id); + texture.upload(tile.bounds, &bytes); + lock.tiles_by_key.insert(key.clone(), tile.clone()); + Ok(tile) + } + } +} + +impl MetalAtlasState { + fn allocate( + &mut self, + size: Size, + texture_kind: AtlasTextureKind, + ) -> Option { + let textures = match texture_kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + AtlasTextureKind::Path => &mut self.path_textures, + }; + + textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + .or_else(|| { + let texture = self.push_texture(size, texture_kind); + texture.allocate(size) + }) + } + + fn push_texture( + &mut self, + min_size: Size, + kind: AtlasTextureKind, + ) -> &mut MetalAtlasTexture { + const DEFAULT_ATLAS_SIZE: Size = Size { + width: DevicePixels(1024), + height: DevicePixels(1024), + }; + // Max texture size on all modern Apple GPUs. Anything bigger than that crashes in validateWithDevice. + const MAX_ATLAS_SIZE: Size = Size { + width: DevicePixels(16384), + height: DevicePixels(16384), + }; + let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE); + let texture_descriptor = metal::TextureDescriptor::new(); + texture_descriptor.set_width(size.width.into()); + texture_descriptor.set_height(size.height.into()); + let pixel_format; + let usage; + match kind { + AtlasTextureKind::Monochrome => { + pixel_format = metal::MTLPixelFormat::A8Unorm; + usage = metal::MTLTextureUsage::ShaderRead; + } + AtlasTextureKind::Polychrome => { + pixel_format = metal::MTLPixelFormat::BGRA8Unorm; + usage = metal::MTLTextureUsage::ShaderRead; + } + AtlasTextureKind::Path => { + pixel_format = metal::MTLPixelFormat::R16Float; + usage = metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead; + } + } + texture_descriptor.set_pixel_format(pixel_format); + texture_descriptor.set_usage(usage); + let metal_texture = self.device.new_texture(&texture_descriptor); + + let textures = match kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + AtlasTextureKind::Path => &mut self.path_textures, + }; + let atlas_texture = MetalAtlasTexture { + id: AtlasTextureId { + index: textures.len() as u32, + kind, + }, + allocator: etagere::BucketedAtlasAllocator::new(size.into()), + metal_texture: AssertSend(metal_texture), + }; + textures.push(atlas_texture); + textures.last_mut().unwrap() + } + + fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture { + let textures = match id.kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, + }; + &textures[id.index as usize] + } +} + +struct MetalAtlasTexture { + id: AtlasTextureId, + allocator: BucketedAtlasAllocator, + metal_texture: AssertSend, +} + +impl MetalAtlasTexture { + fn clear(&mut self) { + self.allocator.clear(); + } + + fn allocate(&mut self, size: Size) -> Option { + let allocation = self.allocator.allocate(size.into())?; + let tile = AtlasTile { + texture_id: self.id, + tile_id: allocation.id.into(), + bounds: Bounds { + origin: allocation.rectangle.min.into(), + size, + }, + padding: 0, + }; + Some(tile) + } + + fn upload(&self, bounds: Bounds, bytes: &[u8]) { + let region = metal::MTLRegion::new_2d( + bounds.origin.x.into(), + bounds.origin.y.into(), + bounds.size.width.into(), + bounds.size.height.into(), + ); + self.metal_texture.replace_region( + region, + 0, + bytes.as_ptr() as *const _, + bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64, + ); + } + + fn bytes_per_pixel(&self) -> u8 { + use metal::MTLPixelFormat::*; + match self.metal_texture.pixel_format() { + A8Unorm | R8Unorm => 1, + RGBA8Unorm | BGRA8Unorm => 4, + _ => unimplemented!(), + } + } +} + +impl From> for etagere::Size { + fn from(size: Size) -> Self { + etagere::Size::new(size.width.into(), size.height.into()) + } +} + +impl From for Point { + fn from(value: etagere::Point) -> Self { + Point { + x: DevicePixels::from(value.x), + y: DevicePixels::from(value.y), + } + } +} + +impl From for Size { + fn from(size: etagere::Size) -> Self { + Size { + width: DevicePixels::from(size.width), + height: DevicePixels::from(size.height), + } + } +} + +impl From for Bounds { + fn from(rectangle: etagere::Rectangle) -> Self { + Bounds { + origin: rectangle.min.into(), + size: rectangle.size().into(), + } + } +} + +#[derive(Deref, DerefMut)] +struct AssertSend(T); + +unsafe impl Send for AssertSend {} diff --git a/crates/ming/src/platform/mac/metal_renderer.rs b/crates/ming/src/platform/mac/metal_renderer.rs new file mode 100644 index 0000000..921924a --- /dev/null +++ b/crates/ming/src/platform/mac/metal_renderer.rs @@ -0,0 +1,1167 @@ +use super::metal_atlas::MetalAtlas; +use crate::{ + point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, + Hsla, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, + ScaledPixels, Scene, Shadow, Size, Surface, Underline, +}; +use block::ConcreteBlock; +use cocoa::{ + base::{NO, YES}, + foundation::NSUInteger, + quartzcore::AutoresizingMask, +}; +use collections::HashMap; +use core_foundation::base::TCFType; +use foreign_types::ForeignType; +use media::core_video::CVMetalTextureCache; +use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; +use objc::{self, msg_send, sel, sel_impl}; +use parking_lot::Mutex; +use smallvec::SmallVec; +use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc}; + +// Exported to metal +pub(crate) type PointF = crate::Point; + +#[cfg(not(feature = "runtime_shaders"))] +const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); +#[cfg(feature = "runtime_shaders")] +const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); +const INSTANCE_BUFFER_SIZE: usize = 2 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...) + +pub type Context = Arc>>; +pub type Renderer = MetalRenderer; + +pub unsafe fn new_renderer( + context: self::Context, + _native_window: *mut c_void, + _native_view: *mut c_void, + _bounds: crate::Size, + _transparent: bool, +) -> Renderer { + MetalRenderer::new(context) +} + +pub(crate) struct MetalRenderer { + device: metal::Device, + layer: metal::MetalLayer, + presents_with_transaction: bool, + command_queue: CommandQueue, + paths_rasterization_pipeline_state: metal::RenderPipelineState, + path_sprites_pipeline_state: metal::RenderPipelineState, + shadows_pipeline_state: metal::RenderPipelineState, + quads_pipeline_state: metal::RenderPipelineState, + underlines_pipeline_state: metal::RenderPipelineState, + monochrome_sprites_pipeline_state: metal::RenderPipelineState, + polychrome_sprites_pipeline_state: metal::RenderPipelineState, + surfaces_pipeline_state: metal::RenderPipelineState, + unit_vertices: metal::Buffer, + #[allow(clippy::arc_with_non_send_sync)] + instance_buffer_pool: Arc>>, + sprite_atlas: Arc, + core_video_texture_cache: CVMetalTextureCache, +} + +impl MetalRenderer { + pub fn new(instance_buffer_pool: Arc>>) -> Self { + let device: metal::Device = if let Some(device) = metal::Device::system_default() { + device + } else { + log::error!("unable to access a compatible graphics device"); + std::process::exit(1); + }; + + let layer = metal::MetalLayer::new(); + layer.set_device(&device); + layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + layer.set_opaque(false); + layer.set_maximum_drawable_count(3); + unsafe { + let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO]; + let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES]; + let _: () = msg_send![ + &*layer, + setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE + | AutoresizingMask::HEIGHT_SIZABLE + ]; + } + #[cfg(feature = "runtime_shaders")] + let library = device + .new_library_with_source(&SHADERS_SOURCE_FILE, &metal::CompileOptions::new()) + .expect("error building metal library"); + #[cfg(not(feature = "runtime_shaders"))] + let library = device + .new_library_with_data(SHADERS_METALLIB) + .expect("error building metal library"); + + fn to_float2_bits(point: PointF) -> u64 { + let mut output = point.y.to_bits() as u64; + output <<= 32; + output |= point.x.to_bits() as u64; + output + } + + let unit_vertices = [ + to_float2_bits(point(0., 0.)), + to_float2_bits(point(1., 0.)), + to_float2_bits(point(0., 1.)), + to_float2_bits(point(0., 1.)), + to_float2_bits(point(1., 0.)), + to_float2_bits(point(1., 1.)), + ]; + let unit_vertices = device.new_buffer_with_data( + unit_vertices.as_ptr() as *const c_void, + mem::size_of_val(&unit_vertices) as u64, + MTLResourceOptions::StorageModeManaged, + ); + + let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state( + &device, + &library, + "paths_rasterization", + "path_rasterization_vertex", + "path_rasterization_fragment", + MTLPixelFormat::R16Float, + ); + let path_sprites_pipeline_state = build_pipeline_state( + &device, + &library, + "path_sprites", + "path_sprite_vertex", + "path_sprite_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let shadows_pipeline_state = build_pipeline_state( + &device, + &library, + "shadows", + "shadow_vertex", + "shadow_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let quads_pipeline_state = build_pipeline_state( + &device, + &library, + "quads", + "quad_vertex", + "quad_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let underlines_pipeline_state = build_pipeline_state( + &device, + &library, + "underlines", + "underline_vertex", + "underline_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let monochrome_sprites_pipeline_state = build_pipeline_state( + &device, + &library, + "monochrome_sprites", + "monochrome_sprite_vertex", + "monochrome_sprite_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let polychrome_sprites_pipeline_state = build_pipeline_state( + &device, + &library, + "polychrome_sprites", + "polychrome_sprite_vertex", + "polychrome_sprite_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + let surfaces_pipeline_state = build_pipeline_state( + &device, + &library, + "surfaces", + "surface_vertex", + "surface_fragment", + MTLPixelFormat::BGRA8Unorm, + ); + + let command_queue = device.new_command_queue(); + let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); + let core_video_texture_cache = + unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() }; + + Self { + device, + layer, + presents_with_transaction: false, + command_queue, + paths_rasterization_pipeline_state, + path_sprites_pipeline_state, + shadows_pipeline_state, + quads_pipeline_state, + underlines_pipeline_state, + monochrome_sprites_pipeline_state, + polychrome_sprites_pipeline_state, + surfaces_pipeline_state, + unit_vertices, + instance_buffer_pool, + sprite_atlas, + core_video_texture_cache, + } + } + + pub fn layer(&self) -> &metal::MetalLayerRef { + &self.layer + } + + pub fn layer_ptr(&self) -> *mut CAMetalLayer { + self.layer.as_ptr() + } + + pub fn sprite_atlas(&self) -> &Arc { + &self.sprite_atlas + } + + pub fn set_presents_with_transaction(&mut self, presents_with_transaction: bool) { + self.presents_with_transaction = presents_with_transaction; + self.layer + .set_presents_with_transaction(presents_with_transaction); + } + + pub fn update_drawable_size(&mut self, size: Size) { + unsafe { + let _: () = msg_send![ + self.layer(), + setDrawableSize: size + ]; + } + } + + pub fn update_transparency(&mut self, _transparent: bool) { + // todo(mac)? + } + + pub fn destroy(&mut self) { + // nothing to do + } + + pub fn draw(&mut self, scene: &Scene) { + let layer = self.layer.clone(); + let viewport_size = layer.drawable_size(); + let viewport_size: Size = size( + (viewport_size.width.ceil() as i32).into(), + (viewport_size.height.ceil() as i32).into(), + ); + let drawable = if let Some(drawable) = layer.next_drawable() { + drawable + } else { + log::error!( + "failed to retrieve next drawable, drawable size: {:?}", + viewport_size + ); + return; + }; + let mut instance_buffer = self.instance_buffer_pool.lock().pop().unwrap_or_else(|| { + self.device.new_buffer( + INSTANCE_BUFFER_SIZE as u64, + MTLResourceOptions::StorageModeManaged, + ) + }); + let command_queue = self.command_queue.clone(); + let command_buffer = command_queue.new_command_buffer(); + let mut instance_offset = 0; + + let Some(path_tiles) = self.rasterize_paths( + scene.paths(), + &mut instance_buffer, + &mut instance_offset, + command_buffer, + ) else { + log::error!("failed to rasterize {} paths", scene.paths().len()); + return; + }; + + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + + color_attachment.set_texture(Some(drawable.texture())); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + let alpha = if self.layer.is_opaque() { 1. } else { 0. }; + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); + let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + + command_encoder.set_viewport(metal::MTLViewport { + originX: 0.0, + originY: 0.0, + width: i32::from(viewport_size.width) as f64, + height: i32::from(viewport_size.height) as f64, + znear: 0.0, + zfar: 1.0, + }); + + for batch in scene.batches() { + let ok = match batch { + PrimitiveBatch::Shadows(shadows) => self.draw_shadows( + shadows, + &mut instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Quads(quads) => self.draw_quads( + quads, + &mut instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Paths(paths) => self.draw_paths( + paths, + &path_tiles, + &mut instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Underlines(underlines) => self.draw_underlines( + underlines, + &mut instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::MonochromeSprites { + texture_id, + sprites, + } => self.draw_monochrome_sprites( + texture_id, + sprites, + &mut instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::PolychromeSprites { + texture_id, + sprites, + } => self.draw_polychrome_sprites( + texture_id, + sprites, + &mut instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces( + surfaces, + &mut instance_buffer, + &mut instance_offset, + viewport_size, + command_encoder, + ), + }; + + if !ok { + log::error!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces", + scene.paths.len(), + scene.shadows.len(), + scene.quads.len(), + scene.underlines.len(), + scene.monochrome_sprites.len(), + scene.polychrome_sprites.len(), + scene.surfaces.len(), + ); + break; + } + } + + command_encoder.end_encoding(); + + instance_buffer.did_modify_range(NSRange { + location: 0, + length: instance_offset as NSUInteger, + }); + + let instance_buffer_pool = self.instance_buffer_pool.clone(); + let instance_buffer = Cell::new(Some(instance_buffer)); + let block = ConcreteBlock::new(move |_| { + if let Some(instance_buffer) = instance_buffer.take() { + instance_buffer_pool.lock().push(instance_buffer); + } + }); + let block = block.copy(); + command_buffer.add_completed_handler(&block); + + self.sprite_atlas.clear_textures(AtlasTextureKind::Path); + + if self.presents_with_transaction { + command_buffer.commit(); + command_buffer.wait_until_scheduled(); + drawable.present(); + } else { + command_buffer.present_drawable(drawable); + command_buffer.commit(); + } + } + + fn rasterize_paths( + &mut self, + paths: &[Path], + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + command_buffer: &metal::CommandBufferRef, + ) -> Option> { + let mut tiles = HashMap::default(); + let mut vertices_by_texture_id = HashMap::default(); + for path in paths { + let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds); + + let tile = self + .sprite_atlas + .allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path)?; + vertices_by_texture_id + .entry(tile.texture_id) + .or_insert(Vec::new()) + .extend(path.vertices.iter().map(|vertex| PathVertex { + xy_position: vertex.xy_position - clipped_bounds.origin + + tile.bounds.origin.map(Into::into), + st_position: vertex.st_position, + content_mask: ContentMask { + bounds: tile.bounds.map(Into::into), + }, + })); + tiles.insert(path.id, tile); + } + + for (texture_id, vertices) in vertices_by_texture_id { + align_offset(instance_offset); + let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); + let next_offset = *instance_offset + vertices_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return None; + } + + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + + let texture = self.sprite_atlas.metal_texture(texture_id); + color_attachment.set_texture(Some(&texture)); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.)); + let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state); + command_encoder.set_vertex_buffer( + PathRasterizationInputIndex::Vertices as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + let texture_size = Size { + width: DevicePixels::from(texture.width()), + height: DevicePixels::from(texture.height()), + }; + command_encoder.set_vertex_bytes( + PathRasterizationInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + + let buffer_contents = + unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) }; + unsafe { + ptr::copy_nonoverlapping( + vertices.as_ptr() as *const u8, + buffer_contents, + vertices_bytes_len, + ); + } + + command_encoder.draw_primitives( + metal::MTLPrimitiveType::Triangle, + 0, + vertices.len() as u64, + ); + command_encoder.end_encoding(); + *instance_offset = next_offset; + } + + Some(tiles) + } + + fn draw_shadows( + &mut self, + shadows: &[Shadow], + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if shadows.is_empty() { + return true; + } + align_offset(instance_offset); + + command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state); + command_encoder.set_vertex_buffer( + ShadowInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + ShadowInputIndex::Shadows as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + ShadowInputIndex::Shadows as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + + command_encoder.set_vertex_bytes( + ShadowInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + let shadow_bytes_len = mem::size_of_val(shadows); + let buffer_contents = + unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + shadow_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + shadows.as_ptr() as *const u8, + buffer_contents, + shadow_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + shadows.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_quads( + &mut self, + quads: &[Quad], + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if quads.is_empty() { + return true; + } + align_offset(instance_offset); + + command_encoder.set_render_pipeline_state(&self.quads_pipeline_state); + command_encoder.set_vertex_buffer( + QuadInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + QuadInputIndex::Quads as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + QuadInputIndex::Quads as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + + command_encoder.set_vertex_bytes( + QuadInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + let quad_bytes_len = mem::size_of_val(quads); + let buffer_contents = + unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + quad_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + + unsafe { + ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + quads.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_paths( + &mut self, + paths: &[Path], + tiles_by_path_id: &HashMap, + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if paths.is_empty() { + return true; + } + + command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + let mut prev_texture_id = None; + let mut sprites = SmallVec::<[_; 1]>::new(); + let mut paths_and_tiles = paths + .iter() + .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap())) + .peekable(); + + loop { + if let Some((path, tile)) = paths_and_tiles.peek() { + if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) { + prev_texture_id = Some(tile.texture_id); + let origin = path.bounds.intersect(&path.content_mask.bounds).origin; + sprites.push(PathSprite { + bounds: Bounds { + origin: origin.map(|p| p.floor()), + size: tile.bounds.size.map(Into::into), + }, + color: path.color, + tile: (*tile).clone(), + }); + paths_and_tiles.next(); + continue; + } + } + + if sprites.is_empty() { + break; + } else { + align_offset(instance_offset); + let texture_id = prev_texture_id.take().unwrap(); + let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id); + let texture_size = size( + DevicePixels(texture.width() as i32), + DevicePixels(texture.height() as i32), + ); + + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_buffer( + SpriteInputIndex::Sprites as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder + .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + + let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + + let buffer_contents = + unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) }; + + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + sprites.clear(); + } + } + true + } + + fn draw_underlines( + &mut self, + underlines: &[Underline], + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if underlines.is_empty() { + return true; + } + align_offset(instance_offset); + + command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state); + command_encoder.set_vertex_buffer( + UnderlineInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + UnderlineInputIndex::Underlines as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_buffer( + UnderlineInputIndex::Underlines as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + + command_encoder.set_vertex_bytes( + UnderlineInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + let underline_bytes_len = mem::size_of_val(underlines); + let buffer_contents = + unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + underline_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + underlines.as_ptr() as *const u8, + buffer_contents, + underline_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + underlines.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_monochrome_sprites( + &mut self, + texture_id: AtlasTextureId, + sprites: &[MonochromeSprite], + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if sprites.is_empty() { + return true; + } + align_offset(instance_offset); + + let texture = self.sprite_atlas.metal_texture(texture_id); + let texture_size = size( + DevicePixels(texture.width() as i32), + DevicePixels(texture.height() as i32), + ); + command_encoder.set_render_pipeline_state(&self.monochrome_sprites_pipeline_state); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_buffer( + SpriteInputIndex::Sprites as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + + let sprite_bytes_len = mem::size_of_val(sprites); + let buffer_contents = + unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_polychrome_sprites( + &mut self, + texture_id: AtlasTextureId, + sprites: &[PolychromeSprite], + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + if sprites.is_empty() { + return true; + } + align_offset(instance_offset); + + let texture = self.sprite_atlas.metal_texture(texture_id); + let texture_size = size( + DevicePixels(texture.width() as i32), + DevicePixels(texture.height() as i32), + ); + command_encoder.set_render_pipeline_state(&self.polychrome_sprites_pipeline_state); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_buffer( + SpriteInputIndex::Sprites as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + + let sprite_bytes_len = mem::size_of_val(sprites); + let buffer_contents = + unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) }; + + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; + true + } + + fn draw_surfaces( + &mut self, + surfaces: &[Surface], + instance_buffer: &mut metal::Buffer, + instance_offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) -> bool { + command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state); + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + for surface in surfaces { + let texture_size = size( + DevicePixels::from(surface.image_buffer.width() as i32), + DevicePixels::from(surface.image_buffer.height() as i32), + ); + + assert_eq!( + surface.image_buffer.pixel_format_type(), + media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ); + + let y_texture = unsafe { + self.core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + MTLPixelFormat::R8Unorm, + surface.image_buffer.plane_width(0), + surface.image_buffer.plane_height(0), + 0, + ) + .unwrap() + }; + let cb_cr_texture = unsafe { + self.core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + MTLPixelFormat::RG8Unorm, + surface.image_buffer.plane_width(1), + surface.image_buffer.plane_height(1), + 1, + ) + .unwrap() + }; + + align_offset(instance_offset); + let next_offset = *instance_offset + mem::size_of::(); + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Surfaces as u64, + Some(instance_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::TextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_texture( + SurfaceInputIndex::YTexture as u64, + Some(y_texture.as_texture_ref()), + ); + command_encoder.set_fragment_texture( + SurfaceInputIndex::CbCrTexture as u64, + Some(cb_cr_texture.as_texture_ref()), + ); + + unsafe { + let buffer_contents = (instance_buffer.contents() as *mut u8).add(*instance_offset) + as *mut SurfaceBounds; + ptr::write( + buffer_contents, + SurfaceBounds { + bounds: surface.bounds, + content_mask: surface.content_mask.clone(), + }, + ); + } + + command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); + *instance_offset = next_offset; + } + true + } +} + +fn build_pipeline_state( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + label: &str, + vertex_fn_name: &str, + fragment_fn_name: &str, + pixel_format: metal::MTLPixelFormat, +) -> metal::RenderPipelineState { + let vertex_fn = library + .get_function(vertex_fn_name, None) + .expect("error locating vertex function"); + let fragment_fn = library + .get_function(fragment_fn_name, None) + .expect("error locating fragment function"); + + let descriptor = metal::RenderPipelineDescriptor::new(); + descriptor.set_label(label); + descriptor.set_vertex_function(Some(vertex_fn.as_ref())); + descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); + color_attachment.set_pixel_format(pixel_format); + color_attachment.set_blending_enabled(true); + color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); + + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") +} + +fn build_path_rasterization_pipeline_state( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + label: &str, + vertex_fn_name: &str, + fragment_fn_name: &str, + pixel_format: metal::MTLPixelFormat, +) -> metal::RenderPipelineState { + let vertex_fn = library + .get_function(vertex_fn_name, None) + .expect("error locating vertex function"); + let fragment_fn = library + .get_function(fragment_fn_name, None) + .expect("error locating fragment function"); + + let descriptor = metal::RenderPipelineDescriptor::new(); + descriptor.set_label(label); + descriptor.set_vertex_function(Some(vertex_fn.as_ref())); + descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); + color_attachment.set_pixel_format(pixel_format); + color_attachment.set_blending_enabled(true); + color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); + + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") +} + +// Align to multiples of 256 make Metal happy. +fn align_offset(offset: &mut usize) { + *offset = ((*offset + 255) / 256) * 256; +} + +#[repr(C)] +enum ShadowInputIndex { + Vertices = 0, + Shadows = 1, + ViewportSize = 2, +} + +#[repr(C)] +enum QuadInputIndex { + Vertices = 0, + Quads = 1, + ViewportSize = 2, +} + +#[repr(C)] +enum UnderlineInputIndex { + Vertices = 0, + Underlines = 1, + ViewportSize = 2, +} + +#[repr(C)] +enum SpriteInputIndex { + Vertices = 0, + Sprites = 1, + ViewportSize = 2, + AtlasTextureSize = 3, + AtlasTexture = 4, +} + +#[repr(C)] +enum SurfaceInputIndex { + Vertices = 0, + Surfaces = 1, + ViewportSize = 2, + TextureSize = 3, + YTexture = 4, + CbCrTexture = 5, +} + +#[repr(C)] +enum PathRasterizationInputIndex { + Vertices = 0, + AtlasTextureSize = 1, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct PathSprite { + pub bounds: Bounds, + pub color: Hsla, + pub tile: AtlasTile, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct SurfaceBounds { + pub bounds: Bounds, + pub content_mask: ContentMask, +} diff --git a/crates/ming/src/platform/mac/open_type.rs b/crates/ming/src/platform/mac/open_type.rs new file mode 100644 index 0000000..d465e8f --- /dev/null +++ b/crates/ming/src/platform/mac/open_type.rs @@ -0,0 +1,394 @@ +#![allow(unused, non_upper_case_globals)] + +use crate::FontFeatures; +use cocoa::appkit::CGFloat; +use core_foundation::{base::TCFType, number::CFNumber}; +use core_graphics::geometry::CGAffineTransform; +use core_text::{ + font::{CTFont, CTFontRef}, + font_descriptor::{ + CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef, + }, +}; +use font_kit::font::Font; +use std::ptr; + +const kCaseSensitiveLayoutOffSelector: i32 = 1; +const kCaseSensitiveLayoutOnSelector: i32 = 0; +const kCaseSensitiveLayoutType: i32 = 33; +const kCaseSensitiveSpacingOffSelector: i32 = 3; +const kCaseSensitiveSpacingOnSelector: i32 = 2; +const kCharacterAlternativesType: i32 = 17; +const kCommonLigaturesOffSelector: i32 = 3; +const kCommonLigaturesOnSelector: i32 = 2; +const kContextualAlternatesOffSelector: i32 = 1; +const kContextualAlternatesOnSelector: i32 = 0; +const kContextualAlternatesType: i32 = 36; +const kContextualLigaturesOffSelector: i32 = 19; +const kContextualLigaturesOnSelector: i32 = 18; +const kContextualSwashAlternatesOffSelector: i32 = 5; +const kContextualSwashAlternatesOnSelector: i32 = 4; +const kDefaultLowerCaseSelector: i32 = 0; +const kDefaultUpperCaseSelector: i32 = 0; +const kDiagonalFractionsSelector: i32 = 2; +const kFractionsType: i32 = 11; +const kHistoricalLigaturesOffSelector: i32 = 21; +const kHistoricalLigaturesOnSelector: i32 = 20; +const kHojoCharactersSelector: i32 = 12; +const kInferiorsSelector: i32 = 2; +const kJIS2004CharactersSelector: i32 = 11; +const kLigaturesType: i32 = 1; +const kLowerCasePetiteCapsSelector: i32 = 2; +const kLowerCaseSmallCapsSelector: i32 = 1; +const kLowerCaseType: i32 = 37; +const kLowerCaseNumbersSelector: i32 = 0; +const kMathematicalGreekOffSelector: i32 = 11; +const kMathematicalGreekOnSelector: i32 = 10; +const kMonospacedNumbersSelector: i32 = 0; +const kNLCCharactersSelector: i32 = 13; +const kNoFractionsSelector: i32 = 0; +const kNormalPositionSelector: i32 = 0; +const kNoStyleOptionsSelector: i32 = 0; +const kNumberCaseType: i32 = 21; +const kNumberSpacingType: i32 = 6; +const kOrdinalsSelector: i32 = 3; +const kProportionalNumbersSelector: i32 = 1; +const kQuarterWidthTextSelector: i32 = 4; +const kScientificInferiorsSelector: i32 = 4; +const kSlashedZeroOffSelector: i32 = 5; +const kSlashedZeroOnSelector: i32 = 4; +const kStyleOptionsType: i32 = 19; +const kStylisticAltEighteenOffSelector: i32 = 37; +const kStylisticAltEighteenOnSelector: i32 = 36; +const kStylisticAltEightOffSelector: i32 = 17; +const kStylisticAltEightOnSelector: i32 = 16; +const kStylisticAltElevenOffSelector: i32 = 23; +const kStylisticAltElevenOnSelector: i32 = 22; +const kStylisticAlternativesType: i32 = 35; +const kStylisticAltFifteenOffSelector: i32 = 31; +const kStylisticAltFifteenOnSelector: i32 = 30; +const kStylisticAltFiveOffSelector: i32 = 11; +const kStylisticAltFiveOnSelector: i32 = 10; +const kStylisticAltFourOffSelector: i32 = 9; +const kStylisticAltFourOnSelector: i32 = 8; +const kStylisticAltFourteenOffSelector: i32 = 29; +const kStylisticAltFourteenOnSelector: i32 = 28; +const kStylisticAltNineOffSelector: i32 = 19; +const kStylisticAltNineOnSelector: i32 = 18; +const kStylisticAltNineteenOffSelector: i32 = 39; +const kStylisticAltNineteenOnSelector: i32 = 38; +const kStylisticAltOneOffSelector: i32 = 3; +const kStylisticAltOneOnSelector: i32 = 2; +const kStylisticAltSevenOffSelector: i32 = 15; +const kStylisticAltSevenOnSelector: i32 = 14; +const kStylisticAltSeventeenOffSelector: i32 = 35; +const kStylisticAltSeventeenOnSelector: i32 = 34; +const kStylisticAltSixOffSelector: i32 = 13; +const kStylisticAltSixOnSelector: i32 = 12; +const kStylisticAltSixteenOffSelector: i32 = 33; +const kStylisticAltSixteenOnSelector: i32 = 32; +const kStylisticAltTenOffSelector: i32 = 21; +const kStylisticAltTenOnSelector: i32 = 20; +const kStylisticAltThirteenOffSelector: i32 = 27; +const kStylisticAltThirteenOnSelector: i32 = 26; +const kStylisticAltThreeOffSelector: i32 = 7; +const kStylisticAltThreeOnSelector: i32 = 6; +const kStylisticAltTwelveOffSelector: i32 = 25; +const kStylisticAltTwelveOnSelector: i32 = 24; +const kStylisticAltTwentyOffSelector: i32 = 41; +const kStylisticAltTwentyOnSelector: i32 = 40; +const kStylisticAltTwoOffSelector: i32 = 5; +const kStylisticAltTwoOnSelector: i32 = 4; +const kSuperiorsSelector: i32 = 1; +const kSwashAlternatesOffSelector: i32 = 3; +const kSwashAlternatesOnSelector: i32 = 2; +const kTitlingCapsSelector: i32 = 4; +const kTypographicExtrasType: i32 = 14; +const kVerticalFractionsSelector: i32 = 1; +const kVerticalPositionType: i32 = 10; + +pub fn apply_features(font: &mut Font, features: &FontFeatures) { + // See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc + // for a reference implementation. + toggle_open_type_feature( + font, + features.calt(), + kContextualAlternatesType, + kContextualAlternatesOnSelector, + kContextualAlternatesOffSelector, + ); + toggle_open_type_feature( + font, + features.case(), + kCaseSensitiveLayoutType, + kCaseSensitiveLayoutOnSelector, + kCaseSensitiveLayoutOffSelector, + ); + toggle_open_type_feature( + font, + features.cpsp(), + kCaseSensitiveLayoutType, + kCaseSensitiveSpacingOnSelector, + kCaseSensitiveSpacingOffSelector, + ); + toggle_open_type_feature( + font, + features.frac(), + kFractionsType, + kDiagonalFractionsSelector, + kNoFractionsSelector, + ); + toggle_open_type_feature( + font, + features.liga(), + kLigaturesType, + kCommonLigaturesOnSelector, + kCommonLigaturesOffSelector, + ); + toggle_open_type_feature( + font, + features.onum(), + kNumberCaseType, + kLowerCaseNumbersSelector, + 2, + ); + toggle_open_type_feature( + font, + features.ordn(), + kVerticalPositionType, + kOrdinalsSelector, + kNormalPositionSelector, + ); + toggle_open_type_feature( + font, + features.pnum(), + kNumberSpacingType, + kProportionalNumbersSelector, + 4, + ); + toggle_open_type_feature( + font, + features.ss01(), + kStylisticAlternativesType, + kStylisticAltOneOnSelector, + kStylisticAltOneOffSelector, + ); + toggle_open_type_feature( + font, + features.ss02(), + kStylisticAlternativesType, + kStylisticAltTwoOnSelector, + kStylisticAltTwoOffSelector, + ); + toggle_open_type_feature( + font, + features.ss03(), + kStylisticAlternativesType, + kStylisticAltThreeOnSelector, + kStylisticAltThreeOffSelector, + ); + toggle_open_type_feature( + font, + features.ss04(), + kStylisticAlternativesType, + kStylisticAltFourOnSelector, + kStylisticAltFourOffSelector, + ); + toggle_open_type_feature( + font, + features.ss05(), + kStylisticAlternativesType, + kStylisticAltFiveOnSelector, + kStylisticAltFiveOffSelector, + ); + toggle_open_type_feature( + font, + features.ss06(), + kStylisticAlternativesType, + kStylisticAltSixOnSelector, + kStylisticAltSixOffSelector, + ); + toggle_open_type_feature( + font, + features.ss07(), + kStylisticAlternativesType, + kStylisticAltSevenOnSelector, + kStylisticAltSevenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss08(), + kStylisticAlternativesType, + kStylisticAltEightOnSelector, + kStylisticAltEightOffSelector, + ); + toggle_open_type_feature( + font, + features.ss09(), + kStylisticAlternativesType, + kStylisticAltNineOnSelector, + kStylisticAltNineOffSelector, + ); + toggle_open_type_feature( + font, + features.ss10(), + kStylisticAlternativesType, + kStylisticAltTenOnSelector, + kStylisticAltTenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss11(), + kStylisticAlternativesType, + kStylisticAltElevenOnSelector, + kStylisticAltElevenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss12(), + kStylisticAlternativesType, + kStylisticAltTwelveOnSelector, + kStylisticAltTwelveOffSelector, + ); + toggle_open_type_feature( + font, + features.ss13(), + kStylisticAlternativesType, + kStylisticAltThirteenOnSelector, + kStylisticAltThirteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss14(), + kStylisticAlternativesType, + kStylisticAltFourteenOnSelector, + kStylisticAltFourteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss15(), + kStylisticAlternativesType, + kStylisticAltFifteenOnSelector, + kStylisticAltFifteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss16(), + kStylisticAlternativesType, + kStylisticAltSixteenOnSelector, + kStylisticAltSixteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss17(), + kStylisticAlternativesType, + kStylisticAltSeventeenOnSelector, + kStylisticAltSeventeenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss18(), + kStylisticAlternativesType, + kStylisticAltEighteenOnSelector, + kStylisticAltEighteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss19(), + kStylisticAlternativesType, + kStylisticAltNineteenOnSelector, + kStylisticAltNineteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss20(), + kStylisticAlternativesType, + kStylisticAltTwentyOnSelector, + kStylisticAltTwentyOffSelector, + ); + toggle_open_type_feature( + font, + features.subs(), + kVerticalPositionType, + kInferiorsSelector, + kNormalPositionSelector, + ); + toggle_open_type_feature( + font, + features.sups(), + kVerticalPositionType, + kSuperiorsSelector, + kNormalPositionSelector, + ); + toggle_open_type_feature( + font, + features.swsh(), + kContextualAlternatesType, + kSwashAlternatesOnSelector, + kSwashAlternatesOffSelector, + ); + toggle_open_type_feature( + font, + features.titl(), + kStyleOptionsType, + kTitlingCapsSelector, + kNoStyleOptionsSelector, + ); + toggle_open_type_feature( + font, + features.tnum(), + kNumberSpacingType, + kMonospacedNumbersSelector, + 4, + ); + toggle_open_type_feature( + font, + features.zero(), + kTypographicExtrasType, + kSlashedZeroOnSelector, + kSlashedZeroOffSelector, + ); +} + +fn toggle_open_type_feature( + font: &mut Font, + enabled: Option, + type_identifier: i32, + on_selector_identifier: i32, + off_selector_identifier: i32, +) { + if let Some(enabled) = enabled { + let native_font = font.native_font(); + unsafe { + let selector_identifier = if enabled { + on_selector_identifier + } else { + off_selector_identifier + }; + let new_descriptor = CTFontDescriptorCreateCopyWithFeature( + native_font.copy_descriptor().as_concrete_TypeRef(), + CFNumber::from(type_identifier).as_concrete_TypeRef(), + CFNumber::from(selector_identifier).as_concrete_TypeRef(), + ); + let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor); + let new_font = CTFontCreateCopyWithAttributes( + font.native_font().as_concrete_TypeRef(), + 0.0, + ptr::null(), + new_descriptor.as_concrete_TypeRef(), + ); + let new_font = CTFont::wrap_under_create_rule(new_font); + *font = Font::from_native_font(&new_font); + } + } +} + +#[link(name = "CoreText", kind = "framework")] +extern "C" { + fn CTFontCreateCopyWithAttributes( + font: CTFontRef, + size: CGFloat, + matrix: *const CGAffineTransform, + attributes: CTFontDescriptorRef, + ) -> CTFontRef; +} diff --git a/crates/ming/src/platform/mac/platform.rs b/crates/ming/src/platform/mac/platform.rs new file mode 100644 index 0000000..f775468 --- /dev/null +++ b/crates/ming/src/platform/mac/platform.rs @@ -0,0 +1,1223 @@ +use super::{events::key_to_native, BoolExt}; +use crate::{ + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, + Keymap, MacDispatcher, MacDisplay, MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, + Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, + WindowAppearance, WindowParams, +}; +use anyhow::{anyhow, bail}; +use block::ConcreteBlock; +use cocoa::{ + appkit::{ + NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, + NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, + NSPasteboardTypeString, NSSavePanel, NSWindow, + }, + base::{id, nil, selector, BOOL, YES}, + foundation::{ + NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString, + NSUInteger, NSURL, + }, +}; +use core_foundation::{ + base::{CFType, CFTypeRef, OSStatus, TCFType as _}, + boolean::CFBoolean, + data::CFData, + dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary}, + string::{CFString, CFStringRef}, +}; +use ctor::ctor; +use futures::channel::oneshot; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use parking_lot::Mutex; +use ptr::null_mut; +use std::{ + cell::Cell, + convert::TryInto, + ffi::{c_void, CStr, OsStr}, + os::{raw::c_char, unix::ffi::OsStrExt}, + path::{Path, PathBuf}, + process::Command, + ptr, + rc::Rc, + slice, str, + sync::Arc, +}; +use time::UtcOffset; + +use super::renderer; + +#[allow(non_upper_case_globals)] +const NSUTF8StringEncoding: NSUInteger = 4; + +const MAC_PLATFORM_IVAR: &str = "platform"; +static mut APP_CLASS: *const Class = ptr::null(); +static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_classes() { + APP_CLASS = { + let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.register() + }; + + APP_DELEGATE_CLASS = { + let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationShouldHandleReopen:hasVisibleWindows:), + should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool), + ); + decl.add_method( + sel!(applicationWillTerminate:), + will_terminate as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(handleGPUIMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + // Add menu item handlers so that OS save panels have the correct key commands + decl.add_method( + sel!(cut:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(copy:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(paste:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(selectAll:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(undo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(redo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(validateMenuItem:), + validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool, + ); + decl.add_method( + sel!(menuWillOpen:), + menu_will_open as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(application:openURLs:), + open_urls as extern "C" fn(&mut Object, Sel, id, id), + ); + + decl.register() + } +} + +pub(crate) struct MacPlatform(Mutex); + +pub(crate) struct MacPlatformState { + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + renderer_context: renderer::Context, + pasteboard: id, + text_hash_pasteboard_type: id, + metadata_pasteboard_type: id, + reopen: Option>, + quit: Option>, + menu_command: Option>, + validate_menu_command: Option bool>>, + will_open_menu: Option>, + menu_actions: Vec>, + open_urls: Option)>>, + finish_launching: Option>, +} + +impl Default for MacPlatform { + fn default() -> Self { + Self::new() + } +} + +impl MacPlatform { + pub(crate) fn new() -> Self { + let dispatcher = Arc::new(MacDispatcher::new()); + Self(Mutex::new(MacPlatformState { + background_executor: BackgroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher), + text_system: Arc::new(MacTextSystem::new()), + renderer_context: renderer::Context::default(), + pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) }, + text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") }, + metadata_pasteboard_type: unsafe { ns_string("zed-metadata") }, + reopen: None, + quit: None, + menu_command: None, + validate_menu_command: None, + will_open_menu: None, + menu_actions: Default::default(), + open_urls: None, + finish_launching: None, + })) + } + + unsafe fn read_from_pasteboard(&self, pasteboard: *mut Object, kind: id) -> Option<&[u8]> { + let data = pasteboard.dataForType(kind); + if data == nil { + None + } else { + Some(slice::from_raw_parts( + data.bytes() as *mut u8, + data.length() as usize, + )) + } + } + + unsafe fn create_menu_bar( + &self, + menus: Vec, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + let application_menu = NSMenu::new(nil).autorelease(); + application_menu.setDelegate_(delegate); + + for menu_config in menus { + let menu = NSMenu::new(nil).autorelease(); + menu.setTitle_(ns_string(menu_config.name)); + menu.setDelegate_(delegate); + + for item_config in menu_config.items { + menu.addItem_(Self::create_menu_item( + item_config, + delegate, + actions, + keymap, + )); + } + + let menu_item = NSMenuItem::new(nil).autorelease(); + menu_item.setSubmenu_(menu); + application_menu.addItem_(menu_item); + + if menu_config.name == "Window" { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setWindowsMenu_(menu); + } + } + + application_menu + } + + unsafe fn create_menu_item( + item: MenuItem, + delegate: id, + actions: &mut Vec>, + keymap: &Keymap, + ) -> id { + match item { + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::Action { + name, + action, + os_action, + } => { + let keystrokes = keymap + .bindings_for_action(action.as_ref()) + .next() + .map(|binding| binding.keystrokes()); + + let selector = match os_action { + Some(crate::OsAction::Cut) => selector("cut:"), + Some(crate::OsAction::Copy) => selector("copy:"), + Some(crate::OsAction::Paste) => selector("paste:"), + Some(crate::OsAction::SelectAll) => selector("selectAll:"), + Some(crate::OsAction::Undo) => selector("undo:"), + Some(crate::OsAction::Redo) => selector("redo:"), + None => selector("handleGPUIMenuItem:"), + }; + + let item; + if let Some(keystrokes) = keystrokes { + if keystrokes.len() == 1 { + let keystroke = &keystrokes[0]; + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + ( + keystroke.modifiers.platform, + NSEventModifierFlags::NSCommandKeyMask, + ), + ( + keystroke.modifiers.control, + NSEventModifierFlags::NSControlKeyMask, + ), + ( + keystroke.modifiers.alt, + NSEventModifierFlags::NSAlternateKeyMask, + ), + ( + keystroke.modifiers.shift, + NSEventModifierFlags::NSShiftKeyMask, + ), + ] { + if *modifier { + mask |= *flag; + } + } + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(key_to_native(&keystroke.key).as_ref()), + ) + .autorelease(); + item.setKeyEquivalentModifierMask_(mask); + } + // For multi-keystroke bindings, render the keystroke as part of the title. + else { + use std::fmt::Write; + + let mut name = format!("{name} ["); + for (i, keystroke) in keystrokes.iter().enumerate() { + if i > 0 { + name.push(' '); + } + write!(&mut name, "{}", keystroke).unwrap(); + } + name.push(']'); + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(&name), + selector, + ns_string(""), + ) + .autorelease(); + } + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(""), + ) + .autorelease(); + } + + let tag = actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + actions.push(action); + item + } + MenuItem::Submenu(Menu { name, items }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + for item in items { + submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap)); + } + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); + item + } + } + } +} + +impl Platform for MacPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.0.lock().background_executor.clone() + } + + fn foreground_executor(&self) -> crate::ForegroundExecutor { + self.0.lock().foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.0.lock().text_system.clone() + } + + fn run(&self, on_finish_launching: Box) { + self.0.lock().finish_launching = Some(on_finish_launching); + + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; + app.setDelegate_(app_delegate); + + let self_ptr = self as *const Self as *const c_void; + (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + + let pool = NSAutoreleasePool::new(nil); + app.run(); + pool.drain(); + + (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + } + } + + fn quit(&self) { + // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks + // synchronously before this method terminates. If we call `Platform::quit` while holding a + // borrow of the app state (which most of the time we will do), we will end up + // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve + // this, we make quitting the application asynchronous so that we aren't holding borrows to + // the app state on the stack when we actually terminate the app. + + use super::dispatcher::{dispatch_get_main_queue, dispatch_sys::dispatch_async_f}; + + unsafe { + dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit)); + } + + unsafe extern "C" fn quit(_: *mut c_void) { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, terminate: nil]; + } + } + + fn restart(&self, _binary_path: Option) { + use std::os::unix::process::CommandExt as _; + + let app_pid = std::process::id().to_string(); + let app_path = self + .app_path() + .ok() + // When the app is not bundled, `app_path` returns the + // directory containing the executable. Disregard this + // and get the path to the executable itself. + .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path)) + .unwrap_or_else(|| std::env::current_exe().unwrap()); + + // Wait until this process has exited and then re-open this path. + let script = r#" + while kill -0 $0 2> /dev/null; do + sleep 0.1 + done + open "$1" + "#; + + let restart_process = Command::new("/bin/bash") + .arg("-c") + .arg(script) + .arg(app_pid) + .arg(app_path) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, ignoring_other_apps: bool) { + unsafe { + let app = NSApplication::sharedApplication(nil); + app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); + } + } + + fn hide(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hide: nil]; + } + } + + fn hide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hideOtherApplications: nil]; + } + } + + fn unhide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, unhideAllApplications: nil]; + } + } + + fn primary_display(&self) -> Option> { + Some(Rc::new(MacDisplay::primary())) + } + + fn displays(&self) -> Vec> { + MacDisplay::all() + .map(|screen| Rc::new(screen) as Rc<_>) + .collect() + } + + fn active_window(&self) -> Option { + MacWindow::active_window() + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Box { + // Clippy thinks that this evaluates to `()`, for some reason. + #[allow(clippy::unit_arg, clippy::clone_on_copy)] + let renderer_context = self.0.lock().renderer_context.clone(); + Box::new(MacWindow::open( + handle, + options, + self.foreground_executor(), + renderer_context, + )) + } + + fn window_appearance(&self) -> WindowAppearance { + unsafe { + let app = NSApplication::sharedApplication(nil); + let appearance: id = msg_send![app, effectiveAppearance]; + WindowAppearance::from_native(appearance) + } + } + + fn open_url(&self, url: &str) { + unsafe { + let url = NSURL::alloc(nil) + .initWithString_(ns_string(url)) + .autorelease(); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + msg_send![workspace, openURL: url] + } + } + + fn register_url_scheme(&self, scheme: &str) -> Task> { + // API only available post Monterey + // https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl + let (done_tx, done_rx) = oneshot::channel(); + if self.os_version().ok() < Some(SemanticVersion::new(12, 0, 0)) { + return Task::ready(Err(anyhow!( + "macOS 12.0 or later is required to register URL schemes" + ))); + } + + let bundle_id = unsafe { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + let bundle_id: id = msg_send![bundle, bundleIdentifier]; + if bundle_id == nil { + return Task::ready(Err(anyhow!("Can only register URL scheme in bundled apps"))); + } + bundle_id + }; + + unsafe { + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let scheme: id = ns_string(scheme); + let app: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id]; + if app == nil { + return Task::ready(Err(anyhow!( + "Cannot register URL scheme until app is installed" + ))); + } + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |error: id| { + let result = if error == nil { + Ok(()) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!("Failed to register: {:?}", msg)) + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let block = block.copy(); + let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block]; + } + + self.background_executor() + .spawn(async { crate::Flatten::flatten(done_rx.await.map_err(|e| anyhow!(e))) }) + } + + fn on_open_urls(&self, callback: Box)>) { + self.0.lock().open_urls = Some(callback); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + let (done_tx, done_rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + unsafe { + let panel = NSOpenPanel::openPanel(nil); + panel.setCanChooseDirectories_(options.directories.to_objc()); + panel.setCanChooseFiles_(options.files.to_objc()); + panel.setAllowsMultipleSelection_(options.multiple.to_objc()); + panel.setResolvesAliases_(false.to_objc()); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let result = if response == NSModalResponse::NSModalResponseOk { + let mut result = Vec::new(); + let urls = panel.URLs(); + for i in 0..urls.count() { + let url = urls.objectAtIndex(i); + if url.isFileURL() == YES { + if let Ok(path) = ns_url_to_path(url) { + result.push(path) + } + } + } + Some(result) + } else { + None + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + } + }) + .detach(); + done_rx + } + + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + let directory = directory.to_owned(); + let (done_tx, done_rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + unsafe { + let panel = NSSavePanel::savePanel(nil); + let path = ns_string(directory.to_string_lossy().as_ref()); + let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); + panel.setDirectoryURL(url); + + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let mut result = None; + if response == NSModalResponse::NSModalResponseOk { + let url = panel.URL(); + if url.isFileURL() == YES { + result = ns_url_to_path(panel.URL()).ok() + } + } + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + } + }) + .detach(); + + done_rx + } + + fn reveal_path(&self, path: &Path) { + unsafe { + let path = path.to_path_buf(); + self.0 + .lock() + .background_executor + .spawn(async move { + let full_path = ns_string(path.to_str().unwrap_or("")); + let root_full_path = ns_string(""); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let _: BOOL = msg_send![ + workspace, + selectFile: full_path + inFileViewerRootedAtPath: root_full_path + ]; + }) + .detach(); + } + } + + fn on_quit(&self, callback: Box) { + self.0.lock().quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.0.lock().reopen = Some(callback); + } + + fn on_app_menu_action(&self, callback: Box) { + self.0.lock().menu_command = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.0.lock().will_open_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.0.lock().validate_menu_command = Some(callback); + } + + fn os_name(&self) -> &'static str { + "macOS" + } + + fn os_version(&self) -> Result { + unsafe { + let process_info = NSProcessInfo::processInfo(nil); + let version = process_info.operatingSystemVersion(); + Ok(SemanticVersion::new( + version.majorVersion as usize, + version.minorVersion as usize, + version.patchVersion as usize, + )) + } + } + + fn app_version(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")]; + if version.is_null() { + bail!("bundle does not have version"); + } + let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = version.UTF8String() as *const u8; + let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); + version.parse() + } + } + } + + fn app_path(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + Ok(path_from_objc(msg_send![bundle, bundlePath])) + } + } + } + + fn set_menus(&self, menus: Vec, keymap: &Keymap) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let mut state = self.0.lock(); + let actions = &mut state.menu_actions; + app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, keymap)); + } + } + + fn add_recent_document(&self, path: &Path) { + if let Some(path_str) = path.to_str() { + unsafe { + let document_controller: id = + msg_send![class!(NSDocumentController), sharedDocumentController]; + let url: id = NSURL::fileURLWithPath_(nil, ns_string(path_str)); + let _: () = msg_send![document_controller, noteNewRecentDocumentURL:url]; + } + } + } + + fn local_timezone(&self) -> UtcOffset { + unsafe { + let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; + let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT]; + UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap() + } + } + + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + let name = ns_string(name); + let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; + if url.is_null() { + Err(anyhow!("resource not found")) + } else { + ns_url_to_path(url) + } + } + } + } + + /// Match cursor style to one of the styles available + /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor). + fn set_cursor_style(&self, style: CursorStyle) { + unsafe { + let new_cursor: id = match style { + CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], + CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], + CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor], + CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor], + CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor], + CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], + CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor], + CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor], + CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor], + CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor], + CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], + CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor], + CursorStyle::DisappearingItem => { + msg_send![class!(NSCursor), disappearingItemCursor] + } + CursorStyle::IBeamCursorForVerticalLayout => { + msg_send![class!(NSCursor), IBeamCursorForVerticalLayout] + } + CursorStyle::OperationNotAllowed => { + msg_send![class!(NSCursor), operationNotAllowedCursor] + } + CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor], + CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor], + CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor], + }; + + let old_cursor: id = msg_send![class!(NSCursor), currentCursor]; + if new_cursor != old_cursor { + let _: () = msg_send![new_cursor, set]; + } + } + } + + fn should_auto_hide_scrollbars(&self) -> bool { + #[allow(non_upper_case_globals)] + const NSScrollerStyleOverlay: NSInteger = 1; + + unsafe { + let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; + style == NSScrollerStyleOverlay + } + } + + fn write_to_primary(&self, _item: ClipboardItem) {} + + fn write_to_clipboard(&self, item: ClipboardItem) { + let state = self.0.lock(); + unsafe { + state.pasteboard.clearContents(); + + let text_bytes = NSData::dataWithBytes_length_( + nil, + item.text.as_ptr() as *const c_void, + item.text.len() as u64, + ); + state + .pasteboard + .setData_forType(text_bytes, NSPasteboardTypeString); + + if let Some(metadata) = item.metadata.as_ref() { + let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes(); + let hash_bytes = NSData::dataWithBytes_length_( + nil, + hash_bytes.as_ptr() as *const c_void, + hash_bytes.len() as u64, + ); + state + .pasteboard + .setData_forType(hash_bytes, state.text_hash_pasteboard_type); + + let metadata_bytes = NSData::dataWithBytes_length_( + nil, + metadata.as_ptr() as *const c_void, + metadata.len() as u64, + ); + state + .pasteboard + .setData_forType(metadata_bytes, state.metadata_pasteboard_type); + } + } + } + + fn read_from_primary(&self) -> Option { + None + } + + fn read_from_clipboard(&self) -> Option { + let state = self.0.lock(); + unsafe { + if let Some(text_bytes) = + self.read_from_pasteboard(state.pasteboard, NSPasteboardTypeString) + { + let text = String::from_utf8_lossy(text_bytes).to_string(); + let hash_bytes = self + .read_from_pasteboard(state.pasteboard, state.text_hash_pasteboard_type) + .and_then(|bytes| bytes.try_into().ok()) + .map(u64::from_be_bytes); + let metadata_bytes = self + .read_from_pasteboard(state.pasteboard, state.metadata_pasteboard_type) + .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok()); + + if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) { + if hash == ClipboardItem::text_hash(&text) { + Some(ClipboardItem { + text, + metadata: Some(metadata), + }) + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } + } else { + None + } + } + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + self.background_executor().spawn(async move { + unsafe { + use security::*; + + let url = CFString::from(url.as_str()); + let username = CFString::from(username.as_str()); + let password = CFData::from_buffer(&password); + + // First, check if there are already credentials for the given server. If so, then + // update the username and password. + let mut verb = "updating"; + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let mut attrs = CFMutableDictionary::with_capacity(4); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef()); + attrs.set(kSecValueData as *const _, password.as_CFTypeRef()); + + let mut status = SecItemUpdate( + query_attrs.as_concrete_TypeRef(), + attrs.as_concrete_TypeRef(), + ); + + // If there were no existing credentials for the given server, then create them. + if status == errSecItemNotFound { + verb = "creating"; + status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut()); + } + + if status != errSecSuccess { + return Err(anyhow!("{} password failed: {}", verb, status)); + } + } + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let url = CFString::from(url.as_str()); + let cf_true = CFBoolean::true_value().as_CFTypeRef(); + + unsafe { + use security::*; + + // Find any credentials for the given server URL. + let mut attrs = CFMutableDictionary::with_capacity(5); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecReturnAttributes as *const _, cf_true); + attrs.set(kSecReturnData as *const _, cf_true); + + let mut result = CFTypeRef::from(ptr::null()); + let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); + match status { + security::errSecSuccess => {} + security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), + _ => return Err(anyhow!("reading password failed: {}", status)), + } + + let result = CFType::wrap_under_create_rule(result) + .downcast::() + .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?; + let username = result + .find(kSecAttrAccount as *const _) + .ok_or_else(|| anyhow!("account was missing from keychain item"))?; + let username = CFType::wrap_under_get_rule(*username) + .downcast::() + .ok_or_else(|| anyhow!("account was not a string"))?; + let password = result + .find(kSecValueData as *const _) + .ok_or_else(|| anyhow!("password was missing from keychain item"))?; + let password = CFType::wrap_under_get_rule(*password) + .downcast::() + .ok_or_else(|| anyhow!("password was not a string"))?; + + Ok(Some((username.to_string(), password.bytes().to_vec()))) + } + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let url = url.to_string(); + + self.background_executor().spawn(async move { + unsafe { + use security::*; + + let url = CFString::from(url.as_str()); + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); + + if status != errSecSuccess { + return Err(anyhow!("delete password failed: {}", status)); + } + } + Ok(()) + }) + } +} + +unsafe fn path_from_objc(path: id) -> PathBuf { + let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = path.UTF8String() as *const u8; + let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); + PathBuf::from(path) +} + +unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform { + let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); + assert!(!platform_ptr.is_null()); + &*(platform_ptr as *const MacPlatform) +} + +extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setActivationPolicy_(NSApplicationActivationPolicyRegular); + let platform = get_mac_platform(this); + let callback = platform.0.lock().finish_launching.take(); + if let Some(callback) = callback { + callback(); + } + } +} + +extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { + if !has_open_windows { + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.reopen.take() { + drop(lock); + callback(); + platform.0.lock().reopen.get_or_insert(callback); + } + } +} + +extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.quit.take() { + drop(lock); + callback(); + platform.0.lock().quit.get_or_insert(callback); + } +} + +extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { + let urls = unsafe { + (0..urls.count()) + .filter_map(|i| { + let url = urls.objectAtIndex(i); + match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { + Ok(string) => Some(string.to_string()), + Err(err) => { + log::error!("error converting path to string: {}", err); + None + } + } + }) + .collect::>() + }; + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.open_urls.take() { + drop(lock); + callback(urls); + platform.0.lock().open_urls.get_or_insert(callback); + } +} + +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); + callback(&*action); + } + platform.0.lock().menu_command.get_or_insert(callback); + } + } +} + +extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { + unsafe { + let mut result = false; + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.validate_menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = lock.menu_actions.get(index) { + let action = action.boxed_clone(); + drop(lock); + result = callback(action.as_ref()); + } + platform + .0 + .lock() + .validate_menu_command + .get_or_insert(callback); + } + result + } +} + +extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { + unsafe { + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.will_open_menu.take() { + drop(lock); + callback(); + platform.0.lock().will_open_menu.get_or_insert(callback); + } + } +} + +unsafe fn ns_string(string: &str) -> id { + NSString::alloc(nil).init_str(string).autorelease() +} + +unsafe fn ns_url_to_path(url: id) -> Result { + let path: *mut c_char = msg_send![url, fileSystemRepresentation]; + if path.is_null() { + Err(anyhow!( + "url is not a file path: {}", + CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() + )) + } else { + Ok(PathBuf::from(OsStr::from_bytes( + CStr::from_ptr(path).to_bytes(), + ))) + } +} + +mod security { + #![allow(non_upper_case_globals)] + use super::*; + + #[link(name = "Security", kind = "framework")] + extern "C" { + pub static kSecClass: CFStringRef; + pub static kSecClassInternetPassword: CFStringRef; + pub static kSecAttrServer: CFStringRef; + pub static kSecAttrAccount: CFStringRef; + pub static kSecValueData: CFStringRef; + pub static kSecReturnAttributes: CFStringRef; + pub static kSecReturnData: CFStringRef; + + pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; + pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; + pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + } + + pub const errSecSuccess: OSStatus = 0; + pub const errSecUserCanceled: OSStatus = -128; + pub const errSecItemNotFound: OSStatus = -25300; +} + +#[cfg(test)] +mod tests { + use crate::ClipboardItem; + + use super::*; + + #[test] + fn test_clipboard() { + let platform = build_platform(); + assert_eq!(platform.read_from_clipboard(), None); + + let item = ClipboardItem::new("1".to_string()); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let text_from_other_app = "text from other app"; + unsafe { + let bytes = NSData::dataWithBytes_length_( + nil, + text_from_other_app.as_ptr() as *const c_void, + text_from_other_app.len() as u64, + ); + platform + .0 + .lock() + .pasteboard + .setData_forType(bytes, NSPasteboardTypeString); + } + assert_eq!( + platform.read_from_clipboard(), + Some(ClipboardItem::new(text_from_other_app.to_string())) + ); + } + + fn build_platform() -> MacPlatform { + let platform = MacPlatform::new(); + platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) }; + platform + } +} diff --git a/crates/ming/src/platform/mac/shaders.metal b/crates/ming/src/platform/mac/shaders.metal new file mode 100644 index 0000000..c1089db --- /dev/null +++ b/crates/ming/src/platform/mac/shaders.metal @@ -0,0 +1,693 @@ +#include +#include + +using namespace metal; + +float4 hsla_to_rgba(Hsla hsla); +float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, + constant Size_DevicePixels *viewport_size); +float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + TransformationMatrix transformation, + constant Size_DevicePixels *input_viewport_size); + +float2 to_tile_position(float2 unit_vertex, AtlasTile tile, + constant Size_DevicePixels *atlas_size); +float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds); +float quad_sdf(float2 point, Bounds_ScaledPixels bounds, + Corners_ScaledPixels corner_radii); +float gaussian(float x, float sigma); +float2 erf(float2 x); +float blur_along_x(float x, float y, float sigma, float corner, + float2 half_size); +float4 over(float4 below, float4 above); + +struct QuadVertexOutput { + float4 position [[position]]; + float4 background_color [[flat]]; + float4 border_color [[flat]]; + uint quad_id [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct QuadFragmentInput { + float4 position [[position]]; + float4 background_color [[flat]]; + float4 border_color [[flat]]; + uint quad_id [[flat]]; +}; + +vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]], + uint quad_id [[instance_id]], + constant float2 *unit_vertices + [[buffer(QuadInputIndex_Vertices)]], + constant Quad *quads + [[buffer(QuadInputIndex_Quads)]], + constant Size_DevicePixels *viewport_size + [[buffer(QuadInputIndex_ViewportSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + Quad quad = quads[quad_id]; + float4 device_position = + to_device_position(unit_vertex, quad.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, + quad.content_mask.bounds); + float4 background_color = hsla_to_rgba(quad.background); + float4 border_color = hsla_to_rgba(quad.border_color); + return QuadVertexOutput{ + device_position, + background_color, + border_color, + quad_id, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], + constant Quad *quads + [[buffer(QuadInputIndex_Quads)]]) { + Quad quad = quads[input.quad_id]; + + // Fast path when the quad is not rounded and doesn't have any border. + if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. && + quad.corner_radii.top_right == 0. && + quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. && + quad.border_widths.left == 0. && quad.border_widths.right == 0. && + quad.border_widths.bottom == 0.) { + return input.background_color; + } + + float2 half_size = + float2(quad.bounds.size.width, quad.bounds.size.height) / 2.; + float2 center = + float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size; + float2 center_to_point = input.position.xy - center; + float corner_radius; + if (center_to_point.x < 0.) { + if (center_to_point.y < 0.) { + corner_radius = quad.corner_radii.top_left; + } else { + corner_radius = quad.corner_radii.bottom_left; + } + } else { + if (center_to_point.y < 0.) { + corner_radius = quad.corner_radii.top_right; + } else { + corner_radius = quad.corner_radii.bottom_right; + } + } + + float2 rounded_edge_to_point = + fabs(center_to_point) - half_size + corner_radius; + float distance = + length(max(0., rounded_edge_to_point)) + + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - + corner_radius; + + float vertical_border = center_to_point.x <= 0. ? quad.border_widths.left + : quad.border_widths.right; + float horizontal_border = center_to_point.y <= 0. ? quad.border_widths.top + : quad.border_widths.bottom; + float2 inset_size = + half_size - corner_radius - float2(vertical_border, horizontal_border); + float2 point_to_inset_corner = fabs(center_to_point) - inset_size; + float border_width; + if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) { + border_width = 0.; + } else if (point_to_inset_corner.y > point_to_inset_corner.x) { + border_width = horizontal_border; + } else { + border_width = vertical_border; + } + + float4 color; + if (border_width == 0.) { + color = input.background_color; + } else { + float inset_distance = distance + border_width; + // Blend the border on top of the background and then linearly interpolate + // between the two as we slide inside the background. + float4 blended_border = over(input.background_color, input.border_color); + color = mix(blended_border, input.background_color, + saturate(0.5 - inset_distance)); + } + + return color * float4(1., 1., 1., saturate(0.5 - distance)); +} + +struct ShadowVertexOutput { + float4 position [[position]]; + float4 color [[flat]]; + uint shadow_id [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct ShadowFragmentInput { + float4 position [[position]]; + float4 color [[flat]]; + uint shadow_id [[flat]]; +}; + +vertex ShadowVertexOutput shadow_vertex( + uint unit_vertex_id [[vertex_id]], uint shadow_id [[instance_id]], + constant float2 *unit_vertices [[buffer(ShadowInputIndex_Vertices)]], + constant Shadow *shadows [[buffer(ShadowInputIndex_Shadows)]], + constant Size_DevicePixels *viewport_size + [[buffer(ShadowInputIndex_ViewportSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + Shadow shadow = shadows[shadow_id]; + + float margin = 3. * shadow.blur_radius; + // Set the bounds of the shadow and adjust its size based on the shadow's + // spread radius to achieve the spreading effect + Bounds_ScaledPixels bounds = shadow.bounds; + bounds.origin.x -= margin; + bounds.origin.y -= margin; + bounds.size.width += 2. * margin; + bounds.size.height += 2. * margin; + + float4 device_position = + to_device_position(unit_vertex, bounds, viewport_size); + float4 clip_distance = + distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds); + float4 color = hsla_to_rgba(shadow.color); + + return ShadowVertexOutput{ + device_position, + color, + shadow_id, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], + constant Shadow *shadows + [[buffer(ShadowInputIndex_Shadows)]]) { + Shadow shadow = shadows[input.shadow_id]; + + float2 origin = float2(shadow.bounds.origin.x, shadow.bounds.origin.y); + float2 size = float2(shadow.bounds.size.width, shadow.bounds.size.height); + float2 half_size = size / 2.; + float2 center = origin + half_size; + float2 point = input.position.xy - center; + float corner_radius; + if (point.x < 0.) { + if (point.y < 0.) { + corner_radius = shadow.corner_radii.top_left; + } else { + corner_radius = shadow.corner_radii.bottom_left; + } + } else { + if (point.y < 0.) { + corner_radius = shadow.corner_radii.top_right; + } else { + corner_radius = shadow.corner_radii.bottom_right; + } + } + + // The signal is only non-zero in a limited range, so don't waste samples + float low = point.y - half_size.y; + float high = point.y + half_size.y; + float start = clamp(-3. * shadow.blur_radius, low, high); + float end = clamp(3. * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.; + float y = start + step * 0.5; + float alpha = 0.; + for (int i = 0; i < 4; i++) { + alpha += blur_along_x(point.x, point.y - y, shadow.blur_radius, + corner_radius, half_size) * + gaussian(y, shadow.blur_radius) * step; + y += step; + } + + return input.color * float4(1., 1., 1., alpha); +} + +struct UnderlineVertexOutput { + float4 position [[position]]; + float4 color [[flat]]; + uint underline_id [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct UnderlineFragmentInput { + float4 position [[position]]; + float4 color [[flat]]; + uint underline_id [[flat]]; +}; + +vertex UnderlineVertexOutput underline_vertex( + uint unit_vertex_id [[vertex_id]], uint underline_id [[instance_id]], + constant float2 *unit_vertices [[buffer(UnderlineInputIndex_Vertices)]], + constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]], + constant Size_DevicePixels *viewport_size + [[buffer(ShadowInputIndex_ViewportSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + Underline underline = underlines[underline_id]; + float4 device_position = + to_device_position(unit_vertex, underline.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, + underline.content_mask.bounds); + float4 color = hsla_to_rgba(underline.color); + return UnderlineVertexOutput{ + device_position, + color, + underline_id, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]], + constant Underline *underlines + [[buffer(UnderlineInputIndex_Underlines)]]) { + Underline underline = underlines[input.underline_id]; + if (underline.wavy) { + float half_thickness = underline.thickness * 0.5; + float2 origin = + float2(underline.bounds.origin.x, underline.bounds.origin.y); + float2 st = ((input.position.xy - origin) / underline.bounds.size.height) - + float2(0., 0.5); + float frequency = (M_PI_F * (3. * underline.thickness)) / 8.; + float amplitude = 1. / (2. * underline.thickness); + float sine = sin(st.x * frequency) * amplitude; + float dSine = cos(st.x * frequency) * amplitude * frequency; + float distance = (st.y - sine) / sqrt(1. + dSine * dSine); + float distance_in_pixels = distance * underline.bounds.size.height; + float distance_from_top_border = distance_in_pixels - half_thickness; + float distance_from_bottom_border = distance_in_pixels + half_thickness; + float alpha = saturate( + 0.5 - max(-distance_from_bottom_border, distance_from_top_border)); + return input.color * float4(1., 1., 1., alpha); + } else { + return input.color; + } +} + +struct MonochromeSpriteVertexOutput { + float4 position [[position]]; + float2 tile_position; + float4 color [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct MonochromeSpriteFragmentInput { + float4 position [[position]]; + float2 tile_position; + float4 color [[flat]]; +}; + +vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex( + uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size + [[buffer(SpriteInputIndex_ViewportSize)]], + constant Size_DevicePixels *atlas_size + [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + MonochromeSprite sprite = sprites[sprite_id]; + float4 device_position = + to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, + sprite.content_mask.bounds); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); + float4 color = hsla_to_rgba(sprite.color); + return MonochromeSpriteVertexOutput{ + device_position, + tile_position, + color, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 monochrome_sprite_fragment( + MonochromeSpriteFragmentInput input [[stage_in]], + constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { + constexpr sampler atlas_texture_sampler(mag_filter::linear, + min_filter::linear); + float4 sample = + atlas_texture.sample(atlas_texture_sampler, input.tile_position); + float4 color = input.color; + color.a *= sample.a; + return color; +} + +struct PolychromeSpriteVertexOutput { + float4 position [[position]]; + float2 tile_position; + uint sprite_id [[flat]]; + float clip_distance [[clip_distance]][4]; +}; + +struct PolychromeSpriteFragmentInput { + float4 position [[position]]; + float2 tile_position; + uint sprite_id [[flat]]; +}; + +vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex( + uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size + [[buffer(SpriteInputIndex_ViewportSize)]], + constant Size_DevicePixels *atlas_size + [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { + + float2 unit_vertex = unit_vertices[unit_vertex_id]; + PolychromeSprite sprite = sprites[sprite_id]; + float4 device_position = + to_device_position(unit_vertex, sprite.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, + sprite.content_mask.bounds); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); + return PolychromeSpriteVertexOutput{ + device_position, + tile_position, + sprite_id, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 polychrome_sprite_fragment( + PolychromeSpriteFragmentInput input [[stage_in]], + constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { + PolychromeSprite sprite = sprites[input.sprite_id]; + constexpr sampler atlas_texture_sampler(mag_filter::linear, + min_filter::linear); + float4 sample = + atlas_texture.sample(atlas_texture_sampler, input.tile_position); + float distance = + quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); + + float4 color = sample; + if (sprite.grayscale) { + float grayscale = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; + color.r = grayscale; + color.g = grayscale; + color.b = grayscale; + } + color.a *= saturate(0.5 - distance); + return color; +} + +struct PathRasterizationVertexOutput { + float4 position [[position]]; + float2 st_position; + float clip_rect_distance [[clip_distance]][4]; +}; + +struct PathRasterizationFragmentInput { + float4 position [[position]]; + float2 st_position; +}; + +vertex PathRasterizationVertexOutput path_rasterization_vertex( + uint vertex_id [[vertex_id]], + constant PathVertex_ScaledPixels *vertices + [[buffer(PathRasterizationInputIndex_Vertices)]], + constant Size_DevicePixels *atlas_size + [[buffer(PathRasterizationInputIndex_AtlasTextureSize)]]) { + PathVertex_ScaledPixels v = vertices[vertex_id]; + float2 vertex_position = float2(v.xy_position.x, v.xy_position.y); + float2 viewport_size = float2(atlas_size->width, atlas_size->height); + return PathRasterizationVertexOutput{ + float4(vertex_position / viewport_size * float2(2., -2.) + + float2(-1., 1.), + 0., 1.), + float2(v.st_position.x, v.st_position.y), + {v.xy_position.x - v.content_mask.bounds.origin.x, + v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width - + v.xy_position.x, + v.xy_position.y - v.content_mask.bounds.origin.y, + v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height - + v.xy_position.y}}; +} + +fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input + [[stage_in]]) { + float2 dx = dfdx(input.st_position); + float2 dy = dfdy(input.st_position); + float2 gradient = float2((2. * input.st_position.x) * dx.x - dx.y, + (2. * input.st_position.x) * dy.x - dy.y); + float f = (input.st_position.x * input.st_position.x) - input.st_position.y; + float distance = f / length(gradient); + float alpha = saturate(0.5 - distance); + return float4(alpha, 0., 0., 1.); +} + +struct PathSpriteVertexOutput { + float4 position [[position]]; + float2 tile_position; + float4 color [[flat]]; +}; + +vertex PathSpriteVertexOutput path_sprite_vertex( + uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size + [[buffer(SpriteInputIndex_ViewportSize)]], + constant Size_DevicePixels *atlas_size + [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { + + float2 unit_vertex = unit_vertices[unit_vertex_id]; + PathSprite sprite = sprites[sprite_id]; + // Don't apply content mask because it was already accounted for when + // rasterizing the path. + float4 device_position = + to_device_position(unit_vertex, sprite.bounds, viewport_size); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); + float4 color = hsla_to_rgba(sprite.color); + return PathSpriteVertexOutput{device_position, tile_position, color}; +} + +fragment float4 path_sprite_fragment( + PathSpriteVertexOutput input [[stage_in]], + constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { + constexpr sampler atlas_texture_sampler(mag_filter::linear, + min_filter::linear); + float4 sample = + atlas_texture.sample(atlas_texture_sampler, input.tile_position); + float mask = 1. - abs(1. - fmod(sample.r, 2.)); + float4 color = input.color; + color.a *= mask; + return color; +} + +struct SurfaceVertexOutput { + float4 position [[position]]; + float2 texture_position; + float clip_distance [[clip_distance]][4]; +}; + +struct SurfaceFragmentInput { + float4 position [[position]]; + float2 texture_position; +}; + +vertex SurfaceVertexOutput surface_vertex( + uint unit_vertex_id [[vertex_id]], uint surface_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SurfaceInputIndex_Vertices)]], + constant SurfaceBounds *surfaces [[buffer(SurfaceInputIndex_Surfaces)]], + constant Size_DevicePixels *viewport_size + [[buffer(SurfaceInputIndex_ViewportSize)]], + constant Size_DevicePixels *texture_size + [[buffer(SurfaceInputIndex_TextureSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + SurfaceBounds surface = surfaces[surface_id]; + float4 device_position = + to_device_position(unit_vertex, surface.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds, + surface.content_mask.bounds); + // We are going to copy the whole texture, so the texture position corresponds + // to the current vertex of the unit triangle. + float2 texture_position = unit_vertex; + return SurfaceVertexOutput{ + device_position, + texture_position, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]], + texture2d y_texture + [[texture(SurfaceInputIndex_YTexture)]], + texture2d cb_cr_texture + [[texture(SurfaceInputIndex_CbCrTexture)]]) { + constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear); + const float4x4 ycbcrToRGBTransform = + float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), + float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), + float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), + float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)); + float4 ycbcr = float4( + y_texture.sample(texture_sampler, input.texture_position).r, + cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0); + + return ycbcrToRGBTransform * ycbcr; +} + +float4 hsla_to_rgba(Hsla hsla) { + float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range + float s = hsla.s; + float l = hsla.l; + float a = hsla.a; + + float c = (1.0 - fabs(2.0 * l - 1.0)) * s; + float x = c * (1.0 - fabs(fmod(h, 2.0) - 1.0)); + float m = l - c / 2.0; + + float r = 0.0; + float g = 0.0; + float b = 0.0; + + if (h >= 0.0 && h < 1.0) { + r = c; + g = x; + b = 0.0; + } else if (h >= 1.0 && h < 2.0) { + r = x; + g = c; + b = 0.0; + } else if (h >= 2.0 && h < 3.0) { + r = 0.0; + g = c; + b = x; + } else if (h >= 3.0 && h < 4.0) { + r = 0.0; + g = x; + b = c; + } else if (h >= 4.0 && h < 5.0) { + r = x; + g = 0.0; + b = c; + } else { + r = c; + g = 0.0; + b = x; + } + + float4 rgba; + rgba.x = (r + m); + rgba.y = (g + m); + rgba.z = (b + m); + rgba.w = a; + return rgba; +} + +float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, + constant Size_DevicePixels *input_viewport_size) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + float2 viewport_size = float2((float)input_viewport_size->width, + (float)input_viewport_size->height); + float2 device_position = + position / viewport_size * float2(2., -2.) + float2(-1., 1.); + return float4(device_position, 0., 1.); +} + +float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + TransformationMatrix transformation, + constant Size_DevicePixels *input_viewport_size) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + + // Apply the transformation matrix to the position via matrix multiplication. + float2 transformed_position = float2(0, 0); + transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1]; + transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1]; + + // Add in the translation component of the transformation matrix. + transformed_position[0] += transformation.translation[0]; + transformed_position[1] += transformation.translation[1]; + + float2 viewport_size = float2((float)input_viewport_size->width, + (float)input_viewport_size->height); + float2 device_position = + transformed_position / viewport_size * float2(2., -2.) + float2(-1., 1.); + return float4(device_position, 0., 1.); +} + + +float2 to_tile_position(float2 unit_vertex, AtlasTile tile, + constant Size_DevicePixels *atlas_size) { + float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y); + float2 tile_size = float2(tile.bounds.size.width, tile.bounds.size.height); + return (tile_origin + unit_vertex * tile_size) / + float2((float)atlas_size->width, (float)atlas_size->height); +} + +float quad_sdf(float2 point, Bounds_ScaledPixels bounds, + Corners_ScaledPixels corner_radii) { + float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.; + float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size; + float2 center_to_point = point - center; + float corner_radius; + if (center_to_point.x < 0.) { + if (center_to_point.y < 0.) { + corner_radius = corner_radii.top_left; + } else { + corner_radius = corner_radii.bottom_left; + } + } else { + if (center_to_point.y < 0.) { + corner_radius = corner_radii.top_right; + } else { + corner_radius = corner_radii.bottom_right; + } + } + + float2 rounded_edge_to_point = + abs(center_to_point) - half_size + corner_radius; + float distance = + length(max(0., rounded_edge_to_point)) + + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - + corner_radius; + + return distance; +} + +// A standard gaussian function, used for weighting samples +float gaussian(float x, float sigma) { + return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +float2 erf(float2 x) { + float2 s = sign(x); + float2 a = abs(x); + x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + x *= x; + return s - s / (x * x); +} + +float blur_along_x(float x, float y, float sigma, float corner, + float2 half_size) { + float delta = min(half_size.y - corner - abs(y), 0.); + float curved = + half_size.x - corner + sqrt(max(0., corner * corner - delta * delta)); + float2 integral = + 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + return float4(position.x - clip_bounds.origin.x, + clip_bounds.origin.x + clip_bounds.size.width - position.x, + position.y - clip_bounds.origin.y, + clip_bounds.origin.y + clip_bounds.size.height - position.y); +} + +float4 over(float4 below, float4 above) { + float4 result; + float alpha = above.a + below.a * (1.0 - above.a); + result.rgb = + (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; + result.a = alpha; + return result; +} diff --git a/crates/ming/src/platform/mac/status_item.rs b/crates/ming/src/platform/mac/status_item.rs new file mode 100644 index 0000000..21cc860 --- /dev/null +++ b/crates/ming/src/platform/mac/status_item.rs @@ -0,0 +1,388 @@ +use crate::{ + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + platform::{ + self, + mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer}, + Event, FontSystem, WindowBounds, + }, + Scene, +}; +use cocoa::{ + appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow}, + base::{id, nil, YES}, + foundation::{NSPoint, NSRect, NSSize}, +}; +use ctor::ctor; +use foreign_types::ForeignTypeRef; +use objc::{ + class, + declare::ClassDecl, + msg_send, + rc::StrongPtr, + runtime::{Class, Object, Protocol, Sel}, + sel, sel_impl, +}; +use std::{ + cell::RefCell, + ffi::c_void, + ptr, + rc::{Rc, Weak}, + sync::Arc, +}; + +use super::screen::Screen; + +static mut VIEW_CLASS: *const Class = ptr::null(); +const STATE_IVAR: &str = "state"; + +#[ctor] +unsafe fn build_classes() { + VIEW_CLASS = { + let mut decl = ClassDecl::new("GPUIStatusItemView", class!(NSView)).unwrap(); + decl.add_ivar::<*mut c_void>(STATE_IVAR); + + decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(mouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseMoved:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(scrollWheel:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(flagsChanged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(makeBackingLayer), + make_backing_layer as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(viewDidChangeEffectiveAppearance), + view_did_change_effective_appearance as extern "C" fn(&Object, Sel), + ); + + decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); + decl.add_method( + sel!(displayLayer:), + display_layer as extern "C" fn(&Object, Sel, id), + ); + + decl.register() + }; +} + +pub struct StatusItem(Rc>); + +struct StatusItemState { + native_item: StrongPtr, + native_view: StrongPtr, + renderer: Renderer, + scene: Option, + event_callback: Option bool>>, + appearance_changed_callback: Option>, +} + +impl StatusItem { + pub fn add(fonts: Arc) -> Self { + unsafe { + let renderer = Renderer::new(false, fonts); + let status_bar = NSStatusBar::systemStatusBar(nil); + let native_item = + StrongPtr::retain(status_bar.statusItemWithLength_(NSSquareStatusItemLength)); + + let button = native_item.button(); + let _: () = msg_send![button, setHidden: YES]; + + let native_view = msg_send![VIEW_CLASS, alloc]; + let state = Rc::new(RefCell::new(StatusItemState { + native_item, + native_view: StrongPtr::new(native_view), + renderer, + scene: None, + event_callback: None, + appearance_changed_callback: None, + })); + + let parent_view = button.superview().superview(); + NSView::initWithFrame_( + native_view, + NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size), + ); + (*native_view).set_ivar( + STATE_IVAR, + Weak::into_raw(Rc::downgrade(&state)) as *const c_void, + ); + native_view.setWantsBestResolutionOpenGLSurface_(YES); + native_view.setWantsLayer(YES); + let _: () = msg_send![ + native_view, + setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize + ]; + + parent_view.addSubview_(native_view); + + { + let state = state.borrow(); + let layer = state.renderer.layer(); + let scale_factor = state.scale_factor(); + let size = state.content_size() * scale_factor; + layer.set_contents_scale(scale_factor.into()); + layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into())); + } + + Self(state) + } + } +} + +impl platform::Window for StatusItem { + fn bounds(&self) -> WindowBounds { + self.0.borrow().bounds() + } + + fn content_size(&self) -> Vector2F { + self.0.borrow().content_size() + } + + fn scale_factor(&self) -> f32 { + self.0.borrow().scale_factor() + } + + fn appearance(&self) -> platform::Appearance { + unsafe { + let appearance: id = + msg_send![self.0.borrow().native_item.button(), effectiveAppearance]; + platform::Appearance::from_native(appearance) + } + } + + fn screen(&self) -> Rc { + unsafe { + Rc::new(Screen { + native_screen: self.0.borrow().native_window().screen(), + }) + } + } + + fn mouse_position(&self) -> Vector2F { + unimplemented!() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn set_input_handler(&mut self, _: Box) {} + + fn prompt( + &self, + _: crate::platform::PromptLevel, + _: &str, + _: &[&str], + ) -> postage::oneshot::Receiver { + unimplemented!() + } + + fn activate(&self) { + unimplemented!() + } + + fn set_title(&mut self, _: &str) { + unimplemented!() + } + + fn set_edited(&mut self, _: bool) { + unimplemented!() + } + + fn show_character_palette(&self) { + unimplemented!() + } + + fn minimize(&self) { + unimplemented!() + } + + fn zoom(&self) { + unimplemented!() + } + + fn present_scene(&mut self, scene: Scene) { + self.0.borrow_mut().scene = Some(scene); + unsafe { + let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES]; + } + } + + fn toggle_fullscreen(&self) { + unimplemented!() + } + + fn on_event(&mut self, callback: Box bool>) { + self.0.borrow_mut().event_callback = Some(callback); + } + + fn on_active_status_change(&mut self, _: Box) {} + + fn on_resize(&mut self, _: Box) {} + + fn on_fullscreen(&mut self, _: Box) {} + + fn on_moved(&mut self, _: Box) {} + + fn on_should_close(&mut self, _: Box bool>) {} + + fn on_close(&mut self, _: Box) {} + + fn on_appearance_changed(&mut self, callback: Box) { + self.0.borrow_mut().appearance_changed_callback = Some(callback); + } + + fn is_topmost_for_position(&self, _: Vector2F) -> bool { + true + } +} + +impl StatusItemState { + fn bounds(&self) -> WindowBounds { + unsafe { + let window: id = self.native_window(); + let screen_frame = window.screen().visibleFrame(); + let window_frame = NSWindow::frame(window); + let origin = vec2f( + window_frame.origin.x as f32, + (window_frame.origin.y - screen_frame.size.height - window_frame.size.height) + as f32, + ); + let size = vec2f( + window_frame.size.width as f32, + window_frame.size.height as f32, + ); + WindowBounds::Fixed(RectF::new(origin, size)) + } + } + + fn content_size(&self) -> Vector2F { + unsafe { + let NSSize { width, height, .. } = + NSView::frame(self.native_item.button().superview().superview()).size; + vec2f(width as f32, height as f32) + } + } + + fn scale_factor(&self) -> f32 { + unsafe { + let window: id = msg_send![self.native_item.button(), window]; + NSScreen::backingScaleFactor(window.screen()) as f32 + } + } + + pub fn native_window(&self) -> id { + unsafe { msg_send![self.native_item.button(), window] } + } +} + +extern "C" fn dealloc_view(this: &Object, _: Sel) { + unsafe { + drop_state(this); + + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } +} + +extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { + unsafe { + if let Some(state) = get_state(this).upgrade() { + let mut state_borrow = state.as_ref().borrow_mut(); + if let Some(event) = + Event::from_native(native_event, Some(state_borrow.content_size().y())) + { + if let Some(mut callback) = state_borrow.event_callback.take() { + drop(state_borrow); + callback(event); + state.borrow_mut().event_callback = Some(callback); + } + } + } + } +} + +extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { + if let Some(state) = unsafe { get_state(this).upgrade() } { + let state = state.borrow(); + state.renderer.layer().as_ptr() as id + } else { + nil + } +} + +extern "C" fn display_layer(this: &Object, _: Sel, _: id) { + unsafe { + if let Some(state) = get_state(this).upgrade() { + let mut state = state.borrow_mut(); + if let Some(scene) = state.scene.take() { + state.renderer.render(&scene); + } + } + } +} + +extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) { + unsafe { + if let Some(state) = get_state(this).upgrade() { + let mut state_borrow = state.as_ref().borrow_mut(); + if let Some(mut callback) = state_borrow.appearance_changed_callback.take() { + drop(state_borrow); + callback(); + state.borrow_mut().appearance_changed_callback = Some(callback); + } + } + } +} + +unsafe fn get_state(object: &Object) -> Weak> { + let raw: *mut c_void = *object.get_ivar(STATE_IVAR); + let weak1 = Weak::from_raw(raw as *mut RefCell); + let weak2 = weak1.clone(); + let _ = Weak::into_raw(weak1); + weak2 +} + +unsafe fn drop_state(object: &Object) { + let raw: *const c_void = *object.get_ivar(STATE_IVAR); + Weak::from_raw(raw as *const RefCell); +} diff --git a/crates/ming/src/platform/mac/text_system.rs b/crates/ming/src/platform/mac/text_system.rs new file mode 100644 index 0000000..8806d1b --- /dev/null +++ b/crates/ming/src/platform/mac/text_system.rs @@ -0,0 +1,704 @@ +use crate::{ + point, px, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, + FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, + RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS, +}; +use anyhow::anyhow; +use cocoa::appkit::{CGFloat, CGPoint}; +use collections::{BTreeSet, HashMap}; +use core_foundation::{ + attributed_string::CFMutableAttributedString, + base::{CFRange, TCFType}, + number::CFNumber, + string::CFString, +}; +use core_graphics::{ + base::{kCGImageAlphaPremultipliedLast, CGGlyph}, + color_space::CGColorSpace, + context::CGContext, +}; +use core_text::{ + font::CTFont, + font_descriptor::{ + kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait, + }, + line::CTLine, + string_attributes::kCTFontAttributeName, +}; +use font_kit::{ + font::Font as FontKitFont, + handle::Handle, + hinting::HintingOptions, + metrics::Metrics, + properties::{Style as FontkitStyle, Weight as FontkitWeight}, + source::SystemSource, + sources::mem::MemSource, +}; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use pathfinder_geometry::{ + rect::{RectF, RectI}, + transform2d::Transform2F, + vector::{Vector2F, Vector2I}, +}; +use smallvec::SmallVec; +use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc}; + +use super::open_type; + +#[allow(non_upper_case_globals)] +const kCGImageAlphaOnly: u32 = 7; + +pub(crate) struct MacTextSystem(RwLock); + +#[derive(Clone, PartialEq, Eq, Hash)] +struct FontKey { + font_family: SharedString, + font_features: FontFeatures, +} + +struct MacTextSystemState { + memory_source: MemSource, + system_source: SystemSource, + fonts: Vec, + font_selections: HashMap, + font_ids_by_postscript_name: HashMap, + font_ids_by_font_key: HashMap>, + postscript_names_by_font_id: HashMap, +} + +impl MacTextSystem { + pub(crate) fn new() -> Self { + Self(RwLock::new(MacTextSystemState { + memory_source: MemSource::empty(), + system_source: SystemSource::new(), + fonts: Vec::new(), + font_selections: HashMap::default(), + font_ids_by_postscript_name: HashMap::default(), + font_ids_by_font_key: HashMap::default(), + postscript_names_by_font_id: HashMap::default(), + })) + } +} + +impl Default for MacTextSystem { + fn default() -> Self { + Self::new() + } +} + +impl PlatformTextSystem for MacTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } + + fn all_font_names(&self) -> Vec { + let collection = core_text::font_collection::create_for_all_families(); + let Some(descriptors) = collection.get_descriptors() else { + return Vec::new(); + }; + let mut names = BTreeSet::new(); + for descriptor in descriptors.into_iter() { + names.extend(lenient_font_attributes::family_name(&descriptor)); + } + if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { + names.extend(fonts_in_memory); + } + names.into_iter().collect() + } + + fn all_font_families(&self) -> Vec { + self.0 + .read() + .system_source + .all_families() + .expect("core text should never return an error") + } + + fn font_id(&self, font: &Font) -> Result { + let lock = self.0.upgradable_read(); + if let Some(font_id) = lock.font_selections.get(font) { + Ok(*font_id) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let font_key = FontKey { + font_family: font.family.clone(), + font_features: font.features.clone(), + }; + let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { + font_ids.as_slice() + } else { + let font_ids = lock.load_family(&font.family, &font.features)?; + lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); + lock.font_ids_by_font_key[&font_key].as_ref() + }; + + let candidate_properties = candidates + .iter() + .map(|font_id| lock.fonts[font_id.0].properties()) + .collect::>(); + + let ix = font_kit::matching::find_best_match( + &candidate_properties, + &font_kit::properties::Properties { + style: font.style.into(), + weight: font.weight.into(), + stretch: Default::default(), + }, + )?; + + let font_id = candidates[ix]; + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) + } + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + self.0.read().fonts[font_id.0].metrics().into() + } + + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(self.0.read().fonts[font_id.0] + .typographic_bounds(glyph_id.0)? + .into()) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + self.0.read().advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + self.0.read().raster_bounds(params) + } + + fn rasterize_glyph( + &self, + glyph_id: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + self.0.read().rasterize_glyph(glyph_id, raster_bounds) + } + + fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + self.0.write().layout_line(text, font_size, font_runs) + } +} + +impl MacTextSystemState { + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + let fonts = fonts + .into_iter() + .map(|bytes| match bytes { + Cow::Borrowed(embedded_font) => { + let data_provider = unsafe { + core_graphics::data_provider::CGDataProvider::from_slice(embedded_font) + }; + let font = core_graphics::font::CGFont::from_data_provider(data_provider) + .map_err(|_| anyhow!("Could not load an embedded font."))?; + let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font); + Ok(Handle::from_native(&font)) + } + Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)), + }) + .collect::>>()?; + self.memory_source.add_fonts(fonts.into_iter())?; + Ok(()) + } + + fn load_family( + &mut self, + name: &str, + features: &FontFeatures, + ) -> Result> { + let name = if name == ".SystemUIFont" { + ".AppleSystemUIFont" + } else { + name + }; + + let mut font_ids = SmallVec::new(); + let family = self + .memory_source + .select_family_by_name(name) + .or_else(|_| self.system_source.select_family_by_name(name))?; + for font in family.fonts() { + let mut font = font.load()?; + + open_type::apply_features(&mut font, features); + + // This block contains a precautionary fix to guard against loading fonts + // that might cause panics due to `.unwrap()`s up the chain. + { + // We use the 'm' character for text measurements in various spots + // (e.g., the editor). However, at time of writing some of those usages + // will panic if the font has no 'm' glyph. + // + // Therefore, we check up front that the font has the necessary glyph. + let has_m_glyph = font.glyph_for_char('m').is_some(); + + // HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph, + // but we need to be able to load it for rendering Windows icons in + // the Storybook (on macOS). + let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons"; + + if !has_m_glyph && !is_segoe_fluent_icons { + // I spent far too long trying to track down why a font missing the 'm' + // character wasn't loading. This log statement will hopefully save + // someone else from suffering the same fate. + log::warn!( + "font '{}' has no 'm' character and was not loaded", + font.full_name() + ); + continue; + } + } + + // We've seen a number of panics in production caused by calling font.properties() + // which unwraps a downcast to CFNumber. This is an attempt to avoid the panic, + // and to try and identify the incalcitrant font. + let traits = font.native_font().all_traits(); + if unsafe { + !(traits + .get(kCTFontSymbolicTrait) + .downcast::() + .is_some() + && traits + .get(kCTFontWidthTrait) + .downcast::() + .is_some() + && traits + .get(kCTFontWeightTrait) + .downcast::() + .is_some() + && traits + .get(kCTFontSlantTrait) + .downcast::() + .is_some()) + } { + log::error!( + "Failed to read traits for font {:?}", + font.postscript_name().unwrap() + ); + continue; + } + + let font_id = FontId(self.fonts.len()); + font_ids.push(font_id); + let postscript_name = font.postscript_name().unwrap(); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts.push(font); + } + Ok(font_ids) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(self.fonts[font_id.0].advance(glyph_id.0)?.into()) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId) + } + + fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId { + let postscript_name = requested_font.postscript_name(); + if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) { + *font_id + } else { + let font_id = FontId(self.fonts.len()); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts + .push(font_kit::font::Font::from_core_graphics_font( + requested_font.copy_to_CGFont(), + )); + font_id + } + } + + fn is_emoji(&self, font_id: FontId) -> bool { + self.postscript_names_by_font_id + .get(&font_id) + .map_or(false, |postscript_name| { + postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" + }) + } + + fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let font = &self.fonts[params.font_id.0]; + let scale = Transform2F::from_scale(params.scale_factor); + Ok(font + .raster_bounds( + params.glyph_id.0, + params.font_size.into(), + scale, + HintingOptions::None, + font_kit::canvas::RasterizationOptions::GrayscaleAa, + )? + .into()) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { + if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { + Err(anyhow!("glyph bounds are empty")) + } else { + // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. + let mut bitmap_size = glyph_bounds.size; + if params.subpixel_variant.x > 0 { + bitmap_size.width += DevicePixels(1); + } + if params.subpixel_variant.y > 0 { + bitmap_size.height += DevicePixels(1); + } + let bitmap_size = bitmap_size; + + let mut bytes; + let cx; + if params.is_emoji { + bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + bitmap_size.width.0 as usize, + bitmap_size.height.0 as usize, + 8, + bitmap_size.width.0 as usize * 4, + &CGColorSpace::create_device_rgb(), + kCGImageAlphaPremultipliedLast, + ); + } else { + bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + bitmap_size.width.0 as usize, + bitmap_size.height.0 as usize, + 8, + bitmap_size.width.0 as usize, + &CGColorSpace::create_device_gray(), + kCGImageAlphaOnly, + ); + } + + // Move the origin to bottom left and account for scaling, this + // makes drawing text consistent with the font-kit's raster_bounds. + cx.translate( + -glyph_bounds.origin.x.0 as CGFloat, + (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat, + ); + cx.scale( + params.scale_factor as CGFloat, + params.scale_factor as CGFloat, + ); + + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); + cx.set_allows_font_subpixel_positioning(true); + cx.set_should_subpixel_position_fonts(true); + cx.set_allows_font_subpixel_quantization(false); + cx.set_should_subpixel_quantize_fonts(false); + self.fonts[params.font_id.0] + .native_font() + .clone_with_font_size(f32::from(params.font_size) as CGFloat) + .draw_glyphs( + &[params.glyph_id.0 as CGGlyph], + &[CGPoint::new( + (subpixel_shift.x / params.scale_factor) as CGFloat, + (subpixel_shift.y / params.scale_factor) as CGFloat, + )], + cx, + ); + + if params.is_emoji { + // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. + for pixel in bytes.chunks_exact_mut(4) { + pixel.swap(0, 2); + let a = pixel[3] as f32 / 255.; + pixel[0] = (pixel[0] as f32 / a) as u8; + pixel[1] = (pixel[1] as f32 / a) as u8; + pixel[2] = (pixel[2] as f32 / a) as u8; + } + } + + Ok((bitmap_size, bytes)) + } + } + + fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. + let mut string = CFMutableAttributedString::new(); + { + string.replace_str(&CFString::new(text), CFRange::init(0, 0)); + let utf16_line_len = string.char_len() as usize; + + let mut ix_converter = StringIndexConverter::new(text); + for run in font_runs { + let utf8_end = ix_converter.utf8_ix + run.len; + let utf16_start = ix_converter.utf16_ix; + + if utf16_start >= utf16_line_len { + break; + } + + ix_converter.advance_to_utf8_ix(utf8_end); + let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len); + + let cf_range = + CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); + + let font: &FontKitFont = &self.fonts[run.font_id.0]; + unsafe { + string.set_attribute( + cf_range, + kCTFontAttributeName, + &font.native_font().clone_with_font_size(font_size.into()), + ); + } + + if utf16_end == utf16_line_len { + break; + } + } + } + + // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. + let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); + + let mut runs = Vec::new(); + for run in line.glyph_runs().into_iter() { + let attributes = run.attributes().unwrap(); + let font = unsafe { + attributes + .get(kCTFontAttributeName) + .downcast::() + .unwrap() + }; + let font_id = self.id_for_native_font(font); + + let mut ix_converter = StringIndexConverter::new(text); + let mut glyphs = SmallVec::new(); + for ((glyph_id, position), glyph_utf16_ix) in run + .glyphs() + .iter() + .zip(run.positions().iter()) + .zip(run.string_indices().iter()) + { + let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap(); + ix_converter.advance_to_utf16_ix(glyph_utf16_ix); + glyphs.push(ShapedGlyph { + id: GlyphId(*glyph_id as u32), + position: point(position.x as f32, position.y as f32).map(px), + index: ix_converter.utf8_ix, + is_emoji: self.is_emoji(font_id), + }); + } + + runs.push(ShapedRun { font_id, glyphs }) + } + + let typographic_bounds = line.get_typographic_bounds(); + LineLayout { + runs, + font_size, + width: typographic_bounds.width.into(), + ascent: typographic_bounds.ascent.into(), + descent: typographic_bounds.descent.into(), + len: text.len(), + } + } +} + +#[derive(Clone)] +struct StringIndexConverter<'a> { + text: &'a str, + utf8_ix: usize, + utf16_ix: usize, +} + +impl<'a> StringIndexConverter<'a> { + fn new(text: &'a str) -> Self { + Self { + text, + utf8_ix: 0, + utf16_ix: 0, + } + } + + fn advance_to_utf8_ix(&mut self, utf8_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf8_ix + ix >= utf8_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } + + fn advance_to_utf16_ix(&mut self, utf16_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf16_ix >= utf16_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } +} + +impl From for FontMetrics { + fn from(metrics: Metrics) -> Self { + FontMetrics { + units_per_em: metrics.units_per_em, + ascent: metrics.ascent, + descent: metrics.descent, + line_gap: metrics.line_gap, + underline_position: metrics.underline_position, + underline_thickness: metrics.underline_thickness, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + bounding_box: metrics.bounding_box.into(), + } + } +} + +impl From for Bounds { + fn from(rect: RectF) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())), + size: size(DevicePixels(rect.width()), DevicePixels(rect.height())), + } + } +} + +impl From for Size { + fn from(value: Vector2I) -> Self { + size(value.x().into(), value.y().into()) + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From> for Vector2I { + fn from(size: Point) -> Self { + Vector2I::new(size.x as i32, size.y as i32) + } +} + +impl From for Size { + fn from(vec: Vector2F) -> Self { + size(vec.x(), vec.y()) + } +} + +impl From for FontkitWeight { + fn from(value: FontWeight) -> Self { + FontkitWeight(value.0) + } +} + +impl From for FontkitStyle { + fn from(style: FontStyle) -> Self { + match style { + FontStyle::Normal => FontkitStyle::Normal, + FontStyle::Italic => FontkitStyle::Italic, + FontStyle::Oblique => FontkitStyle::Oblique, + } + } +} + +// Some fonts may have no attributest despite `core_text` requiring them (and panicking). +// This is the same version as `core_text` has without `expect` calls. +mod lenient_font_attributes { + use core_foundation::{ + base::{CFRetain, CFType, TCFType}, + string::{CFString, CFStringRef}, + }; + use core_text::font_descriptor::{ + kCTFontFamilyNameAttribute, CTFontDescriptor, CTFontDescriptorCopyAttribute, + }; + + pub fn family_name(descriptor: &CTFontDescriptor) -> Option { + unsafe { get_string_attribute(descriptor, kCTFontFamilyNameAttribute) } + } + + fn get_string_attribute( + descriptor: &CTFontDescriptor, + attribute: CFStringRef, + ) -> Option { + unsafe { + let value = CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), attribute); + if value.is_null() { + return None; + } + + let value = CFType::wrap_under_create_rule(value); + assert!(value.instance_of::()); + let s = wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef); + Some(s.to_string()) + } + } + + unsafe fn wrap_under_get_rule(reference: CFStringRef) -> CFString { + assert!(!reference.is_null(), "Attempted to create a NULL object."); + let reference = CFRetain(reference as *const ::std::os::raw::c_void) as CFStringRef; + TCFType::wrap_under_create_rule(reference) + } +} + +#[cfg(test)] +mod tests { + use crate::{font, px, FontRun, GlyphId, MacTextSystem, PlatformTextSystem}; + + #[test] + fn test_layout_line_bom_char() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + let line = "\u{feff}"; + let mut style = FontRun { + font_id, + len: line.len(), + }; + + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert!(layout.runs.is_empty()); + + let line = "a\u{feff}b"; + style.len = line.len(); + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert_eq!(layout.runs.len(), 1); + assert_eq!(layout.runs[0].glyphs.len(), 2); + assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a + // There's no glyph for \u{feff} + assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b + } +} diff --git a/crates/ming/src/platform/mac/window.rs b/crates/ming/src/platform/mac/window.rs new file mode 100644 index 0000000..eab3edd --- /dev/null +++ b/crates/ming/src/platform/mac/window.rs @@ -0,0 +1,1951 @@ +use super::{ns_string, renderer, MacDisplay, NSRange}; +use crate::{ + platform::PlatformInputHandler, point, px, size, AnyWindowHandle, Bounds, DevicePixels, + DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, + Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, + Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, + WindowParams, +}; +use block::ConcreteBlock; +use cocoa::{ + appkit::{ + CGPoint, NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags, + NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable, + NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior, + NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility, + }, + base::{id, nil}, + foundation::{ + NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect, + NSSize, NSString, NSUInteger, + }, +}; +use core_graphics::display::{CGDirectDisplayID, CGRect}; +use ctor::ctor; +use futures::channel::oneshot; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES}, + sel, sel_impl, +}; +use parking_lot::Mutex; +use raw_window_handle as rwh; +use smallvec::SmallVec; +use std::{ + cell::Cell, + ffi::{c_void, CStr}, + mem, + ops::Range, + os::raw::c_char, + path::PathBuf, + ptr::{self, NonNull}, + rc::Rc, + sync::{Arc, Weak}, + time::Duration, +}; +use util::ResultExt; + +const WINDOW_STATE_IVAR: &str = "windowState"; + +static mut WINDOW_CLASS: *const Class = ptr::null(); +static mut PANEL_CLASS: *const Class = ptr::null(); +static mut VIEW_CLASS: *const Class = ptr::null(); + +#[allow(non_upper_case_globals)] +const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask = + unsafe { NSWindowStyleMask::from_bits_unchecked(1 << 7) }; +#[allow(non_upper_case_globals)] +const NSNormalWindowLevel: NSInteger = 0; +#[allow(non_upper_case_globals)] +const NSPopUpWindowLevel: NSInteger = 101; +#[allow(non_upper_case_globals)] +const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01; +#[allow(non_upper_case_globals)] +const NSTrackingMouseMoved: NSUInteger = 0x02; +#[allow(non_upper_case_globals)] +const NSTrackingActiveAlways: NSUInteger = 0x80; +#[allow(non_upper_case_globals)] +const NSTrackingInVisibleRect: NSUInteger = 0x200; +#[allow(non_upper_case_globals)] +const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4; +#[allow(non_upper_case_globals)] +const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; +// https://developer.apple.com/documentation/appkit/nsdragoperation +type NSDragOperation = NSUInteger; +#[allow(non_upper_case_globals)] +const NSDragOperationNone: NSDragOperation = 0; +#[allow(non_upper_case_globals)] +const NSDragOperationCopy: NSDragOperation = 1; + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + // Widely used private APIs; Apple uses them for their Terminal.app. + fn CGSMainConnectionID() -> id; + fn CGSSetWindowBackgroundBlurRadius( + connection_id: id, + window_id: NSInteger, + radius: i64, + ) -> i32; +} + +#[ctor] +unsafe fn build_classes() { + WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow)); + PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel)); + VIEW_CLASS = { + let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap(); + decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); + + decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(performKeyEquivalent:), + handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(keyDown:), + handle_key_down as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseMoved:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseExited:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(mouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(scrollWheel:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(flagsChanged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(cancelOperation:), + cancel_operation as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(makeBackingLayer), + make_backing_layer as extern "C" fn(&Object, Sel) -> id, + ); + + decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); + decl.add_method( + sel!(viewDidChangeBackingProperties), + view_did_change_backing_properties as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(setFrameSize:), + set_frame_size as extern "C" fn(&Object, Sel, NSSize), + ); + decl.add_method( + sel!(displayLayer:), + display_layer as extern "C" fn(&Object, Sel, id), + ); + + decl.add_protocol(Protocol::get("NSTextInputClient").unwrap()); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(markedRange), + marked_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range + as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, + ); + decl.add_method( + sel!(viewDidChangeEffectiveAppearance), + view_did_change_effective_appearance as extern "C" fn(&Object, Sel), + ); + + // Suppress beep on keystrokes with modifier keys. + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(&Object, Sel, Sel), + ); + + decl.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + + decl.register() + }; +} + +pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point { + point( + px(position.x as f32), + // MacOS screen coordinates are relative to bottom left + window_height - px(position.y as f32), + ) +} + +unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class { + let mut decl = ClassDecl::new(name, superclass).unwrap(); + decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); + decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(canBecomeMainWindow), + yes as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(canBecomeKeyWindow), + yes as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeOcclusionState:), + window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidMove:), + window_did_move as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeScreen:), + window_did_change_screen as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidBecomeKey:), + window_did_change_key_status as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidResignKey:), + window_did_change_key_status as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + + decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C" fn(&Object, Sel, id) -> NSDragOperation, + ); + decl.add_method( + sel!(draggingUpdated:), + dragging_updated as extern "C" fn(&Object, Sel, id) -> NSDragOperation, + ); + decl.add_method( + sel!(draggingExited:), + dragging_exited as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(concludeDragOperation:), + conclude_drag_operation as extern "C" fn(&Object, Sel, id), + ); + + decl.register() +} + +#[allow(clippy::enum_variant_names)] +enum ImeInput { + InsertText(String, Option>), + SetMarkedText(String, Option>, Option>), + UnmarkText, +} + +struct MacWindowState { + handle: AnyWindowHandle, + executor: ForegroundExecutor, + native_window: id, + native_view: NonNull, + display_link: Option, + renderer: renderer::Renderer, + request_frame_callback: Option>, + event_callback: Option crate::DispatchEventResult>>, + activate_callback: Option>, + resize_callback: Option, f32)>>, + moved_callback: Option>, + should_close_callback: Option bool>>, + close_callback: Option>, + appearance_changed_callback: Option>, + input_handler: Option, + last_key_equivalent: Option, + synthetic_drag_counter: usize, + last_fresh_keydown: Option, + traffic_light_position: Option>, + previous_modifiers_changed_event: Option, + // State tracking what the IME did after the last request + input_during_keydown: Option>, + previous_keydown_inserted_text: Option, + external_files_dragged: bool, + // Whether the next left-mouse click is also the focusing click. + first_mouse: bool, + fullscreen_restore_bounds: Bounds, +} + +impl MacWindowState { + fn move_traffic_light(&self) { + if let Some(traffic_light_position) = self.traffic_light_position { + if self.is_fullscreen() { + // Moving traffic lights while fullscreen doesn't work, + // see https://github.com/zed-industries/zed/issues/4712 + return; + } + + let titlebar_height = self.titlebar_height(); + + unsafe { + let close_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowCloseButton + ]; + let min_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton + ]; + let zoom_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowZoomButton + ]; + + let mut close_button_frame: CGRect = msg_send![close_button, frame]; + let mut min_button_frame: CGRect = msg_send![min_button, frame]; + let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; + let mut origin = point( + traffic_light_position.x, + titlebar_height + - traffic_light_position.y + - px(close_button_frame.size.height as f32), + ); + let button_spacing = + px((min_button_frame.origin.x - close_button_frame.origin.x) as f32); + + close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![close_button, setFrame: close_button_frame]; + origin.x += button_spacing; + + min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![min_button, setFrame: min_button_frame]; + origin.x += button_spacing; + + zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + origin.x += button_spacing; + } + } + } + + fn start_display_link(&mut self) { + self.stop_display_link(); + unsafe { + if !self + .native_window + .occlusionState() + .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) + { + return; + } + } + let display_id = unsafe { display_id_for_screen(self.native_window.screen()) }; + if let Some(mut display_link) = + DisplayLink::new(display_id, self.native_view.as_ptr() as *mut c_void, step).log_err() + { + display_link.start().log_err(); + self.display_link = Some(display_link); + } + } + + fn stop_display_link(&mut self) { + self.display_link = None; + } + + fn is_maximized(&self) -> bool { + unsafe { + let bounds = self.bounds(); + let screen_size = self.native_window.screen().visibleFrame().into(); + bounds.size == screen_size + } + } + + fn is_fullscreen(&self) -> bool { + unsafe { + let style_mask = self.native_window.styleMask(); + style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } + + fn bounds(&self) -> Bounds { + let mut window_frame = unsafe { NSWindow::frame(self.native_window) }; + let screen_frame = unsafe { + let screen = NSWindow::screen(self.native_window); + NSScreen::frame(screen) + }; + + // Flip the y coordinate to be top-left origin + window_frame.origin.y = + screen_frame.size.height - window_frame.origin.y - window_frame.size.height; + + let bounds = Bounds::new( + point( + ((window_frame.origin.x - screen_frame.origin.x) as i32).into(), + ((window_frame.origin.y - screen_frame.origin.y) as i32).into(), + ), + size( + (window_frame.size.width as i32).into(), + (window_frame.size.height as i32).into(), + ), + ); + bounds + } + + fn content_size(&self) -> Size { + let NSSize { width, height, .. } = + unsafe { NSView::frame(self.native_window.contentView()) }.size; + size(px(width as f32), px(height as f32)) + } + + fn scale_factor(&self) -> f32 { + get_scale_factor(self.native_window) + } + + fn update_drawable_size(&mut self, drawable_size: NSSize) { + self.renderer.update_drawable_size(Size { + width: drawable_size.width, + height: drawable_size.height, + }) + } + + fn titlebar_height(&self) -> Pixels { + unsafe { + let frame = NSWindow::frame(self.native_window); + let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; + px((frame.size.height - content_layout_rect.size.height) as f32) + } + } + + fn window_bounds(&self) -> WindowBounds { + if self.is_fullscreen() { + WindowBounds::Fullscreen(self.fullscreen_restore_bounds) + } else { + WindowBounds::Windowed(self.bounds()) + } + } +} + +unsafe impl Send for MacWindowState {} + +pub(crate) struct MacWindow(Arc>); + +impl MacWindow { + pub fn open( + handle: AnyWindowHandle, + WindowParams { + window_background, + bounds, + titlebar, + kind, + is_movable, + focus, + show, + display_id, + }: WindowParams, + executor: ForegroundExecutor, + renderer_context: renderer::Context, + ) -> Self { + unsafe { + let pool = NSAutoreleasePool::new(nil); + + let mut style_mask; + if let Some(titlebar) = titlebar.as_ref() { + style_mask = NSWindowStyleMask::NSClosableWindowMask + | NSWindowStyleMask::NSMiniaturizableWindowMask + | NSWindowStyleMask::NSResizableWindowMask + | NSWindowStyleMask::NSTitledWindowMask; + + if titlebar.appears_transparent { + style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } + } else { + style_mask = NSWindowStyleMask::NSTitledWindowMask + | NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } + + let native_window: id = match kind { + WindowKind::Normal => msg_send![WINDOW_CLASS, alloc], + WindowKind::PopUp => { + style_mask |= NSWindowStyleMaskNonactivatingPanel; + msg_send![PANEL_CLASS, alloc] + } + }; + + let display = display_id + .and_then(MacDisplay::find_by_id) + .unwrap_or_else(|| MacDisplay::primary()); + + let mut target_screen = nil; + let mut screen_frame = None; + + let screens = NSScreen::screens(nil); + let count: u64 = cocoa::foundation::NSArray::count(screens); + for i in 0..count { + let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i); + let frame = NSScreen::visibleFrame(screen); + let display_id = display_id_for_screen(screen); + if display_id == display.0 { + screen_frame = Some(frame); + target_screen = screen; + } + } + + let screen_frame = screen_frame.unwrap_or_else(|| { + let screen = NSScreen::mainScreen(nil); + target_screen = screen; + NSScreen::visibleFrame(screen) + }); + + let window_rect = NSRect::new( + NSPoint::new( + screen_frame.origin.x + bounds.origin.x.0 as f64, + screen_frame.origin.y + + (display.bounds().size.height - bounds.origin.y).0 as f64, + ), + NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64), + ); + + let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_( + window_rect, + style_mask, + NSBackingStoreBuffered, + NO, + target_screen, + ); + assert!(!native_window.is_null()); + let () = msg_send![ + native_window, + registerForDraggedTypes: + NSArray::arrayWithObject(nil, NSFilenamesPboardType) + ]; + let () = msg_send![ + native_window, + setReleasedWhenClosed: NO + ]; + + let native_view: id = msg_send![VIEW_CLASS, alloc]; + let native_view = NSView::init(native_view); + assert!(!native_view.is_null()); + + let window_size = { + let scale = get_scale_factor(native_window); + size( + bounds.size.width.0 as f32 * scale, + bounds.size.height.0 as f32 * scale, + ) + }; + + let mut window = Self(Arc::new(Mutex::new(MacWindowState { + handle, + executor, + native_window, + native_view: NonNull::new_unchecked(native_view), + display_link: None, + renderer: renderer::new_renderer( + renderer_context, + native_window as *mut _, + native_view as *mut _, + window_size, + window_background != WindowBackgroundAppearance::Opaque, + ), + request_frame_callback: None, + event_callback: None, + activate_callback: None, + resize_callback: None, + moved_callback: None, + should_close_callback: None, + close_callback: None, + appearance_changed_callback: None, + input_handler: None, + last_key_equivalent: None, + synthetic_drag_counter: 0, + last_fresh_keydown: None, + traffic_light_position: titlebar + .as_ref() + .and_then(|titlebar| titlebar.traffic_light_position), + previous_modifiers_changed_event: None, + input_during_keydown: None, + previous_keydown_inserted_text: None, + external_files_dragged: false, + first_mouse: false, + fullscreen_restore_bounds: Bounds::default(), + }))); + + (*native_window).set_ivar( + WINDOW_STATE_IVAR, + Arc::into_raw(window.0.clone()) as *const c_void, + ); + native_window.setDelegate_(native_window); + (*native_view).set_ivar( + WINDOW_STATE_IVAR, + Arc::into_raw(window.0.clone()) as *const c_void, + ); + + if let Some(title) = titlebar + .as_ref() + .and_then(|t| t.title.as_ref().map(AsRef::as_ref)) + { + native_window.setTitle_(NSString::alloc(nil).init_str(title)); + } + + native_window.setMovable_(is_movable as BOOL); + + if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) { + native_window.setTitlebarAppearsTransparent_(YES); + native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); + } + + native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + native_view.setWantsBestResolutionOpenGLSurface_(YES); + + // From winit crate: On Mojave, views automatically become layer-backed shortly after + // being added to a native_window. Changing the layer-backedness of a view breaks the + // association between the view and its associated OpenGL context. To work around this, + // on we explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + native_view.setWantsLayer(YES); + let _: () = msg_send![ + native_view, + setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize + ]; + + native_window.setContentView_(native_view.autorelease()); + native_window.makeFirstResponder_(native_view); + + window.set_background_appearance(window_background); + + match kind { + WindowKind::Normal => { + native_window.setLevel_(NSNormalWindowLevel); + native_window.setAcceptsMouseMovedEvents_(YES); + } + WindowKind::PopUp => { + // Use a tracking area to allow receiving MouseMoved events even when + // the window or application aren't active, which is often the case + // e.g. for notification windows. + let tracking_area: id = msg_send![class!(NSTrackingArea), alloc]; + let _: () = msg_send![ + tracking_area, + initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)) + options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect + owner: native_view + userInfo: nil + ]; + let _: () = + msg_send![native_view, addTrackingArea: tracking_area.autorelease()]; + + native_window.setLevel_(NSPopUpWindowLevel); + let _: () = msg_send![ + native_window, + setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow + ]; + native_window.setCollectionBehavior_( + NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces | + NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary + ); + } + } + + if focus { + native_window.makeKeyAndOrderFront_(nil); + } else if show { + native_window.orderFront_(nil); + } + + // Set the initial position of the window to the specified origin. + // Although we already specified the position using `initWithContentRect_styleMask_backing_defer_screen_`, + // the window position might be incorrect if the main screen (the screen that contains the window that has focus) + // is different from the primary screen. + NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin); + window.0.lock().move_traffic_light(); + + pool.drain(); + + window + } + } + + pub fn active_window() -> Option { + unsafe { + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*main_window).lock().handle; + Some(handle) + } else { + None + } + } + } +} + +impl Drop for MacWindow { + fn drop(&mut self) { + let mut this = self.0.lock(); + this.renderer.destroy(); + let window = this.native_window; + this.display_link.take(); + unsafe { + this.native_window.setDelegate_(nil); + } + this.executor + .spawn(async move { + unsafe { + window.close(); + window.autorelease(); + } + }) + .detach(); + } +} + +impl PlatformWindow for MacWindow { + fn bounds(&self) -> Bounds { + self.0.as_ref().lock().bounds() + } + + fn window_bounds(&self) -> WindowBounds { + self.0.as_ref().lock().window_bounds() + } + + fn is_maximized(&self) -> bool { + self.0.as_ref().lock().is_maximized() + } + + fn content_size(&self) -> Size { + self.0.as_ref().lock().content_size() + } + + fn scale_factor(&self) -> f32 { + self.0.as_ref().lock().scale_factor() + } + + fn appearance(&self) -> WindowAppearance { + unsafe { + let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance]; + WindowAppearance::from_native(appearance) + } + } + + fn display(&self) -> Rc { + unsafe { + let screen = self.0.lock().native_window.screen(); + let device_description: id = msg_send![screen, deviceDescription]; + let screen_number: id = NSDictionary::valueForKey_( + device_description, + NSString::alloc(nil).init_str("NSScreenNumber"), + ); + + let screen_number: u32 = msg_send![screen_number, unsignedIntValue]; + + Rc::new(MacDisplay(screen_number)) + } + } + + fn mouse_position(&self) -> Point { + let position = unsafe { + self.0 + .lock() + .native_window + .mouseLocationOutsideOfEventStream() + }; + convert_mouse_position(position, self.content_size().height) + } + + fn modifiers(&self) -> Modifiers { + unsafe { + let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags]; + + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); + + Modifiers { + control, + alt, + shift, + platform: command, + function, + } + } + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.as_ref().lock().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.as_ref().lock().input_handler.take() + } + + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[&str], + ) -> Option> { + // macOs applies overrides to modal window buttons after they are added. + // Two most important for this logic are: + // * Buttons with "Cancel" title will be displayed as the last buttons in the modal + // * Last button added to the modal via `addButtonWithTitle` stays focused + // * Focused buttons react on "space"/" " keypresses + // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus + // + // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion + // ``` + // By default, the first button has a key equivalent of Return, + // any button with a title of “Cancel” has a key equivalent of Escape, + // and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button). + // ``` + // + // To avoid situations when the last element added is "Cancel" and it gets the focus + // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button + // last, so it gets focus and a Space shortcut. + // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key. + let latest_non_cancel_label = answers + .iter() + .enumerate() + .rev() + .find(|(_, &label)| label != "Cancel") + .filter(|&(label_index, _)| label_index > 0); + + unsafe { + let alert: id = msg_send![class!(NSAlert), alloc]; + let alert: id = msg_send![alert, init]; + let alert_style = match level { + PromptLevel::Info => 1, + PromptLevel::Warning => 0, + PromptLevel::Critical => 2, + }; + let _: () = msg_send![alert, setAlertStyle: alert_style]; + let _: () = msg_send![alert, setMessageText: ns_string(msg)]; + if let Some(detail) = detail { + let _: () = msg_send![alert, setInformativeText: ns_string(detail)]; + } + + for (ix, answer) in answers + .iter() + .enumerate() + .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix)) + { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + } + if let Some((ix, answer)) = latest_non_cancel_label { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + } + + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |answer: NSInteger| { + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(answer.try_into().unwrap()); + } + }); + let block = block.copy(); + let native_window = self.0.lock().native_window; + let executor = self.0.lock().executor.clone(); + executor + .spawn(async move { + let _: () = msg_send![ + alert, + beginSheetModalForWindow: native_window + completionHandler: block + ]; + }) + .detach(); + + Some(done_rx) + } + } + + fn activate(&self) { + let window = self.0.lock().native_window; + let executor = self.0.lock().executor.clone(); + executor + .spawn(async move { + unsafe { + let _: () = msg_send![window, makeKeyAndOrderFront: nil]; + } + }) + .detach(); + } + + fn is_active(&self) -> bool { + unsafe { self.0.lock().native_window.isKeyWindow() == YES } + } + + fn set_title(&mut self, title: &str) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let window = self.0.lock().native_window; + let title = ns_string(title); + let _: () = msg_send![app, changeWindowsItem:window title:title filename:false]; + let _: () = msg_send![window, setTitle: title]; + self.0.lock().move_traffic_light(); + } + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + let mut this = self.0.as_ref().lock(); + this.renderer + .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); + + let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { + 80 + } else { + 0 + }; + let opaque = if background_appearance == WindowBackgroundAppearance::Opaque { + YES + } else { + NO + }; + unsafe { + this.native_window.setOpaque_(opaque); + // Shadows for transparent windows cause artifacts and performance issues + this.native_window.setHasShadow_(opaque); + let clear_color = if opaque == YES { + NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64) + } else { + NSColor::clearColor(nil) + }; + this.native_window.setBackgroundColor_(clear_color); + let window_number = this.native_window.windowNumber(); + CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); + } + } + + fn set_edited(&mut self, edited: bool) { + unsafe { + let window = self.0.lock().native_window; + msg_send![window, setDocumentEdited: edited as BOOL] + } + + // Changing the document edited state resets the traffic light position, + // so we have to move it again. + self.0.lock().move_traffic_light(); + } + + fn show_character_palette(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, orderFrontCharacterPalette: window]; + } + }) + .detach(); + } + + fn minimize(&self) { + let window = self.0.lock().native_window; + unsafe { + window.miniaturize_(nil); + } + } + + fn zoom(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.zoom_(nil); + } + }) + .detach(); + } + + fn toggle_fullscreen(&self) { + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.toggleFullScreen_(nil); + } + }) + .detach(); + } + + fn is_fullscreen(&self) -> bool { + let this = self.0.lock(); + let window = this.native_window; + + unsafe { + window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } + + fn on_request_frame(&self, callback: Box) { + self.0.as_ref().lock().request_frame_callback = Some(callback); + } + + fn on_input(&self, callback: Box crate::DispatchEventResult>) { + self.0.as_ref().lock().event_callback = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.as_ref().lock().activate_callback = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.as_ref().lock().resize_callback = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.as_ref().lock().moved_callback = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.as_ref().lock().should_close_callback = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.as_ref().lock().close_callback = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.lock().appearance_changed_callback = Some(callback); + } + + fn draw(&self, scene: &crate::Scene) { + let mut this = self.0.lock(); + this.renderer.draw(scene); + } + + fn sprite_atlas(&self) -> Arc { + self.0.lock().renderer.sprite_atlas().clone() + } +} + +impl rwh::HasWindowHandle for MacWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + // SAFETY: The AppKitWindowHandle is a wrapper around a pointer to an NSView + unsafe { + Ok(rwh::WindowHandle::borrow_raw(rwh::RawWindowHandle::AppKit( + rwh::AppKitWindowHandle::new(self.0.lock().native_view.cast()), + ))) + } + } +} + +impl rwh::HasDisplayHandle for MacWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + // SAFETY: This is a no-op on macOS + unsafe { + Ok(rwh::DisplayHandle::borrow_raw( + rwh::AppKitDisplayHandle::new().into(), + )) + } + } +} + +fn get_scale_factor(native_window: id) -> f32 { + let factor = unsafe { + let screen: id = msg_send![native_window, screen]; + NSScreen::backingScaleFactor(screen) as f32 + }; + + // We are not certain what triggers this, but it seems that sometimes + // this method would return 0 (https://github.com/zed-industries/zed/issues/6412) + // It seems most likely that this would happen if the window has no screen + // (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before + // it was rendered for real. + // Regardless, attempt to avoid the issue here. + if factor == 0.0 { + 2. + } else { + factor + } +} + +unsafe fn get_window_state(object: &Object) -> Arc> { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + let rc1 = Arc::from_raw(raw as *mut Mutex); + let rc2 = rc1.clone(); + mem::forget(rc1); + rc2 +} + +unsafe fn drop_window_state(object: &Object) { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + Arc::from_raw(raw as *mut Mutex); +} + +extern "C" fn yes(_: &Object, _: Sel) -> BOOL { + YES +} + +extern "C" fn dealloc_window(this: &Object, _: Sel) { + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSWindow)), dealloc]; + } +} + +extern "C" fn dealloc_view(this: &Object, _: Sel) { + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } +} + +extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { + handle_key_event(this, native_event, true) +} + +extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) { + handle_key_event(this, native_event, false); +} + +// Things to test if you're modifying this method: +// Brazilian layout: +// - `" space` should type a quote +// - `" backspace` should delete the marked quote +// - `" up` should type the quote, unmark it, and move up one line +// - `" cmd-down` should not leave a marked quote behind (it maybe should dispatch the key though?) +// - `cmd-ctrl-space` and clicking on an emoji should type it +// Czech (QWERTY) layout: +// - in vim mode `option-4` should go to end of line (same as $) +extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + let window_height = lock.content_size().height; + let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; + + if let Some(PlatformInput::KeyDown(mut event)) = event { + // For certain keystrokes, macOS will first dispatch a "key equivalent" event. + // If that event isn't handled, it will then dispatch a "key down" event. GPUI + // makes no distinction between these two types of events, so we need to ignore + // the "key down" event if we've already just processed its "key equivalent" version. + if key_equivalent { + lock.last_key_equivalent = Some(event.clone()); + } else if lock.last_key_equivalent.take().as_ref() == Some(&event) { + return NO; + } + + let keydown = event.keystroke.clone(); + let fn_modifier = keydown.modifiers.function; + // Ignore events from held-down keys after some of the initially-pressed keys + // were released. + if event.is_held { + if lock.last_fresh_keydown.as_ref() != Some(&keydown) { + return YES; + } + } else { + lock.last_fresh_keydown = Some(keydown.clone()); + } + lock.input_during_keydown = Some(SmallVec::new()); + drop(lock); + + // Send the event to the input context for IME handling, unless the `fn` modifier is + // being pressed. + // this will call back into `insert_text`, etc. + if !fn_modifier { + unsafe { + let input_context: id = msg_send![this, inputContext]; + let _: BOOL = msg_send![input_context, handleEvent: native_event]; + } + } + + let mut handled = false; + let mut lock = window_state.lock(); + let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take(); + let mut input_during_keydown = lock.input_during_keydown.take().unwrap(); + let mut callback = lock.event_callback.take(); + drop(lock); + + let last_ime = input_during_keydown.pop(); + // on a brazilian keyboard typing `"` and then hitting `up` will cause two IME + // events, one to unmark the quote, and one to send the up arrow. + for ime in input_during_keydown { + send_to_input_handler(this, ime); + } + + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + + if let Some(ime) = last_ime { + if let ImeInput::InsertText(text, _) = &ime { + if !is_composing { + window_state.lock().previous_keydown_inserted_text = Some(text.clone()); + if let Some(callback) = callback.as_mut() { + event.keystroke.ime_key = Some(text.clone()); + handled = !callback(PlatformInput::KeyDown(event)).propagate; + } + } + } + + if !handled { + handled = true; + send_to_input_handler(this, ime); + } + } else if !is_composing { + let is_held = event.is_held; + + if let Some(callback) = callback.as_mut() { + handled = !callback(PlatformInput::KeyDown(event)).propagate; + } + + if !handled && is_held { + if let Some(text) = previous_keydown_inserted_text { + // MacOS IME is a bit funky, and even when you've told it there's nothing to + // enter it will still swallow certain keys (e.g. 'f', 'j') and not others + // (e.g. 'n'). This is a problem for certain kinds of views, like the terminal. + with_input_handler(this, |input_handler| { + if input_handler.selected_text_range().is_none() { + handled = true; + input_handler.replace_text_in_range(None, &text) + } + }); + window_state.lock().previous_keydown_inserted_text = Some(text); + } + } + } + + window_state.lock().event_callback = callback; + + handled as BOOL + } else { + NO + } +} + +extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { + let window_state = unsafe { get_window_state(this) }; + let weak_window_state = Arc::downgrade(&window_state); + let mut lock = window_state.as_ref().lock(); + let window_height = lock.content_size().height; + let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; + + if let Some(mut event) = event { + match &mut event { + PlatformInput::MouseDown( + event @ MouseDownEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + // On mac, a ctrl-left click should be handled as a right click. + *event = MouseDownEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + } + + // Handles focusing click. + PlatformInput::MouseDown( + event @ MouseDownEvent { + button: MouseButton::Left, + .. + }, + ) if (lock.first_mouse) => { + *event = MouseDownEvent { + first_mouse: true, + ..*event + }; + lock.first_mouse = false; + } + + // Because we map a ctrl-left_down to a right_down -> right_up let's ignore + // the ctrl-left_up to avoid having a mismatch in button down/up events if the + // user is still holding ctrl when releasing the left mouse button + PlatformInput::MouseUp( + event @ MouseUpEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + *event = MouseUpEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + } + + _ => {} + }; + + match &event { + PlatformInput::MouseMove( + event @ MouseMoveEvent { + pressed_button: Some(_), + .. + }, + ) => { + // Synthetic drag is used for selecting long buffer contents while buffer is being scrolled. + // External file drag and drop is able to emit its own synthetic mouse events which will conflict + // with these ones. + if !lock.external_files_dragged { + lock.synthetic_drag_counter += 1; + let executor = lock.executor.clone(); + executor + .spawn(synthetic_drag( + weak_window_state, + lock.synthetic_drag_counter, + event.clone(), + )) + .detach(); + } + } + + PlatformInput::MouseUp(MouseUpEvent { .. }) => { + lock.synthetic_drag_counter += 1; + } + + PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers }) => { + // Only raise modifiers changed event when they have actually changed + if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: prev_modifiers, + })) = &lock.previous_modifiers_changed_event + { + if prev_modifiers == modifiers { + return; + } + } + + lock.previous_modifiers_changed_event = Some(event.clone()); + } + + _ => {} + } + + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(event); + window_state.lock().event_callback = Some(callback); + } + } +} + +// Allows us to receive `cmd-.` (the shortcut for closing a dialog) +// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 +extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + let keystroke = Keystroke { + modifiers: Default::default(), + key: ".".into(), + ime_key: None, + }; + let event = PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + }); + + lock.last_fresh_keydown = Some(keystroke); + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(event); + window_state.lock().event_callback = Some(callback); + } +} + +extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let lock = &mut *window_state.lock(); + unsafe { + if lock + .native_window + .occlusionState() + .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) + { + lock.start_display_link(); + } else { + lock.stop_display_link(); + } + } +} + +extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + window_state.as_ref().lock().move_traffic_light(); +} + +extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.fullscreen_restore_bounds = lock.bounds(); +} + +extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.moved_callback.take() { + drop(lock); + callback(); + window_state.lock().moved_callback = Some(callback); + } +} + +extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.start_display_link(); +} + +extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let lock = window_state.lock(); + let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; + + // When opening a pop-up while the application isn't active, Cocoa sends a spurious + // `windowDidBecomeKey` message to the previous key window even though that window + // isn't actually key. This causes a bug if the application is later activated while + // the pop-up is still open, making it impossible to activate the previous key window + // even if the pop-up gets closed. The only way to activate it again is to de-activate + // the app and re-activate it, which is a pretty bad UX. + // The following code detects the spurious event and invokes `resignKeyWindow`: + // in theory, we're not supposed to invoke this method manually but it balances out + // the spurious `becomeKeyWindow` event and helps us work around that bug. + if selector == sel!(windowDidBecomeKey:) && !is_active { + unsafe { + let _: () = msg_send![lock.native_window, resignKeyWindow]; + return; + } + } + + let executor = lock.executor.clone(); + drop(lock); + executor + .spawn(async move { + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.activate_callback.take() { + drop(lock); + callback(is_active); + window_state.lock().activate_callback = Some(callback); + }; + }) + .detach(); +} + +extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.should_close_callback.take() { + drop(lock); + let should_close = callback(); + window_state.lock().should_close_callback = Some(callback); + should_close as BOOL + } else { + YES + } +} + +extern "C" fn close_window(this: &Object, _: Sel) { + unsafe { + let close_callback = { + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + lock.close_callback.take() + }; + + if let Some(callback) = close_callback { + callback(); + } + + let _: () = msg_send![super(this, class!(NSWindow)), close]; + } +} + +extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { + let window_state = unsafe { get_window_state(this) }; + let window_state = window_state.as_ref().lock(); + window_state.renderer.layer_ptr() as id +} + +extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + let scale_factor = lock.scale_factor() as f64; + let size = lock.content_size(); + let drawable_size: NSSize = NSSize { + width: f64::from(size.width) * scale_factor, + height: f64::from(size.height) * scale_factor, + }; + unsafe { + let _: () = msg_send![ + lock.renderer.layer(), + setContentsScale: scale_factor + ]; + } + + lock.update_drawable_size(drawable_size); + + if let Some(mut callback) = lock.resize_callback.take() { + let content_size = lock.content_size(); + let scale_factor = lock.scale_factor(); + drop(lock); + callback(content_size, scale_factor); + window_state.as_ref().lock().resize_callback = Some(callback); + }; +} + +extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + + if lock.content_size() == size.into() { + return; + } + + unsafe { + let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; + } + + let scale_factor = lock.scale_factor() as f64; + let drawable_size: NSSize = NSSize { + width: size.width * scale_factor, + height: size.height * scale_factor, + }; + + lock.update_drawable_size(drawable_size); + + drop(lock); + let mut lock = window_state.lock(); + if let Some(mut callback) = lock.resize_callback.take() { + let content_size = lock.content_size(); + let scale_factor = lock.scale_factor(); + drop(lock); + callback(content_size, scale_factor); + window_state.lock().resize_callback = Some(callback); + }; +} + +extern "C" fn display_layer(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + if let Some(mut callback) = lock.request_frame_callback.take() { + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(true); + lock.stop_display_link(); + drop(lock); + callback(); + + let mut lock = window_state.lock(); + lock.request_frame_callback = Some(callback); + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(false); + lock.start_display_link(); + } +} + +unsafe extern "C" fn step(view: *mut c_void) { + let view = view as id; + let window_state = unsafe { get_window_state(&*view) }; + let mut lock = window_state.lock(); + + if let Some(mut callback) = lock.request_frame_callback.take() { + drop(lock); + callback(); + window_state.lock().request_frame_callback = Some(callback); + } +} + +extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { + unsafe { msg_send![class!(NSArray), array] } +} + +extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL { + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some() as BOOL +} + +extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.selected_text_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn first_rect_for_character_range( + this: &Object, + _: Sel, + range: NSRange, + _: id, +) -> NSRect { + let frame = unsafe { + let window = get_window_state(this).lock().native_window; + NSView::frame(window) + }; + with_input_handler(this, |input_handler| { + input_handler.bounds_for_range(range.to_range()?) + }) + .flatten() + .map_or( + NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)), + |bounds| { + NSRect::new( + NSPoint::new( + frame.origin.x + bounds.origin.x.0 as f64, + frame.origin.y + frame.size.height + - bounds.origin.y.0 as f64 + - bounds.size.height.0 as f64, + ), + NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64), + ) + }, + ) +} + +extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + let replacement_range = replacement_range.to_range(); + send_to_input_handler( + this, + ImeInput::InsertText(text.to_string(), replacement_range), + ); + } +} + +extern "C" fn set_marked_text( + this: &Object, + _: Sel, + text: id, + selected_range: NSRange, + replacement_range: NSRange, +) { + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let selected_range = selected_range.to_range(); + let replacement_range = replacement_range.to_range(); + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + + send_to_input_handler( + this, + ImeInput::SetMarkedText(text.to_string(), replacement_range, selected_range), + ); + } +} +extern "C" fn unmark_text(this: &Object, _: Sel) { + send_to_input_handler(this, ImeInput::UnmarkText); +} + +extern "C" fn attributed_substring_for_proposed_range( + this: &Object, + _: Sel, + range: NSRange, + _actual_range: *mut c_void, +) -> id { + with_input_handler(this, |input_handler| { + let range = range.to_range()?; + if range.is_empty() { + return None; + } + + let selected_text = input_handler.text_for_range(range)?; + unsafe { + let string: id = msg_send![class!(NSAttributedString), alloc]; + let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; + Some(string) + } + }) + .flatten() + .unwrap_or(nil) +} + +extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {} + +extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) { + unsafe { + let state = get_window_state(this); + let mut lock = state.as_ref().lock(); + if let Some(mut callback) = lock.appearance_changed_callback.take() { + drop(lock); + callback(); + state.lock().appearance_changed_callback = Some(callback); + } + } +} + +extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.first_mouse = true; + YES +} + +extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + let paths = external_paths_from_event(dragging_info); + if let Some(event) = + paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths })) + { + if send_new_event(&window_state, event) { + window_state.lock().external_files_dragged = true; + return NSDragOperationCopy; + } + } + NSDragOperationNone +} + +extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + if send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Pending { position }), + ) { + NSDragOperationCopy + } else { + NSDragOperationNone + } +} + +extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Exited), + ); + window_state.lock().external_files_dragged = false; +} + +extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let position = drag_event_position(&window_state, dragging_info); + if send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Submit { position }), + ) { + YES + } else { + NO + } +} + +fn external_paths_from_event(dragging_info: *mut Object) -> Option { + let mut paths = SmallVec::new(); + let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] }; + let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) }; + if filenames == nil { + return None; + } + for file in unsafe { filenames.iter() } { + let path = unsafe { + let f = NSString::UTF8String(file); + CStr::from_ptr(f).to_string_lossy().into_owned() + }; + paths.push(PathBuf::from(path)) + } + Some(ExternalPaths(paths)) +} + +extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Exited), + ); +} + +async fn synthetic_drag( + window_state: Weak>, + drag_id: usize, + event: MouseMoveEvent, +) { + loop { + Timer::after(Duration::from_millis(16)).await; + if let Some(window_state) = window_state.upgrade() { + let mut lock = window_state.lock(); + if lock.synthetic_drag_counter == drag_id { + if let Some(mut callback) = lock.event_callback.take() { + drop(lock); + callback(PlatformInput::MouseMove(event.clone())); + window_state.lock().event_callback = Some(callback); + } + } else { + break; + } + } + } +} + +fn send_new_event(window_state_lock: &Mutex, e: PlatformInput) -> bool { + let window_state = window_state_lock.lock().event_callback.take(); + if let Some(mut callback) = window_state { + callback(e); + window_state_lock.lock().event_callback = Some(callback); + true + } else { + false + } +} + +fn drag_event_position(window_state: &Mutex, dragging_info: id) -> Point { + let drag_location: NSPoint = unsafe { msg_send![dragging_info, draggingLocation] }; + convert_mouse_position(drag_location, window_state.lock().content_size().height) +} + +fn with_input_handler(window: &Object, f: F) -> Option +where + F: FnOnce(&mut PlatformInputHandler) -> R, +{ + let window_state = unsafe { get_window_state(window) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut input_handler) = lock.input_handler.take() { + drop(lock); + let result = f(&mut input_handler); + window_state.lock().input_handler = Some(input_handler); + Some(result) + } else { + None + } +} + +fn send_to_input_handler(window: &Object, ime: ImeInput) { + unsafe { + let window_state = get_window_state(window); + let mut lock = window_state.lock(); + if let Some(ime_input) = lock.input_during_keydown.as_mut() { + ime_input.push(ime); + return; + } + if let Some(mut input_handler) = lock.input_handler.take() { + drop(lock); + match ime { + ImeInput::InsertText(text, range) => { + input_handler.replace_text_in_range(range, &text) + } + ImeInput::SetMarkedText(text, range, marked_range) => { + input_handler.replace_and_mark_text_in_range(range, &text, marked_range) + } + ImeInput::UnmarkText => input_handler.unmark_text(), + } + window_state.lock().input_handler = Some(input_handler); + } + } +} + +unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID { + let device_description = NSScreen::deviceDescription(screen); + let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber"); + let screen_number = device_description.objectForKey_(screen_number_key); + let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue]; + screen_number as CGDirectDisplayID +} diff --git a/crates/ming/src/platform/mac/window_appearance.rs b/crates/ming/src/platform/mac/window_appearance.rs new file mode 100644 index 0000000..0a5df85 --- /dev/null +++ b/crates/ming/src/platform/mac/window_appearance.rs @@ -0,0 +1,35 @@ +use crate::WindowAppearance; +use cocoa::{ + appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight}, + base::id, + foundation::NSString, +}; +use objc::{msg_send, sel, sel_impl}; +use std::ffi::CStr; + +impl WindowAppearance { + pub(crate) unsafe fn from_native(appearance: id) -> Self { + let name: id = msg_send![appearance, name]; + if name == NSAppearanceNameVibrantLight { + Self::VibrantLight + } else if name == NSAppearanceNameVibrantDark { + Self::VibrantDark + } else if name == NSAppearanceNameAqua { + Self::Light + } else if name == NSAppearanceNameDarkAqua { + Self::Dark + } else { + println!( + "unknown appearance: {:?}", + CStr::from_ptr(name.UTF8String()) + ); + Self::Light + } + } +} + +#[link(name = "AppKit", kind = "framework")] +extern "C" { + pub static NSAppearanceNameAqua: id; + pub static NSAppearanceNameDarkAqua: id; +} diff --git a/crates/ming/src/platform/test.rs b/crates/ming/src/platform/test.rs new file mode 100644 index 0000000..c602335 --- /dev/null +++ b/crates/ming/src/platform/test.rs @@ -0,0 +1,10 @@ +mod dispatcher; +mod display; +mod platform; +mod text_system; +mod window; + +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use platform::*; +pub(crate) use window::*; diff --git a/crates/ming/src/platform/test/dispatcher.rs b/crates/ming/src/platform/test/dispatcher.rs new file mode 100644 index 0000000..e9cab32 --- /dev/null +++ b/crates/ming/src/platform/test/dispatcher.rs @@ -0,0 +1,298 @@ +use crate::{PlatformDispatcher, TaskLabel}; +use async_task::Runnable; +use backtrace::Backtrace; +use collections::{HashMap, HashSet, VecDeque}; +use parking::{Parker, Unparker}; +use parking_lot::Mutex; +use rand::prelude::*; +use std::{ + future::Future, + ops::RangeInclusive, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::Duration, +}; +use util::post_inc; + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +struct TestDispatcherId(usize); + +#[doc(hidden)] +pub struct TestDispatcher { + id: TestDispatcherId, + state: Arc>, + parker: Arc>, + unparker: Unparker, +} + +struct TestDispatcherState { + random: StdRng, + foreground: HashMap>, + background: Vec, + deprioritized_background: Vec, + delayed: Vec<(Duration, Runnable)>, + time: Duration, + is_main_thread: bool, + next_id: TestDispatcherId, + allow_parking: bool, + waiting_hint: Option, + waiting_backtrace: Option, + deprioritized_task_labels: HashSet, + block_on_ticks: RangeInclusive, +} + +impl TestDispatcher { + pub fn new(random: StdRng) -> Self { + let (parker, unparker) = parking::pair(); + let state = TestDispatcherState { + random, + foreground: HashMap::default(), + background: Vec::new(), + deprioritized_background: Vec::new(), + delayed: Vec::new(), + time: Duration::ZERO, + is_main_thread: true, + next_id: TestDispatcherId(1), + allow_parking: false, + waiting_hint: None, + waiting_backtrace: None, + deprioritized_task_labels: Default::default(), + block_on_ticks: 0..=1000, + }; + + TestDispatcher { + id: TestDispatcherId(0), + state: Arc::new(Mutex::new(state)), + parker: Arc::new(Mutex::new(parker)), + unparker, + } + } + + pub fn advance_clock(&self, by: Duration) { + let new_now = self.state.lock().time + by; + loop { + self.run_until_parked(); + let state = self.state.lock(); + let next_due_time = state.delayed.first().map(|(time, _)| *time); + drop(state); + if let Some(due_time) = next_due_time { + if due_time <= new_now { + self.state.lock().time = due_time; + continue; + } + } + break; + } + self.state.lock().time = new_now; + } + + pub fn simulate_random_delay(&self) -> impl 'static + Send + Future { + struct YieldNow { + pub(crate) count: usize, + } + + impl Future for YieldNow { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + if self.count > 0 { + self.count -= 1; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + } + } + + YieldNow { + count: self.state.lock().random.gen_range(0..10), + } + } + + pub fn tick(&self, background_only: bool) -> bool { + let mut state = self.state.lock(); + + while let Some((deadline, _)) = state.delayed.first() { + if *deadline > state.time { + break; + } + let (_, runnable) = state.delayed.remove(0); + state.background.push(runnable); + } + + let foreground_len: usize = if background_only { + 0 + } else { + state + .foreground + .values() + .map(|runnables| runnables.len()) + .sum() + }; + let background_len = state.background.len(); + + let runnable; + let main_thread; + if foreground_len == 0 && background_len == 0 { + let deprioritized_background_len = state.deprioritized_background.len(); + if deprioritized_background_len == 0 { + return false; + } + let ix = state.random.gen_range(0..deprioritized_background_len); + main_thread = false; + runnable = state.deprioritized_background.swap_remove(ix); + } else { + main_thread = state.random.gen_ratio( + foreground_len as u32, + (foreground_len + background_len) as u32, + ); + if main_thread { + let state = &mut *state; + runnable = state + .foreground + .values_mut() + .filter(|runnables| !runnables.is_empty()) + .choose(&mut state.random) + .unwrap() + .pop_front() + .unwrap(); + } else { + let ix = state.random.gen_range(0..background_len); + runnable = state.background.swap_remove(ix); + }; + }; + + let was_main_thread = state.is_main_thread; + state.is_main_thread = main_thread; + drop(state); + runnable.run(); + self.state.lock().is_main_thread = was_main_thread; + + true + } + + pub fn deprioritize(&self, task_label: TaskLabel) { + self.state + .lock() + .deprioritized_task_labels + .insert(task_label); + } + + pub fn run_until_parked(&self) { + while self.tick(false) {} + } + + pub fn parking_allowed(&self) -> bool { + self.state.lock().allow_parking + } + + pub fn allow_parking(&self) { + self.state.lock().allow_parking = true + } + + pub fn forbid_parking(&self) { + self.state.lock().allow_parking = false + } + + pub fn set_waiting_hint(&self, msg: Option) { + self.state.lock().waiting_hint = msg + } + + pub fn waiting_hint(&self) -> Option { + self.state.lock().waiting_hint.clone() + } + + pub fn start_waiting(&self) { + self.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved()); + } + + pub fn finish_waiting(&self) { + self.state.lock().waiting_backtrace.take(); + } + + pub fn waiting_backtrace(&self) -> Option { + self.state.lock().waiting_backtrace.take().map(|mut b| { + b.resolve(); + b + }) + } + + pub fn rng(&self) -> StdRng { + self.state.lock().random.clone() + } + + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.state.lock().block_on_ticks = range; + } + + pub fn gen_block_on_ticks(&self) -> usize { + let mut lock = self.state.lock(); + let block_on_ticks = lock.block_on_ticks.clone(); + lock.random.gen_range(block_on_ticks) + } +} + +impl Clone for TestDispatcher { + fn clone(&self) -> Self { + let id = post_inc(&mut self.state.lock().next_id.0); + Self { + id: TestDispatcherId(id), + state: self.state.clone(), + parker: self.parker.clone(), + unparker: self.unparker.clone(), + } + } +} + +impl PlatformDispatcher for TestDispatcher { + fn is_main_thread(&self) -> bool { + self.state.lock().is_main_thread + } + + fn dispatch(&self, runnable: Runnable, label: Option) { + { + let mut state = self.state.lock(); + if label.map_or(false, |label| { + state.deprioritized_task_labels.contains(&label) + }) { + state.deprioritized_background.push(runnable); + } else { + state.background.push(runnable); + } + } + self.unparker.unpark(); + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + self.state + .lock() + .foreground + .entry(self.id) + .or_default() + .push_back(runnable); + self.unparker.unpark(); + } + + fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) { + let mut state = self.state.lock(); + let next_time = state.time + duration; + let ix = match state.delayed.binary_search_by_key(&next_time, |e| e.0) { + Ok(ix) | Err(ix) => ix, + }; + state.delayed.insert(ix, (next_time, runnable)); + } + fn park(&self, _: Option) -> bool { + self.parker.lock().park(); + true + } + + fn unparker(&self) -> Unparker { + self.unparker.clone() + } + + fn as_test(&self) -> Option<&TestDispatcher> { + Some(self) + } +} diff --git a/crates/ming/src/platform/test/display.rs b/crates/ming/src/platform/test/display.rs new file mode 100644 index 0000000..4e2e9db --- /dev/null +++ b/crates/ming/src/platform/test/display.rs @@ -0,0 +1,37 @@ +use anyhow::{Ok, Result}; + +use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Point}; + +#[derive(Debug)] +pub(crate) struct TestDisplay { + id: DisplayId, + uuid: uuid::Uuid, + bounds: Bounds, +} + +impl TestDisplay { + pub fn new() -> Self { + TestDisplay { + id: DisplayId(1), + uuid: uuid::Uuid::new_v4(), + bounds: Bounds::from_corners( + Point::default(), + Point::new(DevicePixels(1920), DevicePixels(1080)), + ), + } + } +} + +impl PlatformDisplay for TestDisplay { + fn id(&self) -> crate::DisplayId { + self.id + } + + fn uuid(&self) -> Result { + Ok(self.uuid) + } + + fn bounds(&self) -> crate::Bounds { + self.bounds + } +} diff --git a/crates/ming/src/platform/test/platform.rs b/crates/ming/src/platform/test/platform.rs new file mode 100644 index 0000000..8fa558e --- /dev/null +++ b/crates/ming/src/platform/test/platform.rs @@ -0,0 +1,306 @@ +use crate::{ + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, Keymap, + Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow, WindowAppearance, + WindowParams, +}; +use anyhow::{anyhow, Result}; +use collections::VecDeque; +use futures::channel::oneshot; +use parking_lot::Mutex; +use std::{ + cell::RefCell, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::Arc, +}; + +/// TestPlatform implements the Platform trait for use in tests. +pub(crate) struct TestPlatform { + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + + pub(crate) active_window: RefCell>, + active_display: Rc, + active_cursor: Mutex, + current_clipboard_item: Mutex>, + current_primary_item: Mutex>, + pub(crate) prompts: RefCell, + pub opened_url: RefCell>, + weak: Weak, +} + +#[derive(Default)] +pub(crate) struct TestPrompts { + multiple_choice: VecDeque>, + new_path: VecDeque<(PathBuf, oneshot::Sender>)>, +} + +impl TestPlatform { + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { + Rc::new_cyclic(|weak| TestPlatform { + background_executor: executor, + foreground_executor, + prompts: Default::default(), + active_cursor: Default::default(), + active_display: Rc::new(TestDisplay::new()), + active_window: Default::default(), + current_clipboard_item: Mutex::new(None), + current_primary_item: Mutex::new(None), + weak: weak.clone(), + opened_url: Default::default(), + }) + } + + pub(crate) fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + let (path, tx) = self + .prompts + .borrow_mut() + .new_path + .pop_front() + .expect("no pending new path prompt"); + tx.send(select_path(&path)).ok(); + } + + pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) { + let tx = self + .prompts + .borrow_mut() + .multiple_choice + .pop_front() + .expect("no pending multiple choice prompt"); + self.background_executor().set_waiting_hint(None); + tx.send(response_ix).ok(); + } + + pub(crate) fn has_pending_prompt(&self) -> bool { + !self.prompts.borrow().multiple_choice.is_empty() + } + + pub(crate) fn prompt(&self, msg: &str, detail: Option<&str>) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + self.background_executor() + .set_waiting_hint(Some(format!("PROMPT: {:?} {:?}", msg, detail))); + self.prompts.borrow_mut().multiple_choice.push_back(tx); + rx + } + + pub(crate) fn set_active_window(&self, window: Option) { + let executor = self.foreground_executor().clone(); + let previous_window = self.active_window.borrow_mut().take(); + self.active_window.borrow_mut().clone_from(&window); + + executor + .spawn(async move { + if let Some(previous_window) = previous_window { + if let Some(window) = window.as_ref() { + if Arc::ptr_eq(&previous_window.0, &window.0) { + return; + } + } + previous_window.simulate_active_status_change(false); + } + if let Some(window) = window { + window.simulate_active_status_change(true); + } + }) + .detach(); + } + + pub(crate) fn did_prompt_for_new_path(&self) -> bool { + self.prompts.borrow().new_path.len() > 0 + } +} + +impl Platform for TestPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + #[cfg(target_os = "macos")] + return Arc::new(crate::platform::mac::MacTextSystem::new()); + + #[cfg(target_os = "linux")] + return Arc::new(crate::platform::cosmic_text::CosmicTextSystem::new()); + + #[cfg(target_os = "windows")] + return Arc::new(crate::platform::windows::DirectWriteTextSystem::new().unwrap()); + } + + fn run(&self, _on_finish_launching: Box) { + unimplemented!() + } + + fn quit(&self) {} + + fn restart(&self, _: Option) { + unimplemented!() + } + + fn activate(&self, _ignoring_other_apps: bool) { + // + } + + fn hide(&self) { + unimplemented!() + } + + fn hide_other_apps(&self) { + unimplemented!() + } + + fn unhide_other_apps(&self) { + unimplemented!() + } + + fn displays(&self) -> Vec> { + vec![self.active_display.clone()] + } + + fn primary_display(&self) -> Option> { + Some(self.active_display.clone()) + } + + fn active_window(&self) -> Option { + self.active_window + .borrow() + .as_ref() + .map(|window| window.0.lock().handle) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> Box { + let window = TestWindow::new( + handle, + params, + self.weak.clone(), + self.active_display.clone(), + ); + Box::new(window) + } + + fn window_appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } + + fn open_url(&self, url: &str) { + *self.opened_url.borrow_mut() = Some(url.to_string()) + } + + fn on_open_urls(&self, _callback: Box)>) { + unimplemented!() + } + + fn prompt_for_paths( + &self, + _options: crate::PathPromptOptions, + ) -> oneshot::Receiver>> { + unimplemented!() + } + + fn prompt_for_new_path( + &self, + directory: &std::path::Path, + ) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel(); + self.prompts + .borrow_mut() + .new_path + .push_back((directory.to_path_buf(), tx)); + rx + } + + fn reveal_path(&self, _path: &std::path::Path) { + unimplemented!() + } + + fn on_quit(&self, _callback: Box) {} + + fn on_reopen(&self, _callback: Box) { + unimplemented!() + } + + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} + + fn add_recent_document(&self, _paths: &Path) {} + + fn on_app_menu_action(&self, _callback: Box) {} + + fn on_will_open_app_menu(&self, _callback: Box) {} + + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} + + fn os_name(&self) -> &'static str { + "test" + } + + fn os_version(&self) -> Result { + Err(anyhow!("os_version called on TestPlatform")) + } + + fn app_version(&self) -> Result { + Err(anyhow!("app_version called on TestPlatform")) + } + + fn app_path(&self) -> Result { + unimplemented!() + } + + fn local_timezone(&self) -> time::UtcOffset { + time::UtcOffset::UTC + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + unimplemented!() + } + + fn set_cursor_style(&self, style: crate::CursorStyle) { + *self.active_cursor.lock() = style; + } + + fn should_auto_hide_scrollbars(&self) -> bool { + false + } + + fn write_to_primary(&self, item: ClipboardItem) { + *self.current_primary_item.lock() = Some(item); + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + *self.current_clipboard_item.lock() = Some(item); + } + + fn read_from_primary(&self) -> Option { + self.current_primary_item.lock().clone() + } + + fn read_from_clipboard(&self) -> Option { + self.current_clipboard_item.lock().clone() + } + + fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { + Task::ready(Ok(())) + } + + fn read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } + + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + unimplemented!() + } +} diff --git a/crates/ming/src/platform/test/text_system.rs b/crates/ming/src/platform/test/text_system.rs new file mode 100644 index 0000000..bec559a --- /dev/null +++ b/crates/ming/src/platform/test/text_system.rs @@ -0,0 +1,50 @@ +use crate::{ + Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels, + PlatformTextSystem, RenderGlyphParams, Size, +}; +use anyhow::Result; +use std::borrow::Cow; + +pub(crate) struct TestTextSystem {} + +// todo(linux) +#[allow(unused)] +impl PlatformTextSystem for TestTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + unimplemented!() + } + fn all_font_names(&self) -> Vec { + unimplemented!() + } + fn all_font_families(&self) -> Vec { + unimplemented!() + } + fn font_id(&self, descriptor: &Font) -> Result { + unimplemented!() + } + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + unimplemented!() + } + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unimplemented!() + } + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unimplemented!() + } + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + unimplemented!() + } + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + unimplemented!() + } + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + unimplemented!() + } + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + unimplemented!() + } +} diff --git a/crates/ming/src/platform/test/window.rs b/crates/ming/src/platform/test/window.rs new file mode 100644 index 0000000..1b9654c --- /dev/null +++ b/crates/ming/src/platform/test/window.rs @@ -0,0 +1,319 @@ +use crate::{ + AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, + DispatchEventResult, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowParams, +}; +use collections::HashMap; +use parking_lot::Mutex; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use std::{ + rc::{Rc, Weak}, + sync::{self, Arc}, +}; + +pub(crate) struct TestWindowState { + pub(crate) bounds: Bounds, + pub(crate) handle: AnyWindowHandle, + display: Rc, + pub(crate) title: Option, + pub(crate) edited: bool, + platform: Weak, + sprite_atlas: Arc, + pub(crate) should_close_handler: Option bool>>, + input_callback: Option DispatchEventResult>>, + active_status_change_callback: Option>, + resize_callback: Option, f32)>>, + moved_callback: Option>, + input_handler: Option, + is_fullscreen: bool, +} + +#[derive(Clone)] +pub(crate) struct TestWindow(pub(crate) Arc>); + +impl HasWindowHandle for TestWindow { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unimplemented!("Test Windows are not backed by a real platform window") + } +} + +impl HasDisplayHandle for TestWindow { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unimplemented!("Test Windows are not backed by a real platform window") + } +} + +impl TestWindow { + pub fn new( + handle: AnyWindowHandle, + params: WindowParams, + platform: Weak, + display: Rc, + ) -> Self { + Self(Arc::new(Mutex::new(TestWindowState { + bounds: params.bounds, + display, + platform, + handle, + sprite_atlas: Arc::new(TestAtlas::new()), + title: Default::default(), + edited: false, + should_close_handler: None, + input_callback: None, + active_status_change_callback: None, + resize_callback: None, + moved_callback: None, + input_handler: None, + is_fullscreen: false, + }))) + } + + pub fn simulate_resize(&mut self, size: Size) { + let scale_factor = self.scale_factor(); + let mut lock = self.0.lock(); + let Some(mut callback) = lock.resize_callback.take() else { + return; + }; + lock.bounds.size = size.map(|pixels| (pixels.0 as i32).into()); + drop(lock); + callback(size, scale_factor); + self.0.lock().resize_callback = Some(callback); + } + + pub(crate) fn simulate_active_status_change(&self, active: bool) { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.active_status_change_callback.take() else { + return; + }; + drop(lock); + callback(active); + self.0.lock().active_status_change_callback = Some(callback); + } + + pub fn simulate_input(&mut self, event: PlatformInput) -> bool { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.input_callback.take() else { + return false; + }; + drop(lock); + let result = callback(event); + self.0.lock().input_callback = Some(callback); + !result.propagate + } +} + +impl PlatformWindow for TestWindow { + fn bounds(&self) -> Bounds { + self.0.lock().bounds + } + + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } + + fn is_maximized(&self) -> bool { + false + } + + fn content_size(&self) -> Size { + self.bounds().size.into() + } + + fn scale_factor(&self) -> f32 { + 2.0 + } + + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } + + fn display(&self) -> std::rc::Rc { + self.0.lock().display.clone() + } + + fn mouse_position(&self) -> Point { + Point::default() + } + + fn modifiers(&self) -> crate::Modifiers { + crate::Modifiers::default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.lock().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.lock().input_handler.take() + } + + fn prompt( + &self, + _level: crate::PromptLevel, + msg: &str, + detail: Option<&str>, + _answers: &[&str], + ) -> Option> { + Some( + self.0 + .lock() + .platform + .upgrade() + .expect("platform dropped") + .prompt(msg, detail), + ) + } + + fn activate(&self) { + self.0 + .lock() + .platform + .upgrade() + .unwrap() + .set_active_window(Some(self.clone())) + } + + fn is_active(&self) -> bool { + false + } + + fn set_title(&mut self, title: &str) { + self.0.lock().title = Some(title.to_owned()); + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) { + unimplemented!() + } + + fn set_edited(&mut self, edited: bool) { + self.0.lock().edited = edited; + } + + fn show_character_palette(&self) { + unimplemented!() + } + + fn minimize(&self) { + unimplemented!() + } + + fn zoom(&self) { + unimplemented!() + } + + fn toggle_fullscreen(&self) { + let mut lock = self.0.lock(); + lock.is_fullscreen = !lock.is_fullscreen; + } + + fn is_fullscreen(&self) -> bool { + self.0.lock().is_fullscreen + } + + fn on_request_frame(&self, _callback: Box) {} + + fn on_input(&self, callback: Box DispatchEventResult>) { + self.0.lock().input_callback = Some(callback) + } + + fn on_active_status_change(&self, callback: Box) { + self.0.lock().active_status_change_callback = Some(callback) + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.lock().resize_callback = Some(callback) + } + + fn on_moved(&self, callback: Box) { + self.0.lock().moved_callback = Some(callback) + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.lock().should_close_handler = Some(callback); + } + + fn on_close(&self, _callback: Box) {} + + fn on_appearance_changed(&self, _callback: Box) {} + + fn draw(&self, _scene: &crate::Scene) {} + + fn sprite_atlas(&self) -> sync::Arc { + self.0.lock().sprite_atlas.clone() + } + + fn as_test(&mut self) -> Option<&mut TestWindow> { + Some(self) + } + + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { + unimplemented!() + } +} + +pub(crate) struct TestAtlasState { + next_id: u32, + tiles: HashMap, +} + +pub(crate) struct TestAtlas(Mutex); + +impl TestAtlas { + pub fn new() -> Self { + TestAtlas(Mutex::new(TestAtlasState { + next_id: 0, + tiles: HashMap::default(), + })) + } +} + +impl PlatformAtlas for TestAtlas { + fn get_or_insert_with<'a>( + &self, + key: &crate::AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result<( + Size, + std::borrow::Cow<'a, [u8]>, + )>, + ) -> anyhow::Result { + let mut state = self.0.lock(); + if let Some(tile) = state.tiles.get(key) { + return Ok(tile.clone()); + } + + state.next_id += 1; + let texture_id = state.next_id; + state.next_id += 1; + let tile_id = state.next_id; + + drop(state); + let (size, _) = build()?; + let mut state = self.0.lock(); + + state.tiles.insert( + key.clone(), + crate::AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind: crate::AtlasTextureKind::Path, + }, + tile_id: TileId(tile_id), + padding: 0, + bounds: crate::Bounds { + origin: Point::default(), + size, + }, + }, + ); + + Ok(state.tiles[key].clone()) + } +} diff --git a/crates/ming/src/platform/windows.rs b/crates/ming/src/platform/windows.rs new file mode 100644 index 0000000..f8ea897 --- /dev/null +++ b/crates/ming/src/platform/windows.rs @@ -0,0 +1,19 @@ +mod direct_write; +mod dispatcher; +mod display; +mod events; +mod platform; +mod system_settings; +mod util; +mod window; + +pub(crate) use direct_write::*; +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use events::*; +pub(crate) use platform::*; +pub(crate) use system_settings::*; +pub(crate) use util::*; +pub(crate) use window::*; + +pub(crate) use windows::Win32::Foundation::HWND; diff --git a/crates/ming/src/platform/windows/direct_write.rs b/crates/ming/src/platform/windows/direct_write.rs new file mode 100644 index 0000000..1217d10 --- /dev/null +++ b/crates/ming/src/platform/windows/direct_write.rs @@ -0,0 +1,1341 @@ +use std::{borrow::Cow, sync::Arc}; + +use ::util::ResultExt; +use anyhow::{anyhow, Result}; +use collections::HashMap; +use itertools::Itertools; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use smallvec::SmallVec; +use windows::{ + core::*, + Foundation::Numerics::Matrix3x2, + Win32::{ + Foundation::*, + Globalization::GetUserDefaultLocaleName, + Graphics::{ + Direct2D::{Common::*, *}, + DirectWrite::*, + Dxgi::Common::*, + Gdi::LOGFONTW, + Imaging::{D2D::IWICImagingFactory2, *}, + }, + System::{Com::*, SystemServices::LOCALE_NAME_MAX_LENGTH}, + UI::WindowsAndMessaging::*, + }, +}; + +use crate::*; + +#[derive(Debug)] +struct FontInfo { + font_family: String, + font_face: IDWriteFontFace3, + features: IDWriteTypography, + is_system_font: bool, + is_emoji: bool, +} + +pub(crate) struct DirectWriteTextSystem(RwLock); + +struct DirectWriteComponent { + locale: String, + factory: IDWriteFactory5, + bitmap_factory: IWICImagingFactory2, + d2d1_factory: ID2D1Factory, + in_memory_loader: IDWriteInMemoryFontFileLoader, + builder: IDWriteFontSetBuilder1, + text_renderer: Arc, +} + +// All use of the IUnknown methods should be "thread-safe". +unsafe impl Sync for DirectWriteComponent {} +unsafe impl Send for DirectWriteComponent {} + +struct DirectWriteState { + components: DirectWriteComponent, + system_ui_font_name: SharedString, + system_font_collection: IDWriteFontCollection1, + custom_font_collection: IDWriteFontCollection1, + fonts: Vec, + font_selections: HashMap, + font_id_by_identifier: HashMap, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct FontIdentifier { + postscript_name: String, + weight: i32, + style: i32, +} + +impl DirectWriteComponent { + pub fn new() -> Result { + unsafe { + let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?; + let bitmap_factory: IWICImagingFactory2 = + CoCreateInstance(&CLSID_WICImagingFactory2, None, CLSCTX_INPROC_SERVER)?; + let d2d1_factory: ID2D1Factory = + D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)?; + // The `IDWriteInMemoryFontFileLoader` here is supported starting from + // Windows 10 Creators Update, which consequently requires the entire + // `DirectWriteTextSystem` to run on `win10 1703`+. + let in_memory_loader = factory.CreateInMemoryFontFileLoader()?; + factory.RegisterFontFileLoader(&in_memory_loader)?; + let builder = factory.CreateFontSetBuilder2()?; + let mut locale_vec = vec![0u16; LOCALE_NAME_MAX_LENGTH as usize]; + GetUserDefaultLocaleName(&mut locale_vec); + let locale = String::from_utf16_lossy(&locale_vec); + let text_renderer = Arc::new(TextRendererWrapper::new(&locale)); + + Ok(DirectWriteComponent { + locale, + factory, + bitmap_factory, + d2d1_factory, + in_memory_loader, + builder, + text_renderer, + }) + } + } +} + +impl DirectWriteTextSystem { + pub(crate) fn new() -> Result { + let components = DirectWriteComponent::new()?; + let system_font_collection = unsafe { + let mut result = std::mem::zeroed(); + components + .factory + .GetSystemFontCollection2(false, &mut result, true)?; + result.unwrap() + }; + let custom_font_set = unsafe { components.builder.CreateFontSet()? }; + let custom_font_collection = unsafe { + components + .factory + .CreateFontCollectionFromFontSet(&custom_font_set)? + }; + let system_ui_font_name = get_system_ui_font_name(); + + Ok(Self(RwLock::new(DirectWriteState { + components, + system_ui_font_name, + system_font_collection, + custom_font_collection, + fonts: Vec::new(), + font_selections: HashMap::default(), + font_id_by_identifier: HashMap::default(), + }))) + } +} + +impl PlatformTextSystem for DirectWriteTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } + + fn all_font_names(&self) -> Vec { + self.0.read().all_font_names() + } + + fn all_font_families(&self) -> Vec { + self.0.read().all_font_families() + } + + fn font_id(&self, font: &Font) -> Result { + let lock = self.0.upgradable_read(); + if let Some(font_id) = lock.font_selections.get(font) { + Ok(*font_id) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let font_id = lock.select_font(font); + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) + } + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + self.0.read().font_metrics(font_id) + } + + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + self.0.read().get_typographic_bounds(font_id, glyph_id) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result> { + self.0.read().get_advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn glyph_raster_bounds( + &self, + params: &RenderGlyphParams, + ) -> anyhow::Result> { + self.0.read().raster_bounds(params) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> anyhow::Result<(Size, Vec)> { + self.0.read().rasterize_glyph(params, raster_bounds) + } + + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + self.0.write().layout_line(text, font_size, runs) + } +} + +impl DirectWriteState { + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + for font_data in fonts { + match font_data { + Cow::Borrowed(data) => unsafe { + let font_file = self + .components + .in_memory_loader + .CreateInMemoryFontFileReference( + &self.components.factory, + data.as_ptr() as _, + data.len() as _, + None, + )?; + self.components.builder.AddFontFile(&font_file)?; + }, + Cow::Owned(data) => unsafe { + let font_file = self + .components + .in_memory_loader + .CreateInMemoryFontFileReference( + &self.components.factory, + data.as_ptr() as _, + data.len() as _, + None, + )?; + self.components.builder.AddFontFile(&font_file)?; + }, + } + } + let set = unsafe { self.components.builder.CreateFontSet()? }; + let collection = unsafe { + self.components + .factory + .CreateFontCollectionFromFontSet(&set)? + }; + self.custom_font_collection = collection; + + Ok(()) + } + + unsafe fn generate_font_features( + &self, + font_features: &FontFeatures, + ) -> Result { + let direct_write_features = self.components.factory.CreateTypography()?; + apply_font_features(&direct_write_features, font_features)?; + Ok(direct_write_features) + } + + unsafe fn get_font_id_from_font_collection( + &mut self, + family_name: &str, + font_weight: FontWeight, + font_style: FontStyle, + font_features: &FontFeatures, + is_system_font: bool, + ) -> Option { + let collection = if is_system_font { + &self.system_font_collection + } else { + &self.custom_font_collection + }; + let Some(fontset) = collection.GetFontSet().log_err() else { + return None; + }; + let Some(font) = fontset + .GetMatchingFonts( + &HSTRING::from(family_name), + font_weight.into(), + DWRITE_FONT_STRETCH_NORMAL, + font_style.into(), + ) + .log_err() + else { + return None; + }; + let total_number = font.GetFontCount(); + for index in 0..total_number { + let Some(font_face_ref) = font.GetFontFaceReference(index).log_err() else { + continue; + }; + let Some(font_face) = font_face_ref.CreateFontFace().log_err() else { + continue; + }; + let Some(identifier) = get_font_identifier(&font_face, &self.components.locale) else { + continue; + }; + let is_emoji = font_face.IsColorFont().as_bool(); + let Some(direct_write_features) = self.generate_font_features(font_features).log_err() + else { + continue; + }; + let font_info = FontInfo { + font_family: family_name.to_owned(), + font_face, + is_system_font, + features: direct_write_features, + is_emoji, + }; + let font_id = FontId(self.fonts.len()); + self.fonts.push(font_info); + self.font_id_by_identifier.insert(identifier, font_id); + return Some(font_id); + } + None + } + + unsafe fn update_system_font_collection(&mut self) { + let mut collection = std::mem::zeroed(); + self.components + .factory + .GetSystemFontCollection2(false, &mut collection, true) + .unwrap(); + self.system_font_collection = collection.unwrap(); + } + + fn select_font(&mut self, target_font: &Font) -> FontId { + unsafe { + if target_font.family == ".SystemUIFont" { + let family = self.system_ui_font_name.clone(); + self.find_font_id( + family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + ) + .unwrap() + } else { + self.find_font_id( + target_font.family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + ) + .unwrap_or_else(|| { + let family = self.system_ui_font_name.clone(); + log::error!("{} not found, use {} instead.", target_font.family, family); + self.get_font_id_from_font_collection( + family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + true, + ) + .unwrap() + }) + } + } + } + + unsafe fn find_font_id( + &mut self, + family_name: &str, + weight: FontWeight, + style: FontStyle, + features: &FontFeatures, + ) -> Option { + // try to find target font in custom font collection first + self.get_font_id_from_font_collection(family_name, weight, style, features, false) + .or_else(|| { + self.get_font_id_from_font_collection(family_name, weight, style, features, true) + }) + .or_else(|| { + self.update_system_font_collection(); + self.get_font_id_from_font_collection(family_name, weight, style, features, true) + }) + } + + fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + if font_runs.is_empty() { + return LineLayout { + font_size, + ..Default::default() + }; + } + unsafe { + let text_renderer = self.components.text_renderer.clone(); + let text_wide = text.encode_utf16().collect_vec(); + + let mut utf8_offset = 0usize; + let mut utf16_offset = 0u32; + let text_layout = { + let first_run = &font_runs[0]; + let font_info = &self.fonts[first_run.font_id.0]; + let collection = if font_info.is_system_font { + &self.system_font_collection + } else { + &self.custom_font_collection + }; + let format = self + .components + .factory + .CreateTextFormat( + &HSTRING::from(&font_info.font_family), + collection, + font_info.font_face.GetWeight(), + font_info.font_face.GetStyle(), + DWRITE_FONT_STRETCH_NORMAL, + font_size.0, + &HSTRING::from(&self.components.locale), + ) + .unwrap(); + + let layout = self + .components + .factory + .CreateTextLayout(&text_wide, &format, f32::INFINITY, f32::INFINITY) + .unwrap(); + let current_text = &text[utf8_offset..(utf8_offset + first_run.len)]; + utf8_offset += first_run.len; + let current_text_utf16_length = current_text.encode_utf16().count() as u32; + let text_range = DWRITE_TEXT_RANGE { + startPosition: utf16_offset, + length: current_text_utf16_length, + }; + layout + .SetTypography(&font_info.features, text_range) + .unwrap(); + utf16_offset += current_text_utf16_length; + + layout + }; + + let mut first_run = true; + let mut ascent = Pixels::default(); + let mut descent = Pixels::default(); + for run in font_runs { + if first_run { + first_run = false; + let mut metrics = vec![DWRITE_LINE_METRICS::default(); 4]; + let mut line_count = 0u32; + text_layout + .GetLineMetrics(Some(&mut metrics), &mut line_count as _) + .unwrap(); + ascent = px(metrics[0].baseline); + descent = px(metrics[0].height - metrics[0].baseline); + continue; + } + let font_info = &self.fonts[run.font_id.0]; + let current_text = &text[utf8_offset..(utf8_offset + run.len)]; + utf8_offset += run.len; + let current_text_utf16_length = current_text.encode_utf16().count() as u32; + + let collection = if font_info.is_system_font { + &self.system_font_collection + } else { + &self.custom_font_collection + }; + let text_range = DWRITE_TEXT_RANGE { + startPosition: utf16_offset, + length: current_text_utf16_length, + }; + utf16_offset += current_text_utf16_length; + text_layout + .SetFontCollection(collection, text_range) + .unwrap(); + text_layout + .SetFontFamilyName(&HSTRING::from(&font_info.font_family), text_range) + .unwrap(); + text_layout.SetFontSize(font_size.0, text_range).unwrap(); + text_layout + .SetFontStyle(font_info.font_face.GetStyle(), text_range) + .unwrap(); + text_layout + .SetFontWeight(font_info.font_face.GetWeight(), text_range) + .unwrap(); + text_layout + .SetTypography(&font_info.features, text_range) + .unwrap(); + } + + let mut runs = Vec::new(); + let renderer_context = RendererContext { + text_system: self, + index_converter: StringIndexConverter::new(text), + runs: &mut runs, + utf16_index: 0, + width: 0.0, + }; + text_layout + .Draw( + Some(&renderer_context as *const _ as _), + &text_renderer.0, + 0.0, + 0.0, + ) + .unwrap(); + let width = px(renderer_context.width); + + LineLayout { + font_size, + width, + ascent, + descent, + runs, + len: text.len(), + } + } + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + unsafe { + let font_info = &self.fonts[font_id.0]; + let mut metrics = std::mem::zeroed(); + font_info.font_face.GetMetrics2(&mut metrics); + + FontMetrics { + units_per_em: metrics.Base.designUnitsPerEm as _, + ascent: metrics.Base.ascent as _, + descent: -(metrics.Base.descent as f32), + line_gap: metrics.Base.lineGap as _, + underline_position: metrics.Base.underlinePosition as _, + underline_thickness: metrics.Base.underlineThickness as _, + cap_height: metrics.Base.capHeight as _, + x_height: metrics.Base.xHeight as _, + bounding_box: Bounds { + origin: Point { + x: metrics.glyphBoxLeft as _, + y: metrics.glyphBoxBottom as _, + }, + size: Size { + width: (metrics.glyphBoxRight - metrics.glyphBoxLeft) as _, + height: (metrics.glyphBoxTop - metrics.glyphBoxBottom) as _, + }, + }, + } + } + } + + unsafe fn get_glyphrun_analysis( + &self, + params: &RenderGlyphParams, + ) -> windows::core::Result { + let font = &self.fonts[params.font_id.0]; + let glyph_id = [params.glyph_id.0 as u16]; + let advance = [0.0f32]; + let offset = [DWRITE_GLYPH_OFFSET::default()]; + let glyph_run = DWRITE_GLYPH_RUN { + fontFace: unsafe { std::mem::transmute_copy(&font.font_face) }, + fontEmSize: params.font_size.0, + glyphCount: 1, + glyphIndices: glyph_id.as_ptr(), + glyphAdvances: advance.as_ptr(), + glyphOffsets: offset.as_ptr(), + isSideways: BOOL(0), + bidiLevel: 0, + }; + let transform = DWRITE_MATRIX { + m11: params.scale_factor, + m12: 0.0, + m21: 0.0, + m22: params.scale_factor, + dx: 0.0, + dy: 0.0, + }; + self.components.factory.CreateGlyphRunAnalysis( + &glyph_run as _, + 1.0, + Some(&transform as _), + DWRITE_RENDERING_MODE_NATURAL, + DWRITE_MEASURING_MODE_NATURAL, + 0.0, + 0.0, + ) + } + + fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + unsafe { + let glyph_run_analysis = self.get_glyphrun_analysis(params)?; + let bounds = glyph_run_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1)?; + + Ok(Bounds { + origin: Point { + x: DevicePixels(bounds.left), + y: DevicePixels(bounds.top), + }, + size: Size { + width: DevicePixels(bounds.right - bounds.left), + height: DevicePixels(bounds.bottom - bounds.top), + }, + }) + } + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + let font_info = &self.fonts[font_id.0]; + let codepoints = [ch as u32]; + let mut glyph_indices = vec![0u16; 1]; + unsafe { + font_info + .font_face + .GetGlyphIndices(codepoints.as_ptr(), 1, glyph_indices.as_mut_ptr()) + .log_err() + } + .map(|_| GlyphId(glyph_indices[0] as u32)) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { + if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { + return Err(anyhow!("glyph bounds are empty")); + } + + let font_info = &self.fonts[params.font_id.0]; + let glyph_id = [params.glyph_id.0 as u16]; + let advance = [glyph_bounds.size.width.0 as f32]; + let offset = [DWRITE_GLYPH_OFFSET { + advanceOffset: -glyph_bounds.origin.x.0 as f32 / params.scale_factor, + ascenderOffset: glyph_bounds.origin.y.0 as f32 / params.scale_factor, + }]; + let glyph_run = DWRITE_GLYPH_RUN { + fontFace: unsafe { std::mem::transmute_copy(&font_info.font_face) }, + fontEmSize: params.font_size.0, + glyphCount: 1, + glyphIndices: glyph_id.as_ptr(), + glyphAdvances: advance.as_ptr(), + glyphOffsets: offset.as_ptr(), + isSideways: BOOL(0), + bidiLevel: 0, + }; + + // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. + let mut bitmap_size = glyph_bounds.size; + if params.subpixel_variant.x > 0 { + bitmap_size.width += DevicePixels(1); + } + if params.subpixel_variant.y > 0 { + bitmap_size.height += DevicePixels(1); + } + let bitmap_size = bitmap_size; + let transform = DWRITE_MATRIX { + m11: params.scale_factor, + m12: 0.0, + m21: 0.0, + m22: params.scale_factor, + dx: 0.0, + dy: 0.0, + }; + let brush_property = D2D1_BRUSH_PROPERTIES { + opacity: 1.0, + transform: Matrix3x2 { + M11: params.scale_factor, + M12: 0.0, + M21: 0.0, + M22: params.scale_factor, + M31: 0.0, + M32: 0.0, + }, + }; + + let total_bytes; + let bitmap_format; + let render_target_property; + let bitmap_stride; + if params.is_emoji { + total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize * 4; + bitmap_format = &GUID_WICPixelFormat32bppPBGRA; + render_target_property = D2D1_RENDER_TARGET_PROPERTIES { + r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT, + pixelFormat: D2D1_PIXEL_FORMAT { + format: DXGI_FORMAT_B8G8R8A8_UNORM, + alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED, + }, + dpiX: params.scale_factor * 96.0, + dpiY: params.scale_factor * 96.0, + usage: D2D1_RENDER_TARGET_USAGE_NONE, + minLevel: D2D1_FEATURE_LEVEL_DEFAULT, + }; + bitmap_stride = bitmap_size.width.0 as u32 * 4; + } else { + total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize; + bitmap_format = &GUID_WICPixelFormat8bppAlpha; + render_target_property = D2D1_RENDER_TARGET_PROPERTIES { + r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT, + pixelFormat: D2D1_PIXEL_FORMAT { + format: DXGI_FORMAT_A8_UNORM, + alphaMode: D2D1_ALPHA_MODE_STRAIGHT, + }, + dpiX: params.scale_factor * 96.0, + dpiY: params.scale_factor * 96.0, + usage: D2D1_RENDER_TARGET_USAGE_NONE, + minLevel: D2D1_FEATURE_LEVEL_DEFAULT, + }; + bitmap_stride = bitmap_size.width.0 as u32; + } + + unsafe { + let bitmap = self.components.bitmap_factory.CreateBitmap( + bitmap_size.width.0 as u32, + bitmap_size.height.0 as u32, + bitmap_format, + WICBitmapCacheOnLoad, + )?; + let render_target = self + .components + .d2d1_factory + .CreateWicBitmapRenderTarget(&bitmap, &render_target_property)?; + let brush = render_target.CreateSolidColorBrush(&BRUSH_COLOR, Some(&brush_property))?; + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); + let baseline_origin = D2D_POINT_2F { + x: subpixel_shift.x / params.scale_factor, + y: subpixel_shift.y / params.scale_factor, + }; + + // This `cast()` action here should never fail since we are running on Win10+, and + // ID2D1DeviceContext4 requires Win8+ + let render_target = render_target.cast::().unwrap(); + render_target.BeginDraw(); + if params.is_emoji { + // WARN: only DWRITE_GLYPH_IMAGE_FORMATS_COLR has been tested + let enumerator = self.components.factory.TranslateColorGlyphRun2( + baseline_origin, + &glyph_run as _, + None, + DWRITE_GLYPH_IMAGE_FORMATS_COLR + | DWRITE_GLYPH_IMAGE_FORMATS_SVG + | DWRITE_GLYPH_IMAGE_FORMATS_PNG + | DWRITE_GLYPH_IMAGE_FORMATS_JPEG + | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8, + DWRITE_MEASURING_MODE_NATURAL, + Some(&transform as _), + 0, + )?; + while enumerator.MoveNext().is_ok() { + let Ok(color_glyph) = enumerator.GetCurrentRun2() else { + break; + }; + let color_glyph = &*color_glyph; + let brush_color = translate_color(&color_glyph.Base.runColor); + brush.SetColor(&brush_color); + match color_glyph.glyphImageFormat { + DWRITE_GLYPH_IMAGE_FORMATS_PNG + | DWRITE_GLYPH_IMAGE_FORMATS_JPEG + | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8 => render_target + .DrawColorBitmapGlyphRun( + color_glyph.glyphImageFormat, + baseline_origin, + &color_glyph.Base.glyphRun, + color_glyph.measuringMode, + D2D1_COLOR_BITMAP_GLYPH_SNAP_OPTION_DEFAULT, + ), + DWRITE_GLYPH_IMAGE_FORMATS_SVG => render_target.DrawSvgGlyphRun( + baseline_origin, + &color_glyph.Base.glyphRun, + &brush, + None, + color_glyph.Base.paletteIndex as u32, + color_glyph.measuringMode, + ), + _ => render_target.DrawGlyphRun2( + baseline_origin, + &color_glyph.Base.glyphRun, + Some(color_glyph.Base.glyphRunDescription as *const _), + &brush, + color_glyph.measuringMode, + ), + } + } + } else { + render_target.DrawGlyphRun( + baseline_origin, + &glyph_run, + &brush, + DWRITE_MEASURING_MODE_NATURAL, + ); + } + render_target.EndDraw(None, None)?; + let mut raw_data = vec![0u8; total_bytes]; + bitmap.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?; + if params.is_emoji { + // Convert from BGRA with premultiplied alpha to BGRA with straight alpha. + for pixel in raw_data.chunks_exact_mut(4) { + let a = pixel[3] as f32 / 255.; + pixel[0] = (pixel[0] as f32 / a) as u8; + pixel[1] = (pixel[1] as f32 / a) as u8; + pixel[2] = (pixel[2] as f32 / a) as u8; + } + } + Ok((bitmap_size, raw_data)) + } + } + + fn get_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unsafe { + let font = &self.fonts[font_id.0].font_face; + let glyph_indices = [glyph_id.0 as u16]; + let mut metrics = [DWRITE_GLYPH_METRICS::default()]; + font.GetDesignGlyphMetrics(glyph_indices.as_ptr(), 1, metrics.as_mut_ptr(), false)?; + + let metrics = &metrics[0]; + let advance_width = metrics.advanceWidth as i32; + let advance_height = metrics.advanceHeight as i32; + let left_side_bearing = metrics.leftSideBearing; + let right_side_bearing = metrics.rightSideBearing; + let top_side_bearing = metrics.topSideBearing; + let bottom_side_bearing = metrics.bottomSideBearing; + let vertical_origin_y = metrics.verticalOriginY; + + let y_offset = vertical_origin_y + bottom_side_bearing - advance_height; + let width = advance_width - (left_side_bearing + right_side_bearing); + let height = advance_height - (top_side_bearing + bottom_side_bearing); + + Ok(Bounds { + origin: Point { + x: left_side_bearing as f32, + y: y_offset as f32, + }, + size: Size { + width: width as f32, + height: height as f32, + }, + }) + } + } + + fn get_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unsafe { + let font = &self.fonts[font_id.0].font_face; + let glyph_indices = [glyph_id.0 as u16]; + let mut metrics = [DWRITE_GLYPH_METRICS::default()]; + font.GetDesignGlyphMetrics(glyph_indices.as_ptr(), 1, metrics.as_mut_ptr(), false)?; + + let metrics = &metrics[0]; + + Ok(Size { + width: metrics.advanceWidth as f32, + height: 0.0, + }) + } + } + + fn all_font_names(&self) -> Vec { + let mut result = + get_font_names_from_collection(&self.system_font_collection, &self.components.locale); + result.extend(get_font_names_from_collection( + &self.custom_font_collection, + &self.components.locale, + )); + result + } + + fn all_font_families(&self) -> Vec { + get_font_names_from_collection(&self.system_font_collection, &self.components.locale) + } +} + +impl Drop for DirectWriteState { + fn drop(&mut self) { + unsafe { + let _ = self + .components + .factory + .UnregisterFontFileLoader(&self.components.in_memory_loader); + } + } +} + +struct TextRendererWrapper(pub IDWriteTextRenderer); + +impl TextRendererWrapper { + pub fn new(locale_str: &str) -> Self { + let inner = TextRenderer::new(locale_str); + TextRendererWrapper(inner.into()) + } +} + +#[implement(IDWriteTextRenderer)] +struct TextRenderer { + locale: String, +} + +impl TextRenderer { + pub fn new(locale_str: &str) -> Self { + TextRenderer { + locale: locale_str.to_owned(), + } + } +} + +struct RendererContext<'t, 'a, 'b> { + text_system: &'t mut DirectWriteState, + index_converter: StringIndexConverter<'a>, + runs: &'b mut Vec, + utf16_index: usize, + width: f32, +} + +#[allow(non_snake_case)] +impl IDWritePixelSnapping_Impl for TextRenderer { + fn IsPixelSnappingDisabled( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + ) -> windows::core::Result { + Ok(BOOL(0)) + } + + fn GetCurrentTransform( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + transform: *mut DWRITE_MATRIX, + ) -> windows::core::Result<()> { + unsafe { + *transform = DWRITE_MATRIX { + m11: 1.0, + m12: 0.0, + m21: 0.0, + m22: 1.0, + dx: 0.0, + dy: 0.0, + }; + } + Ok(()) + } + + fn GetPixelsPerDip( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + ) -> windows::core::Result { + Ok(1.0) + } +} + +#[allow(non_snake_case)] +impl IDWriteTextRenderer_Impl for TextRenderer { + fn DrawGlyphRun( + &self, + clientdrawingcontext: *const ::core::ffi::c_void, + _baselineoriginx: f32, + _baselineoriginy: f32, + _measuringmode: DWRITE_MEASURING_MODE, + glyphrun: *const DWRITE_GLYPH_RUN, + glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION, + _clientdrawingeffect: Option<&windows::core::IUnknown>, + ) -> windows::core::Result<()> { + unsafe { + let glyphrun = &*glyphrun; + let glyph_count = glyphrun.glyphCount as usize; + if glyph_count == 0 { + return Ok(()); + } + let desc = &*glyphrundescription; + let utf16_length_per_glyph = desc.stringLength as usize / glyph_count; + let context = + &mut *(clientdrawingcontext as *const RendererContext as *mut RendererContext); + + if glyphrun.fontFace.is_none() { + return Ok(()); + } + + let font_face = glyphrun.fontFace.as_ref().unwrap(); + // This `cast()` action here should never fail since we are running on Win10+, and + // `IDWriteFontFace3` requires Win10 + let font_face = &font_face.cast::().unwrap(); + let Some((font_identifier, font_struct, is_emoji)) = + get_font_identifier_and_font_struct(font_face, &self.locale) + else { + log::error!("none postscript name found"); + return Ok(()); + }; + + let font_id = if let Some(id) = context + .text_system + .font_id_by_identifier + .get(&font_identifier) + { + *id + } else { + context.text_system.select_font(&font_struct) + }; + let mut glyphs = SmallVec::new(); + for index in 0..glyph_count { + let id = GlyphId(*glyphrun.glyphIndices.add(index) as u32); + context + .index_converter + .advance_to_utf16_ix(context.utf16_index); + glyphs.push(ShapedGlyph { + id, + position: point(px(context.width), px(0.0)), + index: context.index_converter.utf8_ix, + is_emoji, + }); + context.utf16_index += utf16_length_per_glyph; + context.width += *glyphrun.glyphAdvances.add(index); + } + context.runs.push(ShapedRun { font_id, glyphs }); + } + Ok(()) + } + + fn DrawUnderline( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + _baselineoriginx: f32, + _baselineoriginy: f32, + _underline: *const DWRITE_UNDERLINE, + _clientdrawingeffect: Option<&windows::core::IUnknown>, + ) -> windows::core::Result<()> { + Err(windows::core::Error::new( + E_NOTIMPL, + "DrawUnderline unimplemented", + )) + } + + fn DrawStrikethrough( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + _baselineoriginx: f32, + _baselineoriginy: f32, + _strikethrough: *const DWRITE_STRIKETHROUGH, + _clientdrawingeffect: Option<&windows::core::IUnknown>, + ) -> windows::core::Result<()> { + Err(windows::core::Error::new( + E_NOTIMPL, + "DrawStrikethrough unimplemented", + )) + } + + fn DrawInlineObject( + &self, + _clientdrawingcontext: *const ::core::ffi::c_void, + _originx: f32, + _originy: f32, + _inlineobject: Option<&IDWriteInlineObject>, + _issideways: BOOL, + _isrighttoleft: BOOL, + _clientdrawingeffect: Option<&windows::core::IUnknown>, + ) -> windows::core::Result<()> { + Err(windows::core::Error::new( + E_NOTIMPL, + "DrawInlineObject unimplemented", + )) + } +} + +struct StringIndexConverter<'a> { + text: &'a str, + utf8_ix: usize, + utf16_ix: usize, +} + +impl<'a> StringIndexConverter<'a> { + fn new(text: &'a str) -> Self { + Self { + text, + utf8_ix: 0, + utf16_ix: 0, + } + } + + fn advance_to_utf8_ix(&mut self, utf8_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf8_ix + ix >= utf8_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } + + fn advance_to_utf16_ix(&mut self, utf16_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf16_ix >= utf16_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } +} + +impl Into for FontStyle { + fn into(self) -> DWRITE_FONT_STYLE { + match self { + FontStyle::Normal => DWRITE_FONT_STYLE_NORMAL, + FontStyle::Italic => DWRITE_FONT_STYLE_ITALIC, + FontStyle::Oblique => DWRITE_FONT_STYLE_OBLIQUE, + } + } +} + +impl From for FontStyle { + fn from(value: DWRITE_FONT_STYLE) -> Self { + match value.0 { + 0 => FontStyle::Normal, + 1 => FontStyle::Italic, + 2 => FontStyle::Oblique, + _ => unreachable!(), + } + } +} + +impl Into for FontWeight { + fn into(self) -> DWRITE_FONT_WEIGHT { + DWRITE_FONT_WEIGHT(self.0 as i32) + } +} + +impl From for FontWeight { + fn from(value: DWRITE_FONT_WEIGHT) -> Self { + FontWeight(value.0 as f32) + } +} + +fn get_font_names_from_collection( + collection: &IDWriteFontCollection1, + locale: &str, +) -> Vec { + unsafe { + let mut result = Vec::new(); + let family_count = collection.GetFontFamilyCount(); + for index in 0..family_count { + let Some(font_family) = collection.GetFontFamily(index).log_err() else { + continue; + }; + let Some(localized_family_name) = font_family.GetFamilyNames().log_err() else { + continue; + }; + let Some(family_name) = get_name(localized_family_name, locale) else { + continue; + }; + result.push(family_name); + } + + result + } +} + +fn get_font_identifier_and_font_struct( + font_face: &IDWriteFontFace3, + locale: &str, +) -> Option<(FontIdentifier, Font, bool)> { + let Some(postscript_name) = get_postscript_name(font_face, locale) else { + return None; + }; + let Some(localized_family_name) = (unsafe { font_face.GetFamilyNames().log_err() }) else { + return None; + }; + let Some(family_name) = get_name(localized_family_name, locale) else { + return None; + }; + let weight = unsafe { font_face.GetWeight() }; + let style = unsafe { font_face.GetStyle() }; + let identifier = FontIdentifier { + postscript_name, + weight: weight.0, + style: style.0, + }; + let font_struct = Font { + family: family_name.into(), + features: FontFeatures::default(), + weight: weight.into(), + style: style.into(), + }; + let is_emoji = unsafe { font_face.IsColorFont().as_bool() }; + Some((identifier, font_struct, is_emoji)) +} + +#[inline] +fn get_font_identifier(font_face: &IDWriteFontFace3, locale: &str) -> Option { + let weight = unsafe { font_face.GetWeight().0 }; + let style = unsafe { font_face.GetStyle().0 }; + get_postscript_name(font_face, locale).map(|postscript_name| FontIdentifier { + postscript_name, + weight, + style, + }) +} + +#[inline] +fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option { + let mut info = None; + let mut exists = BOOL(0); + unsafe { + font_face + .GetInformationalStrings( + DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME, + &mut info, + &mut exists, + ) + .log_err(); + } + if !exists.as_bool() || info.is_none() { + return None; + } + + get_name(info.unwrap(), locale) +} + +// https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_feature_tag +fn apply_font_features( + direct_write_features: &IDWriteTypography, + features: &FontFeatures, +) -> Result<()> { + let tag_values = features.tag_value_list(); + if tag_values.is_empty() { + return Ok(()); + } + + // All of these features are enabled by default by DirectWrite. + // If you want to (and can) peek into the source of DirectWrite + let mut feature_liga = make_direct_write_feature("liga", true); + let mut feature_clig = make_direct_write_feature("clig", true); + let mut feature_calt = make_direct_write_feature("calt", true); + + for (tag, enable) in tag_values { + if tag == *"liga" && !enable { + feature_liga.parameter = 0; + continue; + } + if tag == *"clig" && !enable { + feature_clig.parameter = 0; + continue; + } + if tag == *"calt" && !enable { + feature_calt.parameter = 0; + continue; + } + + unsafe { + direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?; + } + } + unsafe { + direct_write_features.AddFontFeature(feature_liga)?; + direct_write_features.AddFontFeature(feature_clig)?; + direct_write_features.AddFontFeature(feature_calt)?; + } + + Ok(()) +} + +#[inline] +fn make_direct_write_feature(feature_name: &str, enable: bool) -> DWRITE_FONT_FEATURE { + let tag = make_direct_write_tag(feature_name); + if enable { + DWRITE_FONT_FEATURE { + nameTag: tag, + parameter: 1, + } + } else { + DWRITE_FONT_FEATURE { + nameTag: tag, + parameter: 0, + } + } +} + +#[inline] +fn make_open_type_tag(tag_name: &str) -> u32 { + assert_eq!(tag_name.chars().count(), 4); + let bytes = tag_name.bytes().collect_vec(); + ((bytes[3] as u32) << 24) + | ((bytes[2] as u32) << 16) + | ((bytes[1] as u32) << 8) + | (bytes[0] as u32) +} + +#[inline] +fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG { + DWRITE_FONT_FEATURE_TAG(make_open_type_tag(tag_name)) +} + +#[inline] +fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Option { + let mut locale_name_index = 0u32; + let mut exists = BOOL(0); + unsafe { + string + .FindLocaleName( + &HSTRING::from(locale), + &mut locale_name_index, + &mut exists as _, + ) + .log_err(); + } + if !exists.as_bool() { + unsafe { + string + .FindLocaleName( + DEFAULT_LOCALE_NAME, + &mut locale_name_index as _, + &mut exists as _, + ) + .log_err(); + } + if !exists.as_bool() { + return None; + } + } + + let name_length = unsafe { string.GetStringLength(locale_name_index).unwrap() } as usize; + let mut name_vec = vec![0u16; name_length + 1]; + unsafe { + string.GetString(locale_name_index, &mut name_vec).unwrap(); + } + + Some(String::from_utf16_lossy(&name_vec[..name_length])) +} + +#[inline] +fn translate_color(color: &DWRITE_COLOR_F) -> D2D1_COLOR_F { + D2D1_COLOR_F { + r: color.r, + g: color.g, + b: color.b, + a: color.a, + } +} + +fn get_system_ui_font_name() -> SharedString { + unsafe { + let mut info: LOGFONTW = std::mem::zeroed(); + let font_family = if SystemParametersInfoW( + SPI_GETICONTITLELOGFONT, + std::mem::size_of::() as u32, + Some(&mut info as *mut _ as _), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ) + .log_err() + .is_none() + { + // https://learn.microsoft.com/en-us/windows/win32/uxguide/vis-fonts + // Segoe UI is the Windows font intended for user interface text strings. + "Segoe UI".into() + } else { + let font_name = String::from_utf16_lossy(&info.lfFaceName); + font_name.trim_matches(char::from(0)).to_owned().into() + }; + log::info!("Use {} as UI font.", font_family); + font_family + } +} + +const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US"); +const BRUSH_COLOR: D2D1_COLOR_F = D2D1_COLOR_F { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, +}; diff --git a/crates/ming/src/platform/windows/dispatcher.rs b/crates/ming/src/platform/windows/dispatcher.rs new file mode 100644 index 0000000..724b83a --- /dev/null +++ b/crates/ming/src/platform/windows/dispatcher.rs @@ -0,0 +1,137 @@ +use std::{ + thread::{current, ThreadId}, + time::Duration, +}; + +use async_task::Runnable; +use parking::Parker; +use parking_lot::Mutex; +use util::ResultExt; +use windows::{ + Foundation::TimeSpan, + System::{ + DispatcherQueue, DispatcherQueueController, DispatcherQueueHandler, + Threading::{ + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, + WorkItemPriority, + }, + }, + Win32::System::WinRT::{ + CreateDispatcherQueueController, DispatcherQueueOptions, DQTAT_COM_NONE, + DQTYPE_THREAD_CURRENT, + }, +}; + +use crate::{PlatformDispatcher, TaskLabel}; + +pub(crate) struct WindowsDispatcher { + controller: DispatcherQueueController, + main_queue: DispatcherQueue, + parker: Mutex, + main_thread_id: ThreadId, +} + +unsafe impl Send for WindowsDispatcher {} +unsafe impl Sync for WindowsDispatcher {} + +impl WindowsDispatcher { + pub(crate) fn new() -> Self { + let controller = unsafe { + let options = DispatcherQueueOptions { + dwSize: std::mem::size_of::() as u32, + threadType: DQTYPE_THREAD_CURRENT, + apartmentType: DQTAT_COM_NONE, + }; + CreateDispatcherQueueController(options).unwrap() + }; + let main_queue = controller.DispatcherQueue().unwrap(); + let parker = Mutex::new(Parker::new()); + let main_thread_id = current().id(); + + WindowsDispatcher { + controller, + main_queue, + parker, + main_thread_id, + } + } + + fn dispatch_on_threadpool(&self, runnable: Runnable) { + let handler = { + let mut task_wrapper = Some(runnable); + WorkItemHandler::new(move |_| { + task_wrapper.take().unwrap().run(); + Ok(()) + }) + }; + ThreadPool::RunWithPriorityAndOptionsAsync( + &handler, + WorkItemPriority::High, + WorkItemOptions::TimeSliced, + ) + .log_err(); + } + + fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { + let handler = { + let mut task_wrapper = Some(runnable); + TimerElapsedHandler::new(move |_| { + task_wrapper.take().unwrap().run(); + Ok(()) + }) + }; + let delay = TimeSpan { + // A time period expressed in 100-nanosecond units. + // 10,000,000 ticks per second + Duration: (duration.as_nanos() / 100) as i64, + }; + ThreadPoolTimer::CreateTimer(&handler, delay).log_err(); + } +} + +impl Drop for WindowsDispatcher { + fn drop(&mut self) { + self.controller.ShutdownQueueAsync().log_err(); + } +} + +impl PlatformDispatcher for WindowsDispatcher { + fn is_main_thread(&self) -> bool { + current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: Runnable, label: Option) { + self.dispatch_on_threadpool(runnable); + if let Some(label) = label { + log::debug!("TaskLabel: {label:?}"); + } + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + let handler = { + let mut task_wrapper = Some(runnable); + DispatcherQueueHandler::new(move || { + task_wrapper.take().unwrap().run(); + Ok(()) + }) + }; + self.main_queue.TryEnqueue(&handler).log_err(); + } + + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + self.dispatch_on_threadpool_after(runnable, duration); + } + + fn park(&self, timeout: Option) -> bool { + if let Some(timeout) = timeout { + self.parker.lock().park_timeout(timeout) + } else { + self.parker.lock().park(); + true + } + } + + fn unparker(&self) -> parking::Unparker { + self.parker.lock().unparker() + } +} diff --git a/crates/ming/src/platform/windows/display.rs b/crates/ming/src/platform/windows/display.rs new file mode 100644 index 0000000..17d0a5b --- /dev/null +++ b/crates/ming/src/platform/windows/display.rs @@ -0,0 +1,199 @@ +use itertools::Itertools; +use smallvec::SmallVec; +use std::rc::Rc; +use uuid::Uuid; +use windows::{ + core::*, + Win32::{Foundation::*, Graphics::Gdi::*}, +}; + +use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Point, Size}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct WindowsDisplay { + pub handle: HMONITOR, + pub display_id: DisplayId, + bounds: Bounds, + uuid: Uuid, +} + +impl WindowsDisplay { + pub(crate) fn new(display_id: DisplayId) -> Option { + let Some(screen) = available_monitors().into_iter().nth(display_id.0 as _) else { + return None; + }; + let Ok(info) = get_monitor_info(screen).inspect_err(|e| log::error!("{}", e)) else { + return None; + }; + let size = info.monitorInfo.rcMonitor; + let uuid = generate_uuid(&info.szDevice); + + Some(WindowsDisplay { + handle: screen, + display_id, + bounds: Bounds { + origin: Point { + x: DevicePixels(size.left), + y: DevicePixels(size.top), + }, + size: Size { + width: DevicePixels(size.right - size.left), + height: DevicePixels(size.bottom - size.top), + }, + }, + uuid, + }) + } + + pub fn new_with_handle(monitor: HMONITOR) -> Self { + let info = get_monitor_info(monitor).expect("unable to get monitor info"); + let size = info.monitorInfo.rcMonitor; + let uuid = generate_uuid(&info.szDevice); + let display_id = available_monitors() + .iter() + .position(|handle| handle.0 == monitor.0) + .unwrap(); + + WindowsDisplay { + handle: monitor, + display_id: DisplayId(display_id as _), + bounds: Bounds { + origin: Point { + x: DevicePixels(size.left as i32), + y: DevicePixels(size.top as i32), + }, + size: Size { + width: DevicePixels((size.right - size.left) as i32), + height: DevicePixels((size.bottom - size.top) as i32), + }, + }, + uuid, + } + } + + fn new_with_handle_and_id(handle: HMONITOR, display_id: DisplayId) -> Self { + let info = get_monitor_info(handle).expect("unable to get monitor info"); + let size = info.monitorInfo.rcMonitor; + let uuid = generate_uuid(&info.szDevice); + + WindowsDisplay { + handle, + display_id, + bounds: Bounds { + origin: Point { + x: DevicePixels(size.left as i32), + y: DevicePixels(size.top as i32), + }, + size: Size { + width: DevicePixels((size.right - size.left) as i32), + height: DevicePixels((size.bottom - size.top) as i32), + }, + }, + uuid, + } + } + + pub fn primary_monitor() -> Option { + // https://devblogs.microsoft.com/oldnewthing/20070809-00/?p=25643 + const POINT_ZERO: POINT = POINT { x: 0, y: 0 }; + let monitor = unsafe { MonitorFromPoint(POINT_ZERO, MONITOR_DEFAULTTOPRIMARY) }; + if monitor.is_invalid() { + log::error!( + "can not find the primary monitor: {}", + std::io::Error::last_os_error() + ); + return None; + } + Some(WindowsDisplay::new_with_handle(monitor)) + } + + pub fn displays() -> Vec> { + available_monitors() + .into_iter() + .enumerate() + .map(|(id, handle)| { + Rc::new(WindowsDisplay::new_with_handle_and_id( + handle, + DisplayId(id as _), + )) as Rc + }) + .collect() + } + + pub(crate) fn frequency(&self) -> Option { + get_monitor_info(self.handle).ok().and_then(|info| { + let mut devmode = DEVMODEW::default(); + unsafe { + EnumDisplaySettingsW( + PCWSTR(info.szDevice.as_ptr()), + ENUM_CURRENT_SETTINGS, + &mut devmode, + ) + } + .as_bool() + .then(|| devmode.dmDisplayFrequency) + }) + } +} + +impl PlatformDisplay for WindowsDisplay { + fn id(&self) -> DisplayId { + self.display_id + } + + fn uuid(&self) -> anyhow::Result { + Ok(self.uuid) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} + +fn available_monitors() -> SmallVec<[HMONITOR; 4]> { + let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new(); + unsafe { + EnumDisplayMonitors( + HDC::default(), + None, + Some(monitor_enum_proc), + LPARAM(&mut monitors as *mut _ as _), + ); + } + monitors +} + +unsafe extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _place: *mut RECT, + data: LPARAM, +) -> BOOL { + let monitors = data.0 as *mut SmallVec<[HMONITOR; 4]>; + unsafe { (*monitors).push(hmonitor) }; + BOOL(1) +} + +fn get_monitor_info(hmonitor: HMONITOR) -> anyhow::Result { + let mut monitor_info: MONITORINFOEXW = unsafe { std::mem::zeroed() }; + monitor_info.monitorInfo.cbSize = std::mem::size_of::() as u32; + let status = unsafe { + GetMonitorInfoW( + hmonitor, + &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO, + ) + }; + if status.as_bool() { + Ok(monitor_info) + } else { + Err(anyhow::anyhow!(std::io::Error::last_os_error())) + } +} + +fn generate_uuid(device_name: &[u16]) -> Uuid { + let name = device_name + .iter() + .flat_map(|&a| a.to_be_bytes().to_vec()) + .collect_vec(); + Uuid::new_v5(&Uuid::NAMESPACE_DNS, &name) +} diff --git a/crates/ming/src/platform/windows/events.rs b/crates/ming/src/platform/windows/events.rs new file mode 100644 index 0000000..b79ddce --- /dev/null +++ b/crates/ming/src/platform/windows/events.rs @@ -0,0 +1,1274 @@ +use std::rc::Rc; + +use ::util::ResultExt; +use anyhow::Context; +use windows::Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::SystemServices::*, + UI::{ + HiDpi::*, + Input::{Ime::*, KeyboardAndMouse::*}, + WindowsAndMessaging::*, + }, +}; + +use crate::*; + +pub(crate) const CURSOR_STYLE_CHANGED: u32 = WM_USER + 1; +pub(crate) const MOUSE_WHEEL_SETTINGS_CHANGED: u32 = WM_USER + 2; +pub(crate) const MOUSE_WHEEL_SETTINGS_SCROLL_CHARS_CHANGED: isize = 1; +pub(crate) const MOUSE_WHEEL_SETTINGS_SCROLL_LINES_CHANGED: isize = 2; +pub(crate) const CLOSE_ONE_WINDOW: u32 = WM_USER + 3; +const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; + +pub(crate) fn handle_msg( + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> LRESULT { + let handled = match msg { + WM_ACTIVATE => handle_activate_msg(handle, wparam, state_ptr), + WM_CREATE => handle_create_msg(handle, state_ptr), + WM_MOVE => handle_move_msg(handle, lparam, state_ptr), + WM_SIZE => handle_size_msg(lparam, state_ptr), + WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle), + WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle), + WM_TIMER => handle_timer_msg(handle, wparam, state_ptr), + WM_NCCALCSIZE => handle_calc_client_size(handle, wparam, lparam, state_ptr), + WM_DPICHANGED => handle_dpi_changed_msg(handle, wparam, lparam, state_ptr), + WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr), + WM_PAINT => handle_paint_msg(handle, state_ptr), + WM_CLOSE => handle_close_msg(state_ptr), + WM_DESTROY => handle_destroy_msg(handle, state_ptr), + WM_MOUSEMOVE => handle_mouse_move_msg(lparam, wparam, state_ptr), + WM_NCMOUSEMOVE => handle_nc_mouse_move_msg(handle, lparam, state_ptr), + WM_NCLBUTTONDOWN => { + handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) + } + WM_NCRBUTTONDOWN => { + handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) + } + WM_NCMBUTTONDOWN => { + handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) + } + WM_NCLBUTTONUP => { + handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) + } + WM_NCRBUTTONUP => { + handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) + } + WM_NCMBUTTONUP => { + handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) + } + WM_LBUTTONDOWN => handle_mouse_down_msg(MouseButton::Left, lparam, state_ptr), + WM_RBUTTONDOWN => handle_mouse_down_msg(MouseButton::Right, lparam, state_ptr), + WM_MBUTTONDOWN => handle_mouse_down_msg(MouseButton::Middle, lparam, state_ptr), + WM_XBUTTONDOWN => handle_xbutton_msg(wparam, lparam, handle_mouse_down_msg, state_ptr), + WM_LBUTTONUP => handle_mouse_up_msg(MouseButton::Left, lparam, state_ptr), + WM_RBUTTONUP => handle_mouse_up_msg(MouseButton::Right, lparam, state_ptr), + WM_MBUTTONUP => handle_mouse_up_msg(MouseButton::Middle, lparam, state_ptr), + WM_XBUTTONUP => handle_xbutton_msg(wparam, lparam, handle_mouse_up_msg, state_ptr), + WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr), + WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr), + WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr), + WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, state_ptr), + WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr), + WM_KEYUP => handle_keyup_msg(handle, wparam, state_ptr), + WM_CHAR => handle_char_msg(handle, wparam, lparam, state_ptr), + WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr), + WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr), + WM_SETCURSOR => handle_set_cursor(lparam, state_ptr), + CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr), + MOUSE_WHEEL_SETTINGS_CHANGED => handle_mouse_wheel_settings_msg(wparam, lparam, state_ptr), + _ => None, + }; + if let Some(n) = handled { + LRESULT(n) + } else { + unsafe { DefWindowProcW(handle, msg, wparam, lparam) } + } +} + +fn handle_move_msg( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let x = lparam.signed_loword() as i32; + let y = lparam.signed_hiword() as i32; + let mut lock = state_ptr.state.borrow_mut(); + lock.origin = point(x.into(), y.into()); + let size = lock.physical_size; + let center_x = x + size.width.0 / 2; + let center_y = y + size.height.0 / 2; + let monitor_bounds = lock.display.bounds(); + if center_x < monitor_bounds.left().0 + || center_x > monitor_bounds.right().0 + || center_y < monitor_bounds.top().0 + || center_y > monitor_bounds.bottom().0 + { + // center of the window may have moved to another monitor + let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + if !monitor.is_invalid() && lock.display.handle != monitor { + // we will get the same monitor if we only have one + lock.display = WindowsDisplay::new_with_handle(monitor); + } + } + if let Some(mut callback) = lock.callbacks.moved.take() { + drop(lock); + callback(); + state_ptr.state.borrow_mut().callbacks.moved = Some(callback); + } + Some(0) +} + +fn handle_size_msg(lparam: LPARAM, state_ptr: Rc) -> Option { + let width = lparam.loword().max(1) as i32; + let height = lparam.hiword().max(1) as i32; + let new_physical_size = size(width.into(), height.into()); + let mut lock = state_ptr.state.borrow_mut(); + let scale_factor = lock.scale_factor; + lock.physical_size = new_physical_size; + lock.renderer.update_drawable_size(Size { + width: width as f64, + height: height as f64, + }); + if let Some(mut callback) = lock.callbacks.resize.take() { + drop(lock); + let logical_size = logical_size(new_physical_size, scale_factor); + callback(logical_size, scale_factor); + state_ptr.state.borrow_mut().callbacks.resize = Some(callback); + } + Some(0) +} + +fn handle_size_move_loop(handle: HWND) -> Option { + unsafe { + let ret = SetTimer(handle, SIZE_MOVE_LOOP_TIMER_ID, USER_TIMER_MINIMUM, None); + if ret == 0 { + log::error!( + "unable to create timer: {}", + std::io::Error::last_os_error() + ); + } + } + None +} + +fn handle_size_move_loop_exit(handle: HWND) -> Option { + unsafe { + KillTimer(handle, SIZE_MOVE_LOOP_TIMER_ID).log_err(); + } + None +} + +fn handle_timer_msg( + handle: HWND, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { + handle_paint_msg(handle, state_ptr) + } else { + None + } +} + +fn handle_paint_msg(handle: HWND, state_ptr: Rc) -> Option { + let mut paint_struct = PAINTSTRUCT::default(); + let _hdc = unsafe { BeginPaint(handle, &mut paint_struct) }; + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut request_frame) = lock.callbacks.request_frame.take() { + drop(lock); + request_frame(); + state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame); + } + unsafe { EndPaint(handle, &paint_struct) }; + Some(0) +} + +fn handle_close_msg(state_ptr: Rc) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.should_close.take() { + drop(lock); + let should_close = callback(); + state_ptr.state.borrow_mut().callbacks.should_close = Some(callback); + if should_close { + None + } else { + Some(0) + } + } else { + None + } +} + +fn handle_destroy_msg(handle: HWND, state_ptr: Rc) -> Option { + let callback = { + let mut lock = state_ptr.state.borrow_mut(); + lock.callbacks.close.take() + }; + if let Some(callback) = callback { + callback(); + } + unsafe { + PostMessageW(None, CLOSE_ONE_WINDOW, None, LPARAM(handle.0)).log_err(); + } + Some(0) +} + +fn handle_mouse_move_msg( + lparam: LPARAM, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + drop(lock); + let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { + flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), + flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), + flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), + flags if flags.contains(MK_XBUTTON1) => { + Some(MouseButton::Navigate(NavigationDirection::Back)) + } + flags if flags.contains(MK_XBUTTON2) => { + Some(MouseButton::Navigate(NavigationDirection::Forward)) + } + _ => None, + }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let event = MouseMoveEvent { + position: logical_point(x, y, scale_factor), + pressed_button, + modifiers: current_modifiers(), + }; + let result = if callback(PlatformInput::MouseMove(event)).default_prevented { + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + return result; + } + Some(1) +} + +fn handle_syskeydown_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` + // shortcuts. + let Some(keystroke) = parse_syskeydown_msg_keystroke(wparam) else { + return None; + }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return None; + }; + drop(lock); + let event = KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }; + let result = if func(PlatformInput::KeyDown(event)).default_prevented { + invalidate_client_area(handle); + Some(0) + } else { + None + }; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + result +} + +fn handle_syskeyup_msg( + handle: HWND, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` + // shortcuts. + let Some(keystroke) = parse_syskeydown_msg_keystroke(wparam) else { + return None; + }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return None; + }; + drop(lock); + let event = KeyUpEvent { keystroke }; + let result = if func(PlatformInput::KeyUp(event)).default_prevented { + invalidate_client_area(handle); + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + result +} + +fn handle_keydown_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let Some(keystroke) = parse_keydown_msg_keystroke(wparam) else { + return Some(1); + }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + drop(lock); + let event = KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }; + let result = if func(PlatformInput::KeyDown(event)).default_prevented { + invalidate_client_area(handle); + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + result +} + +fn handle_keyup_msg( + handle: HWND, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + let Some(keystroke) = parse_keydown_msg_keystroke(wparam) else { + return Some(1); + }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + drop(lock); + let event = KeyUpEvent { keystroke }; + let result = if func(PlatformInput::KeyUp(event)).default_prevented { + invalidate_client_area(handle); + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + result +} + +fn handle_char_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let Some(keystroke) = parse_char_msg_keystroke(wparam) else { + return Some(1); + }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + drop(lock); + let ime_key = keystroke.ime_key.clone(); + let event = KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }; + + let dispatch_event_result = func(PlatformInput::KeyDown(event)); + let mut lock = state_ptr.state.borrow_mut(); + lock.callbacks.input = Some(func); + if dispatch_event_result.default_prevented || !dispatch_event_result.propagate { + invalidate_client_area(handle); + return Some(0); + } + let Some(ime_char) = ime_key else { + return Some(1); + }; + let Some(mut input_handler) = lock.input_handler.take() else { + return Some(1); + }; + drop(lock); + input_handler.replace_text_in_range(None, &ime_char); + invalidate_client_area(handle); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + + Some(0) +} + +fn handle_mouse_down_msg( + button: MouseButton, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.input.take() { + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); + let click_count = lock.click_state.update(button, physical_point); + let scale_factor = lock.scale_factor; + drop(lock); + + let event = MouseDownEvent { + button, + position: logical_point(x, y, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }; + let result = if callback(PlatformInput::MouseDown(event)).default_prevented { + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + + result + } else { + Some(1) + } +} + +fn handle_mouse_up_msg( + button: MouseButton, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.input.take() { + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let click_count = lock.click_state.current_count; + let scale_factor = lock.scale_factor; + drop(lock); + + let event = MouseUpEvent { + button, + position: logical_point(x, y, scale_factor), + modifiers: current_modifiers(), + click_count, + }; + let result = if callback(PlatformInput::MouseUp(event)).default_prevented { + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + + result + } else { + Some(1) + } +} + +fn handle_xbutton_msg( + wparam: WPARAM, + lparam: LPARAM, + handler: impl Fn(MouseButton, LPARAM, Rc) -> Option, + state_ptr: Rc, +) -> Option { + let nav_dir = match wparam.hiword() { + XBUTTON1 => NavigationDirection::Back, + XBUTTON2 => NavigationDirection::Forward, + _ => return Some(1), + }; + handler(MouseButton::Navigate(nav_dir), lparam, state_ptr) +} + +fn handle_mouse_wheel_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + let wheel_scroll_lines = lock.mouse_wheel_settings.wheel_scroll_lines; + drop(lock); + let wheel_distance = + (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_lines as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point) }; + let event = ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(Point { + x: 0.0, + y: wheel_distance, + }), + modifiers: current_modifiers(), + touch_phase: TouchPhase::Moved, + }; + let result = if callback(PlatformInput::ScrollWheel(event)).default_prevented { + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + + result + } else { + Some(1) + } +} + +fn handle_mouse_horizontal_wheel_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + let wheel_scroll_chars = lock.mouse_wheel_settings.wheel_scroll_chars; + drop(lock); + let wheel_distance = + (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point) }; + let event = ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(Point { + x: wheel_distance, + y: 0.0, + }), + modifiers: current_modifiers(), + touch_phase: TouchPhase::Moved, + }; + let result = if callback(PlatformInput::ScrollWheel(event)).default_prevented { + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + + result + } else { + Some(1) + } +} + +fn handle_ime_position(handle: HWND, state_ptr: Rc) -> Option { + unsafe { + let mut lock = state_ptr.state.borrow_mut(); + let ctx = ImmGetContext(handle); + let Some(mut input_handler) = lock.input_handler.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + drop(lock); + + let Some(caret_range) = input_handler.selected_text_range() else { + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + return Some(0); + }; + let caret_position = input_handler.bounds_for_range(caret_range).unwrap(); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + let config = CANDIDATEFORM { + dwStyle: CFS_CANDIDATEPOS, + // logical to physical + ptCurrentPos: POINT { + x: (caret_position.origin.x.0 * scale_factor) as i32, + y: (caret_position.origin.y.0 * scale_factor) as i32 + + ((caret_position.size.height.0 * scale_factor) as i32 / 2), + }, + ..Default::default() + }; + ImmSetCandidateWindow(ctx, &config as _); + ImmReleaseContext(handle, ctx); + Some(0) + } +} + +fn handle_ime_composition( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut ime_input = None; + if lparam.0 as u32 & GCS_COMPSTR.0 > 0 { + let Some((string, string_len)) = parse_ime_compostion_string(handle) else { + return None; + }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut input_handler) = lock.input_handler.take() else { + return None; + }; + drop(lock); + input_handler.replace_and_mark_text_in_range(None, string.as_str(), Some(0..string_len)); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + ime_input = Some(string); + } + if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 { + let Some(ref comp_string) = ime_input else { + return None; + }; + let caret_pos = retrieve_composition_cursor_position(handle); + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut input_handler) = lock.input_handler.take() else { + return None; + }; + drop(lock); + input_handler.replace_and_mark_text_in_range(None, comp_string, Some(0..caret_pos)); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + } + if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 { + let Some(comp_result) = parse_ime_compostion_result(handle) else { + return None; + }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut input_handler) = lock.input_handler.take() else { + return Some(1); + }; + drop(lock); + input_handler.replace_text_in_range(None, &comp_result); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + invalidate_client_area(handle); + return Some(0); + } + // currently, we don't care other stuff + None +} + +/// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize +fn handle_calc_client_size( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() { + return None; + } + + if wparam.0 == 0 { + return None; + } + + let dpi = unsafe { GetDpiForWindow(handle) }; + + let frame_x = unsafe { GetSystemMetricsForDpi(SM_CXFRAME, dpi) }; + let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; + let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) }; + + // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure + let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; + let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; + + requested_client_rect[0].right -= frame_x + padding; + requested_client_rect[0].left += frame_x + padding; + requested_client_rect[0].bottom -= frame_y + padding; + + Some(0) +} + +fn handle_activate_msg( + handle: HWND, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + let activated = wparam.loword() > 0; + if state_ptr.hide_title_bar { + if let Some(titlebar_rect) = state_ptr.state.borrow().get_titlebar_rect().log_err() { + unsafe { InvalidateRect(handle, Some(&titlebar_rect), FALSE) }; + } + } + let this = state_ptr.clone(); + state_ptr + .executor + .spawn(async move { + let mut lock = this.state.borrow_mut(); + if let Some(mut cb) = lock.callbacks.active_status_change.take() { + drop(lock); + cb(activated); + this.state.borrow_mut().callbacks.active_status_change = Some(cb); + } + }) + .detach(); + + None +} + +fn handle_create_msg(handle: HWND, state_ptr: Rc) -> Option { + let mut size_rect = RECT::default(); + unsafe { GetWindowRect(handle, &mut size_rect).log_err() }; + + let width = size_rect.right - size_rect.left; + let height = size_rect.bottom - size_rect.top; + + if state_ptr.hide_title_bar { + unsafe { + SetWindowPos( + handle, + None, + size_rect.left, + size_rect.top, + width, + height, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE, + ) + .log_err() + }; + } + + Some(0) +} + +fn handle_dpi_changed_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let new_dpi = wparam.loword() as f32; + state_ptr.state.borrow_mut().scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; + + let rect = unsafe { &*(lparam.0 as *const RECT) }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + // this will emit `WM_SIZE` and `WM_MOVE` right here + // even before this function returns + // the new size is handled in `WM_SIZE` + unsafe { + SetWindowPos( + handle, + None, + rect.left, + rect.top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + .context("unable to set window position after dpi has changed") + .log_err(); + } + invalidate_client_area(handle); + + Some(0) +} + +fn handle_hit_test_msg( + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if !state_ptr.hide_title_bar { + return None; + } + + // default handler for resize areas + let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; + if matches!( + hit.0 as u32, + HTNOWHERE + | HTRIGHT + | HTLEFT + | HTTOPLEFT + | HTTOP + | HTTOPRIGHT + | HTBOTTOMRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + ) { + return Some(hit.0); + } + + if state_ptr.state.borrow().is_fullscreen() { + return Some(HTCLIENT as _); + } + + let dpi = unsafe { GetDpiForWindow(handle) }; + let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; + let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) }; + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point) }; + if cursor_point.y > 0 && cursor_point.y < frame_y + padding { + return Some(HTTOP as _); + } + + let titlebar_rect = state_ptr.state.borrow().get_titlebar_rect(); + if let Ok(titlebar_rect) = titlebar_rect { + if cursor_point.y < titlebar_rect.bottom { + let caption_btn_width = (state_ptr.state.borrow().caption_button_width().0 + * state_ptr.state.borrow().scale_factor) as i32; + if cursor_point.x >= titlebar_rect.right - caption_btn_width { + return Some(HTCLOSE as _); + } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 2 { + return Some(HTMAXBUTTON as _); + } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 3 { + return Some(HTMINBUTTON as _); + } + + return Some(HTCAPTION as _); + } + } + + Some(HTCLIENT as _) +} + +fn handle_nc_mouse_move_msg( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if !state_ptr.hide_title_bar { + return None; + } + + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + drop(lock); + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point) }; + let event = MouseMoveEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + pressed_button: None, + modifiers: current_modifiers(), + }; + let result = if callback(PlatformInput::MouseMove(event)).default_prevented { + Some(0) + } else { + Some(1) + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + + result + } else { + None + } +} + +fn handle_nc_mouse_down_msg( + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if !state_ptr.hide_title_bar { + return None; + } + + let mut lock = state_ptr.state.borrow_mut(); + let result = if let Some(mut callback) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point) }; + let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); + let click_count = lock.click_state.update(button, physical_point); + drop(lock); + let event = MouseDownEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }; + let result = if callback(PlatformInput::MouseDown(event)).default_prevented { + Some(0) + } else { + None + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + + result + } else { + None + }; + + // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc + result.or_else(|| matches!(wparam.0 as u32, HTMINBUTTON | HTMAXBUTTON | HTCLOSE).then_some(0)) +} + +fn handle_nc_mouse_up_msg( + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if !state_ptr.hide_title_bar { + return None; + } + + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + drop(lock); + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point) }; + let event = MouseUpEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count: 1, + }; + let result = if callback(PlatformInput::MouseUp(event)).default_prevented { + Some(0) + } else { + None + }; + state_ptr.state.borrow_mut().callbacks.input = Some(callback); + if result.is_some() { + return result; + } + } else { + drop(lock); + } + + if button == MouseButton::Left { + match wparam.0 as u32 { + HTMINBUTTON => unsafe { + ShowWindowAsync(handle, SW_MINIMIZE); + }, + HTMAXBUTTON => unsafe { + if state_ptr.state.borrow().is_maximized() { + ShowWindowAsync(handle, SW_NORMAL); + } else { + ShowWindowAsync(handle, SW_MAXIMIZE); + } + }, + HTCLOSE => unsafe { + PostMessageW(handle, WM_CLOSE, WPARAM::default(), LPARAM::default()).log_err(); + }, + _ => return None, + }; + return Some(0); + } + + None +} + +fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc) -> Option { + state_ptr.state.borrow_mut().current_cursor = HCURSOR(lparam.0); + Some(0) +} + +fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc) -> Option { + if matches!( + lparam.loword() as u32, + HTLEFT | HTRIGHT | HTTOP | HTTOPLEFT | HTTOPRIGHT | HTBOTTOM | HTBOTTOMLEFT | HTBOTTOMRIGHT + ) { + return None; + } + unsafe { SetCursor(state_ptr.state.borrow().current_cursor) }; + Some(1) +} + +fn handle_mouse_wheel_settings_msg( + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + match lparam.0 { + 1 => { + state_ptr + .state + .borrow_mut() + .mouse_wheel_settings + .wheel_scroll_chars = wparam.0 as u32 + } + 2 => { + state_ptr + .state + .borrow_mut() + .mouse_wheel_settings + .wheel_scroll_lines = wparam.0 as u32 + } + _ => unreachable!(), + } + Some(0) +} + +fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option { + let modifiers = current_modifiers(); + if !modifiers.alt { + // on Windows, F10 can trigger this event, not just the alt key + // and we just don't care about F10 + return None; + } + + let vk_code = wparam.loword(); + let basic_key = basic_vkcode_to_string(vk_code, modifiers); + if basic_key.is_some() { + return basic_key; + } + + let key = match VIRTUAL_KEY(vk_code) { + VK_BACK => Some("backspace"), + VK_RETURN => Some("enter"), + VK_TAB => Some("tab"), + VK_UP => Some("up"), + VK_DOWN => Some("down"), + VK_RIGHT => Some("right"), + VK_LEFT => Some("left"), + VK_HOME => Some("home"), + VK_END => Some("end"), + VK_PRIOR => Some("pageup"), + VK_NEXT => Some("pagedown"), + VK_ESCAPE => Some("escape"), + VK_INSERT => Some("insert"), + _ => None, + }; + + if let Some(key) = key { + Some(Keystroke { + modifiers, + key: key.to_string(), + ime_key: None, + }) + } else { + None + } +} + +fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option { + let vk_code = wparam.loword(); + + let modifiers = current_modifiers(); + if modifiers.control || modifiers.alt { + let basic_key = basic_vkcode_to_string(vk_code, modifiers); + if basic_key.is_some() { + return basic_key; + } + } + + if vk_code >= VK_F1.0 && vk_code <= VK_F24.0 { + let offset = vk_code - VK_F1.0; + return Some(Keystroke { + modifiers, + key: format!("f{}", offset + 1), + ime_key: None, + }); + } + + let key = match VIRTUAL_KEY(vk_code) { + VK_BACK => Some("backspace"), + VK_RETURN => Some("enter"), + VK_TAB => Some("tab"), + VK_UP => Some("up"), + VK_DOWN => Some("down"), + VK_RIGHT => Some("right"), + VK_LEFT => Some("left"), + VK_HOME => Some("home"), + VK_END => Some("end"), + VK_PRIOR => Some("pageup"), + VK_NEXT => Some("pagedown"), + VK_ESCAPE => Some("escape"), + VK_INSERT => Some("insert"), + VK_DELETE => Some("delete"), + _ => None, + }; + + if let Some(key) = key { + Some(Keystroke { + modifiers, + key: key.to_string(), + ime_key: None, + }) + } else { + None + } +} + +fn parse_char_msg_keystroke(wparam: WPARAM) -> Option { + let src = [wparam.0 as u16]; + let Ok(first_char) = char::decode_utf16(src).collect::>()[0] else { + return None; + }; + if first_char.is_control() { + None + } else { + let mut modifiers = current_modifiers(); + // for characters that use 'shift' to type it is expected that the + // shift is not reported if the uppercase/lowercase are the same and instead only the key is reported + if first_char.to_lowercase().to_string() == first_char.to_uppercase().to_string() { + modifiers.shift = false; + } + let key = match first_char { + ' ' => "space".to_string(), + first_char => first_char.to_lowercase().to_string(), + }; + Some(Keystroke { + modifiers, + key, + ime_key: Some(first_char.to_string()), + }) + } +} + +/// mark window client rect to be re-drawn +/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-invalidaterect +pub(crate) fn invalidate_client_area(handle: HWND) { + unsafe { InvalidateRect(handle, None, FALSE) }; +} + +fn parse_ime_compostion_string(handle: HWND) -> Option<(String, usize)> { + unsafe { + let ctx = ImmGetContext(handle); + let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0); + let result = if string_len >= 0 { + let mut buffer = vec![0u8; string_len as usize + 2]; + ImmGetCompositionStringW( + ctx, + GCS_COMPSTR, + Some(buffer.as_mut_ptr() as _), + string_len as _, + ); + let wstring = std::slice::from_raw_parts::( + buffer.as_mut_ptr().cast::(), + string_len as usize / 2, + ); + let string = String::from_utf16_lossy(wstring); + Some((string, string_len as usize / 2)) + } else { + None + }; + ImmReleaseContext(handle, ctx); + result + } +} + +fn retrieve_composition_cursor_position(handle: HWND) -> usize { + unsafe { + let ctx = ImmGetContext(handle); + let ret = ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0); + ImmReleaseContext(handle, ctx); + ret as usize + } +} + +fn parse_ime_compostion_result(handle: HWND) -> Option { + unsafe { + let ctx = ImmGetContext(handle); + let string_len = ImmGetCompositionStringW(ctx, GCS_RESULTSTR, None, 0); + let result = if string_len >= 0 { + let mut buffer = vec![0u8; string_len as usize + 2]; + ImmGetCompositionStringW( + ctx, + GCS_RESULTSTR, + Some(buffer.as_mut_ptr() as _), + string_len as _, + ); + let wstring = std::slice::from_raw_parts::( + buffer.as_mut_ptr().cast::(), + string_len as usize / 2, + ); + let string = String::from_utf16_lossy(wstring); + Some(string) + } else { + None + }; + ImmReleaseContext(handle, ctx); + result + } +} + +fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option { + match code { + // VK_0 - VK_9 + 48..=57 => Some(Keystroke { + modifiers, + key: format!("{}", code - VK_0.0), + ime_key: None, + }), + // VK_A - VK_Z + 65..=90 => Some(Keystroke { + modifiers, + key: format!("{}", (b'a' + code as u8 - VK_A.0 as u8) as char), + ime_key: None, + }), + // VK_F1 - VK_F24 + 112..=135 => Some(Keystroke { + modifiers, + key: format!("f{}", code - VK_F1.0 + 1), + ime_key: None, + }), + // OEM3: `/~, OEM_MINUS: -/_, OEM_PLUS: =/+, ... + _ => { + if let Some(key) = oemkey_vkcode_to_string(code) { + Some(Keystroke { + modifiers, + key, + ime_key: None, + }) + } else { + None + } + } + } +} + +fn oemkey_vkcode_to_string(code: u16) -> Option { + match code { + 186 => Some(";".to_string()), // VK_OEM_1 + 187 => Some("=".to_string()), // VK_OEM_PLUS + 188 => Some(",".to_string()), // VK_OEM_COMMA + 189 => Some("-".to_string()), // VK_OEM_MINUS + 190 => Some(".".to_string()), // VK_OEM_PERIOD + // https://kbdlayout.info/features/virtualkeys/VK_ABNT_C1 + 191 | 193 => Some("/".to_string()), // VK_OEM_2 VK_ABNT_C1 + 192 => Some("`".to_string()), // VK_OEM_3 + 219 => Some("[".to_string()), // VK_OEM_4 + 220 => Some("\\".to_string()), // VK_OEM_5 + 221 => Some("]".to_string()), // VK_OEM_6 + 222 => Some("'".to_string()), // VK_OEM_7 + _ => None, + } +} + +#[inline] +fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool { + unsafe { GetKeyState(vkey.0 as i32) < 0 } +} + +#[inline] +fn current_modifiers() -> Modifiers { + Modifiers { + control: is_virtual_key_pressed(VK_CONTROL), + alt: is_virtual_key_pressed(VK_MENU), + shift: is_virtual_key_pressed(VK_SHIFT), + platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN), + function: false, + } +} diff --git a/crates/ming/src/platform/windows/platform.rs b/crates/ming/src/platform/windows/platform.rs new file mode 100644 index 0000000..470d6be --- /dev/null +++ b/crates/ming/src/platform/windows/platform.rs @@ -0,0 +1,872 @@ +// todo(windows): remove +#![allow(unused_variables)] + +use std::{ + cell::{Cell, RefCell}, + ffi::{c_void, OsString}, + mem::transmute, + os::windows::ffi::{OsStrExt, OsStringExt}, + path::{Path, PathBuf}, + rc::Rc, + sync::{Arc, OnceLock}, +}; + +use ::util::ResultExt; +use anyhow::{anyhow, Context, Result}; +use copypasta::{ClipboardContext, ClipboardProvider}; +use futures::channel::oneshot::{self, Receiver}; +use itertools::Itertools; +use parking_lot::RwLock; +use semantic_version::SemanticVersion; +use smallvec::SmallVec; +use time::UtcOffset; +use windows::{ + core::*, + Wdk::System::SystemServices::*, + Win32::{ + Foundation::*, + Graphics::Gdi::*, + Media::*, + Security::Credentials::*, + Storage::FileSystem::*, + System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*, Time::*}, + UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, + }, +}; + +use crate::*; + +pub(crate) struct WindowsPlatform { + state: RefCell, + raw_window_handles: RwLock>, + // The below members will never change throughout the entire lifecycle of the app. + icon: HICON, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, +} + +pub(crate) struct WindowsPlatformState { + callbacks: PlatformCallbacks, + pub(crate) settings: WindowsPlatformSystemSettings, + // NOTE: standard cursor handles don't need to close. + pub(crate) current_cursor: HCURSOR, +} + +#[derive(Default)] +struct PlatformCallbacks { + open_urls: Option)>>, + quit: Option>, + reopen: Option>, + app_menu_action: Option>, + will_open_app_menu: Option>, + validate_app_menu_command: Option bool>>, +} + +impl WindowsPlatformState { + fn new() -> Self { + let callbacks = PlatformCallbacks::default(); + let settings = WindowsPlatformSystemSettings::new(); + let current_cursor = load_cursor(CursorStyle::Arrow); + + Self { + callbacks, + settings, + current_cursor, + } + } +} + +impl WindowsPlatform { + pub(crate) fn new() -> Self { + unsafe { + OleInitialize(None).expect("unable to initialize Windows OLE"); + } + let dispatcher = Arc::new(WindowsDispatcher::new()); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + let text_system = if let Some(direct_write) = DirectWriteTextSystem::new().log_err() { + log::info!("Using direct write text system."); + Arc::new(direct_write) as Arc + } else { + log::info!("Using cosmic text system."); + Arc::new(CosmicTextSystem::new()) as Arc + }; + let icon = load_icon().unwrap_or_default(); + let state = RefCell::new(WindowsPlatformState::new()); + let raw_window_handles = RwLock::new(SmallVec::new()); + + Self { + state, + raw_window_handles, + icon, + background_executor, + foreground_executor, + text_system, + } + } + + fn redraw_all(&self) { + for handle in self.raw_window_handles.read().iter() { + unsafe { + RedrawWindow( + *handle, + None, + HRGN::default(), + RDW_INVALIDATE | RDW_UPDATENOW, + ); + } + } + } + + pub fn try_get_windows_inner_from_hwnd(&self, hwnd: HWND) -> Option> { + self.raw_window_handles + .read() + .iter() + .find(|entry| *entry == &hwnd) + .and_then(|hwnd| try_get_window_inner(*hwnd)) + } + + #[inline] + fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) { + self.raw_window_handles + .read() + .iter() + .for_each(|handle| unsafe { + PostMessageW(*handle, message, wparam, lparam).log_err(); + }); + } + + fn close_one_window(&self, target_window: HWND) -> bool { + let mut lock = self.raw_window_handles.write(); + let index = lock + .iter() + .position(|handle| *handle == target_window) + .unwrap(); + lock.remove(index); + + lock.is_empty() + } + + fn update_system_settings(&self) { + let mut lock = self.state.borrow_mut(); + // mouse wheel + { + let (scroll_chars, scroll_lines) = lock.settings.mouse_wheel_settings.update(); + if let Some(scroll_chars) = scroll_chars { + self.post_message( + MOUSE_WHEEL_SETTINGS_CHANGED, + WPARAM(scroll_chars as usize), + LPARAM(MOUSE_WHEEL_SETTINGS_SCROLL_CHARS_CHANGED), + ); + } + if let Some(scroll_lines) = scroll_lines { + self.post_message( + MOUSE_WHEEL_SETTINGS_CHANGED, + WPARAM(scroll_lines as usize), + LPARAM(MOUSE_WHEEL_SETTINGS_SCROLL_LINES_CHANGED), + ); + } + } + } +} + +impl Platform for WindowsPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.text_system.clone() + } + + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); + let vsync_event = create_event().unwrap(); + let timer_stop_event = create_event().unwrap(); + let raw_timer_stop_event = timer_stop_event.to_raw(); + begin_vsync_timer(vsync_event.to_raw(), timer_stop_event); + 'a: loop { + let wait_result = unsafe { + MsgWaitForMultipleObjects( + Some(&[vsync_event.to_raw()]), + false, + INFINITE, + QS_ALLINPUT, + ) + }; + + match wait_result { + // compositor clock ticked so we should draw a frame + WAIT_EVENT(0) => { + self.redraw_all(); + } + // Windows thread messages are posted + WAIT_EVENT(1) => { + let mut msg = MSG::default(); + unsafe { + while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { + match msg.message { + WM_QUIT => break 'a, + CLOSE_ONE_WINDOW => { + if self.close_one_window(HWND(msg.lParam.0)) { + break 'a; + } + } + WM_SETTINGCHANGE => self.update_system_settings(), + _ => { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + } + } + } + _ => { + log::error!("Something went wrong while waiting {:?}", wait_result); + break; + } + } + } + end_vsync_timer(raw_timer_stop_event); + + if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit { + callback(); + } + } + + fn quit(&self) { + self.foreground_executor() + .spawn(async { unsafe { PostQuitMessage(0) } }) + .detach(); + } + + fn restart(&self, _: Option) { + let pid = std::process::id(); + let Some(app_path) = self.app_path().log_err() else { + return; + }; + let script = format!( + r#" + $pidToWaitFor = {} + $exePath = "{}" + + while ($true) {{ + $process = Get-Process -Id $pidToWaitFor -ErrorAction SilentlyContinue + if (-not $process) {{ + Start-Process -FilePath $exePath + break + }} + Start-Sleep -Seconds 0.1 + }} + "#, + pid, + app_path.display(), + ); + let restart_process = std::process::Command::new("powershell.exe") + .arg("-command") + .arg(script) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + // todo(windows) + fn activate(&self, ignoring_other_apps: bool) {} + + // todo(windows) + fn hide(&self) { + unimplemented!() + } + + // todo(windows) + fn hide_other_apps(&self) { + unimplemented!() + } + + // todo(windows) + fn unhide_other_apps(&self) { + unimplemented!() + } + + fn displays(&self) -> Vec> { + WindowsDisplay::displays() + } + + fn primary_display(&self) -> Option> { + WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc) + } + + fn active_window(&self) -> Option { + let active_window_hwnd = unsafe { GetActiveWindow() }; + self.try_get_windows_inner_from_hwnd(active_window_hwnd) + .map(|inner| inner.handle) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Box { + let lock = self.state.borrow(); + let window = WindowsWindow::new( + handle, + options, + self.icon, + self.foreground_executor.clone(), + lock.settings.mouse_wheel_settings, + lock.current_cursor, + ); + drop(lock); + let handle = window.get_raw_handle(); + self.raw_window_handles.write().push(handle); + + Box::new(window) + } + + // todo(windows) + fn window_appearance(&self) -> WindowAppearance { + WindowAppearance::Dark + } + + fn open_url(&self, url: &str) { + let url_string = url.to_string(); + self.background_executor() + .spawn(async move { + if url_string.is_empty() { + return; + } + open_target(url_string.as_str()); + }) + .detach(); + } + + fn on_open_urls(&self, callback: Box)>) { + self.state.borrow_mut().callbacks.open_urls = Some(callback); + } + + fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver>> { + let (tx, rx) = oneshot::channel(); + + self.foreground_executor() + .spawn(async move { + let tx = Cell::new(Some(tx)); + + // create file open dialog + let folder_dialog: IFileOpenDialog = unsafe { + CoCreateInstance::, IFileOpenDialog>( + &FileOpenDialog, + None, + CLSCTX_ALL, + ) + .unwrap() + }; + + // dialog options + let mut dialog_options: FILEOPENDIALOGOPTIONS = FOS_FILEMUSTEXIST; + if options.multiple { + dialog_options |= FOS_ALLOWMULTISELECT; + } + if options.directories { + dialog_options |= FOS_PICKFOLDERS; + } + + unsafe { + folder_dialog.SetOptions(dialog_options).unwrap(); + folder_dialog + .SetTitle(&HSTRING::from(OsString::from("Select a folder"))) + .unwrap(); + } + + let hr = unsafe { folder_dialog.Show(None) }; + + if hr.is_err() { + if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) { + // user canceled error + if let Some(tx) = tx.take() { + tx.send(None).unwrap(); + } + return; + } + } + + let mut results = unsafe { folder_dialog.GetResults().unwrap() }; + + let mut paths: Vec = Vec::new(); + for i in 0..unsafe { results.GetCount().unwrap() } { + let mut item: IShellItem = unsafe { results.GetItemAt(i).unwrap() }; + let mut path: PWSTR = + unsafe { item.GetDisplayName(SIGDN_FILESYSPATH).unwrap() }; + let mut path_os_string = OsString::from_wide(unsafe { path.as_wide() }); + + paths.push(PathBuf::from(path_os_string)); + } + + if let Some(tx) = tx.take() { + if paths.len() == 0 { + tx.send(None).unwrap(); + } else { + tx.send(Some(paths)).unwrap(); + } + } + }) + .detach(); + + rx + } + + fn prompt_for_new_path(&self, directory: &Path) -> Receiver> { + let directory = directory.to_owned(); + let (tx, rx) = oneshot::channel(); + self.foreground_executor() + .spawn(async move { + unsafe { + let Ok(dialog) = show_savefile_dialog(directory) else { + let _ = tx.send(None); + return; + }; + let Ok(_) = dialog.Show(None) else { + let _ = tx.send(None); // user cancel + return; + }; + if let Ok(shell_item) = dialog.GetResult() { + if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) { + let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap()))); + return; + } + } + let _ = tx.send(None); + } + }) + .detach(); + + rx + } + + fn reveal_path(&self, path: &Path) { + let Ok(file_full_path) = path.canonicalize() else { + log::error!("unable to parse file path"); + return; + }; + self.background_executor() + .spawn(async move { + let Some(path) = file_full_path.to_str() else { + return; + }; + if path.is_empty() { + return; + } + open_target(path); + }) + .detach(); + } + + fn on_quit(&self, callback: Box) { + self.state.borrow_mut().callbacks.quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.state.borrow_mut().callbacks.reopen = Some(callback); + } + + // todo(windows) + fn set_menus(&self, menus: Vec, keymap: &Keymap) {} + + fn on_app_menu_action(&self, callback: Box) { + self.state.borrow_mut().callbacks.app_menu_action = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.state.borrow_mut().callbacks.will_open_app_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.state.borrow_mut().callbacks.validate_app_menu_command = Some(callback); + } + + fn os_name(&self) -> &'static str { + "Windows" + } + + fn os_version(&self) -> Result { + let mut info = unsafe { std::mem::zeroed() }; + let status = unsafe { RtlGetVersion(&mut info) }; + if status.is_ok() { + Ok(SemanticVersion::new( + info.dwMajorVersion as _, + info.dwMinorVersion as _, + info.dwBuildNumber as _, + )) + } else { + Err(anyhow::anyhow!( + "unable to get Windows version: {}", + std::io::Error::last_os_error() + )) + } + } + + fn app_version(&self) -> Result { + let mut file_name_buffer = vec![0u16; MAX_PATH as usize]; + let file_name = { + let mut file_name_buffer_capacity = MAX_PATH as usize; + let mut file_name_length; + loop { + file_name_length = + unsafe { GetModuleFileNameW(None, &mut file_name_buffer) } as usize; + if file_name_length < file_name_buffer_capacity { + break; + } + // buffer too small + file_name_buffer_capacity *= 2; + file_name_buffer = vec![0u16; file_name_buffer_capacity]; + } + PCWSTR::from_raw(file_name_buffer[0..(file_name_length + 1)].as_ptr()) + }; + + let version_info_block = { + let mut version_handle = 0; + let version_info_size = + unsafe { GetFileVersionInfoSizeW(file_name, Some(&mut version_handle)) } as usize; + if version_info_size == 0 { + log::error!( + "unable to get version info size: {}", + std::io::Error::last_os_error() + ); + return Err(anyhow!("unable to get version info size")); + } + let mut version_data = vec![0u8; version_info_size + 2]; + unsafe { + GetFileVersionInfoW( + file_name, + version_handle, + version_info_size as u32, + version_data.as_mut_ptr() as _, + ) + } + .inspect_err(|_| { + log::error!( + "unable to retrieve version info: {}", + std::io::Error::last_os_error() + ) + })?; + version_data + }; + + let version_info_raw = { + let mut buffer = unsafe { std::mem::zeroed() }; + let mut size = 0; + let entry = "\\".encode_utf16().chain(Some(0)).collect_vec(); + if !unsafe { + VerQueryValueW( + version_info_block.as_ptr() as _, + PCWSTR::from_raw(entry.as_ptr()), + &mut buffer, + &mut size, + ) + } + .as_bool() + { + log::error!( + "unable to query version info data: {}", + std::io::Error::last_os_error() + ); + return Err(anyhow!("the specified resource is not valid")); + } + if size == 0 { + log::error!( + "unable to query version info data: {}", + std::io::Error::last_os_error() + ); + return Err(anyhow!("no value is available for the specified name")); + } + buffer + }; + + let version_info = unsafe { &*(version_info_raw as *mut VS_FIXEDFILEINFO) }; + // https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo + if version_info.dwSignature == 0xFEEF04BD { + return Ok(SemanticVersion::new( + ((version_info.dwProductVersionMS >> 16) & 0xFFFF) as usize, + (version_info.dwProductVersionMS & 0xFFFF) as usize, + ((version_info.dwProductVersionLS >> 16) & 0xFFFF) as usize, + )); + } else { + log::error!( + "no version info present: {}", + std::io::Error::last_os_error() + ); + return Err(anyhow!("no version info present")); + } + } + + fn app_path(&self) -> Result { + Ok(std::env::current_exe()?) + } + + fn local_timezone(&self) -> UtcOffset { + let mut info = unsafe { std::mem::zeroed() }; + let ret = unsafe { GetTimeZoneInformation(&mut info) }; + if ret == TIME_ZONE_ID_INVALID { + log::error!( + "Unable to get local timezone: {}", + std::io::Error::last_os_error() + ); + return UtcOffset::UTC; + } + // Windows treat offset as: + // UTC = localtime + offset + // so we add a minus here + let hours = -info.Bias / 60; + let minutes = -info.Bias % 60; + + UtcOffset::from_hms(hours as _, minutes as _, 0).unwrap() + } + + // todo(windows) + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + Err(anyhow!("not yet implemented")) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let hcursor = load_cursor(style); + self.post_message(CURSOR_STYLE_CHANGED, WPARAM(0), LPARAM(hcursor.0)); + self.state.borrow_mut().current_cursor = hcursor; + } + + // todo(windows) + fn should_auto_hide_scrollbars(&self) -> bool { + false + } + + fn write_to_primary(&self, _item: ClipboardItem) {} + + fn write_to_clipboard(&self, item: ClipboardItem) { + if item.text.len() > 0 { + let mut ctx = ClipboardContext::new().unwrap(); + ctx.set_contents(item.text().to_owned()).unwrap(); + } + } + + fn read_from_primary(&self) -> Option { + None + } + + fn read_from_clipboard(&self) -> Option { + let mut ctx = ClipboardContext::new().unwrap(); + let content = ctx.get_contents().ok()?; + Some(ClipboardItem { + text: content, + metadata: None, + }) + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let mut password = password.to_vec(); + let mut username = username.encode_utf16().chain(Some(0)).collect_vec(); + let mut target_name = windows_credentials_target_name(url) + .encode_utf16() + .chain(Some(0)) + .collect_vec(); + self.foreground_executor().spawn(async move { + let credentials = CREDENTIALW { + LastWritten: unsafe { GetSystemTimeAsFileTime() }, + Flags: CRED_FLAGS(0), + Type: CRED_TYPE_GENERIC, + TargetName: PWSTR::from_raw(target_name.as_mut_ptr()), + CredentialBlobSize: password.len() as u32, + CredentialBlob: password.as_ptr() as *mut _, + Persist: CRED_PERSIST_LOCAL_MACHINE, + UserName: PWSTR::from_raw(username.as_mut_ptr()), + ..CREDENTIALW::default() + }; + unsafe { CredWriteW(&credentials, 0) }?; + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let mut target_name = windows_credentials_target_name(url) + .encode_utf16() + .chain(Some(0)) + .collect_vec(); + self.foreground_executor().spawn(async move { + let mut credentials: *mut CREDENTIALW = std::ptr::null_mut(); + unsafe { + CredReadW( + PCWSTR::from_raw(target_name.as_ptr()), + CRED_TYPE_GENERIC, + 0, + &mut credentials, + )? + }; + + if credentials.is_null() { + Ok(None) + } else { + let username: String = unsafe { (*credentials).UserName.to_string()? }; + let credential_blob = unsafe { + std::slice::from_raw_parts( + (*credentials).CredentialBlob, + (*credentials).CredentialBlobSize as usize, + ) + }; + let password = credential_blob.to_vec(); + unsafe { CredFree(credentials as *const c_void) }; + Ok(Some((username, password))) + } + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let mut target_name = windows_credentials_target_name(url) + .encode_utf16() + .chain(Some(0)) + .collect_vec(); + self.foreground_executor().spawn(async move { + unsafe { CredDeleteW(PCWSTR::from_raw(target_name.as_ptr()), CRED_TYPE_GENERIC, 0)? }; + Ok(()) + }) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } +} + +impl Drop for WindowsPlatform { + fn drop(&mut self) { + unsafe { + OleUninitialize(); + } + } +} + +fn open_target(target: &str) { + unsafe { + let ret = ShellExecuteW( + None, + windows::core::w!("open"), + &HSTRING::from(target), + None, + None, + SW_SHOWDEFAULT, + ); + if ret.0 <= 32 { + log::error!("Unable to open target: {}", std::io::Error::last_os_error()); + } + } +} + +unsafe fn show_savefile_dialog(directory: PathBuf) -> Result { + let dialog: IFileSaveDialog = CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)?; + let bind_context = CreateBindCtx(0)?; + let Ok(full_path) = directory.canonicalize() else { + return Ok(dialog); + }; + let dir_str = full_path.into_os_string(); + if dir_str.is_empty() { + return Ok(dialog); + } + let dir_vec = dir_str.encode_wide().collect_vec(); + let ret = SHCreateItemFromParsingName(PCWSTR::from_raw(dir_vec.as_ptr()), &bind_context) + .inspect_err(|e| log::error!("unable to create IShellItem: {}", e)); + if ret.is_ok() { + let dir_shell_item: IShellItem = ret.unwrap(); + let _ = dialog + .SetFolder(&dir_shell_item) + .inspect_err(|e| log::error!("unable to set folder for save file dialog: {}", e)); + } + + Ok(dialog) +} + +fn begin_vsync_timer(vsync_event: HANDLE, timer_stop_event: OwnedHandle) { + let vsync_fn = select_vsync_fn(); + std::thread::spawn(move || loop { + if vsync_fn(timer_stop_event.to_raw()) { + if unsafe { SetEvent(vsync_event) }.log_err().is_none() { + break; + } + } + }); +} + +fn end_vsync_timer(timer_stop_event: HANDLE) { + unsafe { SetEvent(timer_stop_event) }.log_err(); +} + +fn select_vsync_fn() -> Box bool + Send> { + if let Some(dcomp_fn) = load_dcomp_vsync_fn() { + log::info!("use DCompositionWaitForCompositorClock for vsync"); + return Box::new(move |timer_stop_event| { + // will be 0 if woken up by timer_stop_event or 1 if the compositor clock ticked + // SEE: https://learn.microsoft.com/en-us/windows/win32/directcomp/compositor-clock/compositor-clock + (unsafe { dcomp_fn(1, &timer_stop_event, INFINITE) }) == 1 + }); + } + log::info!("use fallback vsync function"); + Box::new(fallback_vsync_fn()) +} + +fn load_dcomp_vsync_fn() -> Option u32> { + static FN: OnceLock u32>> = + OnceLock::new(); + *FN.get_or_init(|| { + let hmodule = unsafe { LoadLibraryW(windows::core::w!("dcomp.dll")) }.ok()?; + let address = unsafe { + GetProcAddress( + hmodule, + windows::core::s!("DCompositionWaitForCompositorClock"), + ) + }?; + Some(unsafe { transmute(address) }) + }) +} + +fn fallback_vsync_fn() -> impl Fn(HANDLE) -> bool + Send { + let freq = WindowsDisplay::primary_monitor() + .and_then(|monitor| monitor.frequency()) + .unwrap_or(60); + log::info!("primaly refresh rate is {freq}Hz"); + + let interval = (1000 / freq).max(1); + log::info!("expected interval is {interval}ms"); + + unsafe { timeBeginPeriod(1) }; + + struct TimePeriod; + impl Drop for TimePeriod { + fn drop(&mut self) { + unsafe { timeEndPeriod(1) }; + } + } + let period = TimePeriod; + + move |timer_stop_event| { + let _ = (&period,); + (unsafe { WaitForSingleObject(timer_stop_event, interval) }) == WAIT_TIMEOUT + } +} + +fn load_icon() -> Result { + let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? }; + let handle = unsafe { + LoadImageW( + module, + IDI_APPLICATION, + IMAGE_ICON, + 0, + 0, + LR_DEFAULTSIZE | LR_SHARED, + ) + .context("unable to load icon file")? + }; + Ok(HICON(handle.0)) +} diff --git a/crates/ming/src/platform/windows/system_settings.rs b/crates/ming/src/platform/windows/system_settings.rs new file mode 100644 index 0000000..af670b4 --- /dev/null +++ b/crates/ming/src/platform/windows/system_settings.rs @@ -0,0 +1,81 @@ +use std::ffi::{c_uint, c_void}; + +use util::ResultExt; +use windows::Win32::UI::WindowsAndMessaging::{ + SystemParametersInfoW, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, +}; + +/// Windows settings pulled from SystemParametersInfo +/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow +#[derive(Default, Debug)] +pub(crate) struct WindowsPlatformSystemSettings { + pub(crate) mouse_wheel_settings: MouseWheelSettings, +} + +#[derive(Default, Debug, Clone, Copy)] +pub(crate) struct MouseWheelSettings { + /// SEE: SPI_GETWHEELSCROLLCHARS + pub(crate) wheel_scroll_chars: u32, + /// SEE: SPI_GETWHEELSCROLLLINES + pub(crate) wheel_scroll_lines: u32, +} + +impl WindowsPlatformSystemSettings { + pub(crate) fn new() -> Self { + let mut settings = Self::default(); + settings.init(); + settings + } + + fn init(&mut self) { + self.mouse_wheel_settings.update(); + } +} + +impl MouseWheelSettings { + pub(crate) fn update(&mut self) -> (Option, Option) { + ( + self.update_wheel_scroll_chars(), + self.update_wheel_scroll_lines(), + ) + } + + fn update_wheel_scroll_chars(&mut self) -> Option { + let mut value = c_uint::default(); + let result = unsafe { + SystemParametersInfoW( + SPI_GETWHEELSCROLLCHARS, + 0, + Some((&mut value) as *mut c_uint as *mut c_void), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(), + ) + }; + + if result.log_err() != None && self.wheel_scroll_chars != value { + self.wheel_scroll_chars = value; + Some(value) + } else { + None + } + } + + fn update_wheel_scroll_lines(&mut self) -> Option { + let mut value = c_uint::default(); + let result = unsafe { + SystemParametersInfoW( + SPI_GETWHEELSCROLLLINES, + 0, + Some((&mut value) as *mut c_uint as *mut c_void), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(), + ) + }; + + if result.log_err() != None && self.wheel_scroll_lines != value { + self.wheel_scroll_lines = value; + Some(value) + } else { + None + } + } +} diff --git a/crates/ming/src/platform/windows/util.rs b/crates/ming/src/platform/windows/util.rs new file mode 100644 index 0000000..a9c0c00 --- /dev/null +++ b/crates/ming/src/platform/windows/util.rs @@ -0,0 +1,156 @@ +use std::sync::OnceLock; + +use ::util::ResultExt; +use windows::Win32::{Foundation::*, System::Threading::*, UI::WindowsAndMessaging::*}; + +use crate::*; + +pub(crate) trait HiLoWord { + fn hiword(&self) -> u16; + fn loword(&self) -> u16; + fn signed_hiword(&self) -> i16; + fn signed_loword(&self) -> i16; +} + +impl HiLoWord for WPARAM { + fn hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } + + fn loword(&self) -> u16 { + (self.0 & 0xFFFF) as u16 + } + + fn signed_hiword(&self) -> i16 { + ((self.0 >> 16) & 0xFFFF) as i16 + } + + fn signed_loword(&self) -> i16 { + (self.0 & 0xFFFF) as i16 + } +} + +impl HiLoWord for LPARAM { + fn hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } + + fn loword(&self) -> u16 { + (self.0 & 0xFFFF) as u16 + } + + fn signed_hiword(&self) -> i16 { + ((self.0 >> 16) & 0xFFFF) as i16 + } + + fn signed_loword(&self) -> i16 { + (self.0 & 0xFFFF) as i16 + } +} + +pub(crate) unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { + #[cfg(target_pointer_width = "64")] + unsafe { + GetWindowLongPtrW(hwnd, nindex) + } + #[cfg(target_pointer_width = "32")] + unsafe { + GetWindowLongW(hwnd, nindex) as isize + } +} + +pub(crate) unsafe fn set_window_long( + hwnd: HWND, + nindex: WINDOW_LONG_PTR_INDEX, + dwnewlong: isize, +) -> isize { + #[cfg(target_pointer_width = "64")] + unsafe { + SetWindowLongPtrW(hwnd, nindex, dwnewlong) + } + #[cfg(target_pointer_width = "32")] + unsafe { + SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize + } +} + +#[derive(Debug, Clone)] +pub(crate) struct OwnedHandle(HANDLE); + +impl OwnedHandle { + pub(crate) fn new(handle: HANDLE) -> Self { + Self(handle) + } + + #[inline(always)] + pub(crate) fn to_raw(&self) -> HANDLE { + self.0 + } +} + +impl Drop for OwnedHandle { + fn drop(&mut self) { + if !self.0.is_invalid() { + unsafe { CloseHandle(self.0) }.log_err(); + } + } +} + +pub(crate) fn create_event() -> windows::core::Result { + Ok(OwnedHandle::new(unsafe { + CreateEventW(None, false, false, None)? + })) +} + +pub(crate) fn windows_credentials_target_name(url: &str) -> String { + format!("zed:url={}", url) +} + +pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR { + static ARROW: OnceLock = OnceLock::new(); + static IBEAM: OnceLock = OnceLock::new(); + static CROSS: OnceLock = OnceLock::new(); + static HAND: OnceLock = OnceLock::new(); + static SIZEWE: OnceLock = OnceLock::new(); + static SIZENS: OnceLock = OnceLock::new(); + static NO: OnceLock = OnceLock::new(); + let (lock, name) = match style { + CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => (&IBEAM, IDC_IBEAM), + CursorStyle::Crosshair => (&CROSS, IDC_CROSS), + CursorStyle::PointingHand | CursorStyle::DragLink => (&HAND, IDC_HAND), + CursorStyle::ResizeLeft + | CursorStyle::ResizeRight + | CursorStyle::ResizeLeftRight + | CursorStyle::ResizeColumn => (&SIZEWE, IDC_SIZEWE), + CursorStyle::ResizeUp + | CursorStyle::ResizeDown + | CursorStyle::ResizeUpDown + | CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS), + CursorStyle::OperationNotAllowed => (&NO, IDC_NO), + _ => (&ARROW, IDC_ARROW), + }; + *lock.get_or_init(|| { + HCURSOR( + unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) } + .log_err() + .unwrap_or_default() + .0, + ) + }) +} + +#[inline] +pub(crate) fn logical_size(physical_size: Size, scale_factor: f32) -> Size { + Size { + width: px(physical_size.width.0 as f32 / scale_factor), + height: px(physical_size.height.0 as f32 / scale_factor), + } +} + +#[inline] +pub(crate) fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point { + Point { + x: px(x / scale_factor), + y: px(y / scale_factor), + } +} diff --git a/crates/ming/src/platform/windows/window.rs b/crates/ming/src/platform/windows/window.rs new file mode 100644 index 0000000..91e6af0 --- /dev/null +++ b/crates/ming/src/platform/windows/window.rs @@ -0,0 +1,1032 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +use std::{ + cell::RefCell, + num::NonZeroIsize, + path::PathBuf, + rc::{Rc, Weak}, + str::FromStr, + sync::{Arc, Once}, + time::{Duration, Instant}, +}; + +use ::util::ResultExt; +use anyhow::Context; +use futures::channel::oneshot::{self, Receiver}; +use itertools::Itertools; +use raw_window_handle as rwh; +use smallvec::SmallVec; +use windows::{ + core::*, + Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*}, + UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, + }, +}; + +use crate::platform::blade::BladeRenderer; +use crate::*; + +pub(crate) struct WindowsWindow(pub Rc); + +pub struct WindowsWindowState { + pub origin: Point, + pub physical_size: Size, + pub fullscreen_restore_bounds: Bounds, + pub scale_factor: f32, + + pub callbacks: Callbacks, + pub input_handler: Option, + + pub renderer: BladeRenderer, + + pub click_state: ClickState, + pub mouse_wheel_settings: MouseWheelSettings, + pub current_cursor: HCURSOR, + + pub display: WindowsDisplay, + fullscreen: Option, + hwnd: HWND, +} + +pub(crate) struct WindowsWindowStatePtr { + hwnd: HWND, + pub(crate) state: RefCell, + pub(crate) handle: AnyWindowHandle, + pub(crate) hide_title_bar: bool, + pub(crate) executor: ForegroundExecutor, +} + +impl WindowsWindowState { + fn new( + hwnd: HWND, + transparent: bool, + cs: &CREATESTRUCTW, + mouse_wheel_settings: MouseWheelSettings, + current_cursor: HCURSOR, + display: WindowsDisplay, + ) -> Self { + let origin = point(cs.x.into(), cs.y.into()); + let physical_size = size(cs.cx.into(), cs.cy.into()); + let fullscreen_restore_bounds = Bounds { + origin, + size: physical_size, + }; + let scale_factor = { + let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; + monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32 + }; + let renderer = windows_renderer::windows_renderer(hwnd, transparent); + let callbacks = Callbacks::default(); + let input_handler = None; + let click_state = ClickState::new(); + let fullscreen = None; + + Self { + origin, + physical_size, + fullscreen_restore_bounds, + scale_factor, + callbacks, + input_handler, + renderer, + click_state, + mouse_wheel_settings, + current_cursor, + display, + fullscreen, + hwnd, + } + } + + #[inline] + pub(crate) fn is_fullscreen(&self) -> bool { + self.fullscreen.is_some() + } + + pub(crate) fn is_maximized(&self) -> bool { + !self.is_fullscreen() && unsafe { IsZoomed(self.hwnd) }.as_bool() + } + + fn bounds(&self) -> Bounds { + Bounds { + origin: self.origin, + size: self.physical_size, + } + } + + fn window_bounds(&self) -> WindowBounds { + let placement = unsafe { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..Default::default() + }; + GetWindowPlacement(self.hwnd, &mut placement).log_err(); + placement + }; + let bounds = Bounds { + origin: point( + DevicePixels(placement.rcNormalPosition.left), + DevicePixels(placement.rcNormalPosition.top), + ), + size: size( + DevicePixels(placement.rcNormalPosition.right - placement.rcNormalPosition.left), + DevicePixels(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top), + ), + }; + + if self.is_fullscreen() { + WindowBounds::Fullscreen(self.fullscreen_restore_bounds) + } else if placement.showCmd == SW_SHOWMAXIMIZED.0 as u32 { + WindowBounds::Maximized(bounds) + } else { + WindowBounds::Windowed(bounds) + } + } + + /// get the logical size of the app's drawable area. + /// + /// Currently, GPUI uses logical size of the app to handle mouse interactions (such as + /// whether the mouse collides with other elements of GPUI). + fn content_size(&self) -> Size { + logical_size(self.physical_size, self.scale_factor) + } + + fn title_bar_padding(&self) -> Pixels { + // using USER_DEFAULT_SCREEN_DPI because GPUI handles the scale with the scale factor + let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) }; + px(padding as f32) + } + + fn title_bar_top_offset(&self) -> Pixels { + if self.is_maximized() { + self.title_bar_padding() * 2 + } else { + px(0.) + } + } + + fn title_bar_height(&self) -> Pixels { + // todo(windows) this is hard set to match the ui title bar + // in the future the ui title bar component will report the size + px(32.) + self.title_bar_top_offset() + } + + pub(crate) fn caption_button_width(&self) -> Pixels { + // todo(windows) this is hard set to match the ui title bar + // in the future the ui title bar component will report the size + px(36.) + } + + pub(crate) fn get_titlebar_rect(&self) -> anyhow::Result { + let height = self.title_bar_height(); + let mut rect = RECT::default(); + unsafe { GetClientRect(self.hwnd, &mut rect) }?; + rect.bottom = rect.top + ((height.0 * self.scale_factor).round() as i32); + Ok(rect) + } +} + +impl WindowsWindowStatePtr { + fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Rc { + let state = RefCell::new(WindowsWindowState::new( + hwnd, + context.transparent, + cs, + context.mouse_wheel_settings, + context.current_cursor, + context.display, + )); + + Rc::new(Self { + state, + hwnd, + handle: context.handle, + hide_title_bar: context.hide_title_bar, + executor: context.executor.clone(), + }) + } +} + +#[derive(Default)] +pub(crate) struct Callbacks { + pub(crate) request_frame: Option>, + pub(crate) input: Option DispatchEventResult>>, + pub(crate) active_status_change: Option>, + pub(crate) resize: Option, f32)>>, + pub(crate) moved: Option>, + pub(crate) should_close: Option bool>>, + pub(crate) close: Option>, + pub(crate) appearance_changed: Option>, +} + +struct WindowCreateContext { + inner: Option>, + handle: AnyWindowHandle, + hide_title_bar: bool, + display: WindowsDisplay, + transparent: bool, + executor: ForegroundExecutor, + mouse_wheel_settings: MouseWheelSettings, + current_cursor: HCURSOR, +} + +impl WindowsWindow { + pub(crate) fn new( + handle: AnyWindowHandle, + params: WindowParams, + icon: HICON, + executor: ForegroundExecutor, + mouse_wheel_settings: MouseWheelSettings, + current_cursor: HCURSOR, + ) -> Self { + let classname = register_wnd_class(icon); + let hide_title_bar = params + .titlebar + .as_ref() + .map(|titlebar| titlebar.appears_transparent) + .unwrap_or(false); + let windowname = HSTRING::from( + params + .titlebar + .as_ref() + .and_then(|titlebar| titlebar.title.as_ref()) + .map(|title| title.as_ref()) + .unwrap_or(""), + ); + let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX; + let hinstance = get_module_handle(); + let mut context = WindowCreateContext { + inner: None, + handle, + hide_title_bar, + // todo(windows) move window to target monitor + // options.display_id + display: WindowsDisplay::primary_monitor().unwrap(), + transparent: params.window_background != WindowBackgroundAppearance::Opaque, + executor, + mouse_wheel_settings, + current_cursor, + }; + let lpparam = Some(&context as *const _ as *const _); + let raw_hwnd = unsafe { + CreateWindowExW( + WS_EX_APPWINDOW, + classname, + &windowname, + dwstyle, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + None, + None, + hinstance, + lpparam, + ) + }; + let state_ptr = Rc::clone(context.inner.as_ref().unwrap()); + register_drag_drop(state_ptr.clone()); + let wnd = Self(state_ptr); + + unsafe { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..Default::default() + }; + GetWindowPlacement(raw_hwnd, &mut placement).log_err(); + placement.rcNormalPosition.left = params.bounds.left().0; + placement.rcNormalPosition.right = params.bounds.right().0; + placement.rcNormalPosition.top = params.bounds.top().0; + placement.rcNormalPosition.bottom = params.bounds.bottom().0; + SetWindowPlacement(raw_hwnd, &placement).log_err(); + } + unsafe { ShowWindow(raw_hwnd, SW_SHOW) }; + + wnd + } +} + +impl rwh::HasWindowHandle for WindowsWindow { + fn window_handle(&self) -> std::result::Result, rwh::HandleError> { + let raw = + rwh::Win32WindowHandle::new(unsafe { NonZeroIsize::new_unchecked(self.0.hwnd.0) }) + .into(); + Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) }) + } +} + +// todo(windows) +impl rwh::HasDisplayHandle for WindowsWindow { + fn display_handle(&self) -> std::result::Result, rwh::HandleError> { + unimplemented!() + } +} + +impl Drop for WindowsWindow { + fn drop(&mut self) { + self.0.state.borrow_mut().renderer.destroy(); + // clone this `Rc` to prevent early release of the pointer + let this = self.0.clone(); + self.0 + .executor + .spawn(async move { + let handle = this.hwnd; + unsafe { + RevokeDragDrop(handle).log_err(); + DestroyWindow(handle).log_err(); + } + }) + .detach(); + } +} + +impl PlatformWindow for WindowsWindow { + fn bounds(&self) -> Bounds { + self.0.state.borrow().bounds() + } + + fn is_maximized(&self) -> bool { + self.0.state.borrow().is_maximized() + } + + fn window_bounds(&self) -> WindowBounds { + self.0.state.borrow().window_bounds() + } + + /// get the logical size of the app's drawable area. + /// + /// Currently, GPUI uses logical size of the app to handle mouse interactions (such as + /// whether the mouse collides with other elements of GPUI). + fn content_size(&self) -> Size { + self.0.state.borrow().content_size() + } + + fn scale_factor(&self) -> f32 { + self.0.state.borrow().scale_factor + } + + // todo(windows) + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Dark + } + + fn display(&self) -> Rc { + Rc::new(self.0.state.borrow().display) + } + + fn mouse_position(&self) -> Point { + let scale_factor = self.scale_factor(); + let point = unsafe { + let mut point: POINT = std::mem::zeroed(); + GetCursorPos(&mut point) + .context("unable to get cursor position") + .log_err(); + ScreenToClient(self.0.hwnd, &mut point); + point + }; + logical_point(point.x as f32, point.y as f32, scale_factor) + } + + // todo(windows) + fn modifiers(&self) -> Modifiers { + Modifiers::none() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.state.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.state.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[&str], + ) -> Option> { + let (done_tx, done_rx) = oneshot::channel(); + let msg = msg.to_string(); + let detail_string = match detail { + Some(info) => Some(info.to_string()), + None => None, + }; + let answers = answers.iter().map(|s| s.to_string()).collect::>(); + let handle = self.0.hwnd; + self.0 + .executor + .spawn(async move { + unsafe { + let mut config; + config = std::mem::zeroed::(); + config.cbSize = std::mem::size_of::() as _; + config.hwndParent = handle; + let title; + let main_icon; + match level { + crate::PromptLevel::Info => { + title = windows::core::w!("Info"); + main_icon = TD_INFORMATION_ICON; + } + crate::PromptLevel::Warning => { + title = windows::core::w!("Warning"); + main_icon = TD_WARNING_ICON; + } + crate::PromptLevel::Critical => { + title = windows::core::w!("Critical"); + main_icon = TD_ERROR_ICON; + } + }; + config.pszWindowTitle = title; + config.Anonymous1.pszMainIcon = main_icon; + let instruction = msg.encode_utf16().chain(Some(0)).collect_vec(); + config.pszMainInstruction = PCWSTR::from_raw(instruction.as_ptr()); + let hints_encoded; + if let Some(ref hints) = detail_string { + hints_encoded = hints.encode_utf16().chain(Some(0)).collect_vec(); + config.pszContent = PCWSTR::from_raw(hints_encoded.as_ptr()); + }; + let mut buttons = Vec::new(); + let mut btn_encoded = Vec::new(); + for (index, btn_string) in answers.iter().enumerate() { + let encoded = btn_string.encode_utf16().chain(Some(0)).collect_vec(); + buttons.push(TASKDIALOG_BUTTON { + nButtonID: index as _, + pszButtonText: PCWSTR::from_raw(encoded.as_ptr()), + }); + btn_encoded.push(encoded); + } + config.cButtons = buttons.len() as _; + config.pButtons = buttons.as_ptr(); + + config.pfCallback = None; + let mut res = std::mem::zeroed(); + let _ = TaskDialogIndirect(&config, Some(&mut res), None, None) + .inspect_err(|e| log::error!("unable to create task dialog: {}", e)); + + let _ = done_tx.send(res as usize); + } + }) + .detach(); + + Some(done_rx) + } + + fn activate(&self) { + let hwnd = self.0.hwnd; + unsafe { SetActiveWindow(hwnd) }; + unsafe { SetFocus(hwnd) }; + unsafe { SetForegroundWindow(hwnd) }; + } + + fn is_active(&self) -> bool { + self.0.hwnd == unsafe { GetActiveWindow() } + } + + fn set_title(&mut self, title: &str) { + unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) } + .inspect_err(|e| log::error!("Set title failed: {e}")) + .ok(); + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + self.0 + .state + .borrow_mut() + .renderer + .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); + } + + // todo(windows) + fn set_edited(&mut self, _edited: bool) {} + + // todo(windows) + fn show_character_palette(&self) {} + + fn minimize(&self) { + unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE) }; + } + + fn zoom(&self) { + unsafe { ShowWindowAsync(self.0.hwnd, SW_MAXIMIZE) }; + } + + fn toggle_fullscreen(&self) { + let state_ptr = self.0.clone(); + self.0 + .executor + .spawn(async move { + let mut lock = state_ptr.state.borrow_mut(); + lock.fullscreen_restore_bounds = Bounds { + origin: lock.origin, + size: lock.physical_size, + }; + let StyleAndBounds { + style, + x, + y, + cx, + cy, + } = if let Some(state) = lock.fullscreen.take() { + state + } else { + let style = + WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _); + let mut rc = RECT::default(); + unsafe { GetWindowRect(state_ptr.hwnd, &mut rc) }.log_err(); + let _ = lock.fullscreen.insert(StyleAndBounds { + style, + x: rc.left, + y: rc.top, + cx: rc.right - rc.left, + cy: rc.bottom - rc.top, + }); + let style = style + & !(WS_THICKFRAME + | WS_SYSMENU + | WS_MAXIMIZEBOX + | WS_MINIMIZEBOX + | WS_CAPTION); + let bounds = lock.display.bounds(); + StyleAndBounds { + style, + x: bounds.left().0, + y: bounds.top().0, + cx: bounds.size.width.0, + cy: bounds.size.height.0, + } + }; + drop(lock); + unsafe { set_window_long(state_ptr.hwnd, GWL_STYLE, style.0 as isize) }; + unsafe { + SetWindowPos( + state_ptr.hwnd, + HWND::default(), + x, + y, + cx, + cy, + SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOZORDER, + ) + } + .log_err(); + }) + .detach(); + } + + fn is_fullscreen(&self) -> bool { + self.0.state.borrow().is_fullscreen() + } + + fn on_request_frame(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.request_frame = Some(callback); + } + + fn on_input(&self, callback: Box DispatchEventResult>) { + self.0.state.borrow_mut().callbacks.input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.active_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.state.borrow_mut().callbacks.resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.state.borrow_mut().callbacks.should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.close = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.appearance_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + self.0.state.borrow_mut().renderer.draw(scene) + } + + fn sprite_atlas(&self) -> Arc { + self.0.state.borrow().renderer.sprite_atlas().clone() + } + + fn get_raw_handle(&self) -> HWND { + self.0.hwnd + } +} + +#[implement(IDropTarget)] +struct WindowsDragDropHandler(pub Rc); + +impl WindowsDragDropHandler { + fn handle_drag_drop(&self, input: PlatformInput) { + let mut lock = self.0.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + drop(lock); + func(input); + self.0.state.borrow_mut().callbacks.input = Some(func); + } + } +} + +#[allow(non_snake_case)] +impl IDropTarget_Impl for WindowsDragDropHandler { + fn DragEnter( + &self, + pdataobj: Option<&IDataObject>, + _grfkeystate: MODIFIERKEYS_FLAGS, + pt: &POINTL, + pdweffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + unsafe { + let Some(idata_obj) = pdataobj else { + log::info!("no dragging file or directory detected"); + return Ok(()); + }; + let config = FORMATETC { + cfFormat: CF_HDROP.0, + ptd: std::ptr::null_mut() as _, + dwAspect: DVASPECT_CONTENT.0, + lindex: -1, + tymed: TYMED_HGLOBAL.0 as _, + }; + if idata_obj.QueryGetData(&config as _) == S_OK { + *pdweffect = DROPEFFECT_LINK; + let Some(mut idata) = idata_obj.GetData(&config as _).log_err() else { + return Ok(()); + }; + if idata.u.hGlobal.is_invalid() { + return Ok(()); + } + let hdrop = idata.u.hGlobal.0 as *mut HDROP; + let mut paths = SmallVec::<[PathBuf; 2]>::new(); + let file_count = DragQueryFileW(*hdrop, DRAGDROP_GET_FILES_COUNT, None); + for file_index in 0..file_count { + let filename_length = DragQueryFileW(*hdrop, file_index, None) as usize; + let mut buffer = vec![0u16; filename_length + 1]; + let ret = DragQueryFileW(*hdrop, file_index, Some(buffer.as_mut_slice())); + if ret == 0 { + log::error!("unable to read file name"); + continue; + } + if let Some(file_name) = + String::from_utf16(&buffer[0..filename_length]).log_err() + { + if let Some(path) = PathBuf::from_str(&file_name).log_err() { + paths.push(path); + } + } + } + ReleaseStgMedium(&mut idata); + let mut cursor_position = POINT { x: pt.x, y: pt.y }; + ScreenToClient(self.0.hwnd, &mut cursor_position); + let scale_factor = self.0.state.borrow().scale_factor; + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: logical_point( + cursor_position.x as f32, + cursor_position.y as f32, + scale_factor, + ), + paths: ExternalPaths(paths), + }); + self.handle_drag_drop(input); + } else { + *pdweffect = DROPEFFECT_NONE; + } + } + Ok(()) + } + + fn DragOver( + &self, + _grfkeystate: MODIFIERKEYS_FLAGS, + pt: &POINTL, + _pdweffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + let mut cursor_position = POINT { x: pt.x, y: pt.y }; + unsafe { + ScreenToClient(self.0.hwnd, &mut cursor_position); + } + let scale_factor = self.0.state.borrow().scale_factor; + let input = PlatformInput::FileDrop(FileDropEvent::Pending { + position: logical_point( + cursor_position.x as f32, + cursor_position.y as f32, + scale_factor, + ), + }); + self.handle_drag_drop(input); + + Ok(()) + } + + fn DragLeave(&self) -> windows::core::Result<()> { + let input = PlatformInput::FileDrop(FileDropEvent::Exited); + self.handle_drag_drop(input); + + Ok(()) + } + + fn Drop( + &self, + _pdataobj: Option<&IDataObject>, + _grfkeystate: MODIFIERKEYS_FLAGS, + pt: &POINTL, + _pdweffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + let mut cursor_position = POINT { x: pt.x, y: pt.y }; + unsafe { + ScreenToClient(self.0.hwnd, &mut cursor_position); + } + let scale_factor = self.0.state.borrow().scale_factor; + let input = PlatformInput::FileDrop(FileDropEvent::Submit { + position: logical_point( + cursor_position.x as f32, + cursor_position.y as f32, + scale_factor, + ), + }); + self.handle_drag_drop(input); + + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ClickState { + button: MouseButton, + last_click: Instant, + last_position: Point, + pub(crate) current_count: usize, +} + +impl ClickState { + pub fn new() -> Self { + ClickState { + button: MouseButton::Left, + last_click: Instant::now(), + last_position: Point::default(), + current_count: 0, + } + } + + /// update self and return the needed click count + pub fn update(&mut self, button: MouseButton, new_position: Point) -> usize { + if self.button == button && self.is_double_click(new_position) { + self.current_count += 1; + } else { + self.current_count = 1; + } + self.last_click = Instant::now(); + self.last_position = new_position; + self.button = button; + + self.current_count + } + + #[inline] + fn is_double_click(&self, new_position: Point) -> bool { + let diff = self.last_position - new_position; + + self.last_click.elapsed() < DOUBLE_CLICK_INTERVAL + && diff.x.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE + && diff.y.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE + } +} + +struct StyleAndBounds { + style: WINDOW_STYLE, + x: i32, + y: i32, + cx: i32, + cy: i32, +} + +fn register_wnd_class(icon_handle: HICON) -> PCWSTR { + const CLASS_NAME: PCWSTR = w!("Zed::Window"); + + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let wc = WNDCLASSW { + lpfnWndProc: Some(wnd_proc), + hIcon: icon_handle, + lpszClassName: PCWSTR(CLASS_NAME.as_ptr()), + style: CS_HREDRAW | CS_VREDRAW, + hInstance: get_module_handle().into(), + ..Default::default() + }; + unsafe { RegisterClassW(&wc) }; + }); + + CLASS_NAME +} + +unsafe extern "system" fn wnd_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + if msg == WM_NCCREATE { + let cs = lparam.0 as *const CREATESTRUCTW; + let cs = unsafe { &*cs }; + let ctx = cs.lpCreateParams as *mut WindowCreateContext; + let ctx = unsafe { &mut *ctx }; + let state_ptr = WindowsWindowStatePtr::new(ctx, hwnd, cs); + let weak = Box::new(Rc::downgrade(&state_ptr)); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + ctx.inner = Some(state_ptr); + return LRESULT(1); + } + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + if ptr.is_null() { + return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; + } + let inner = unsafe { &*ptr }; + let r = if let Some(state) = inner.upgrade() { + handle_msg(hwnd, msg, wparam, lparam, state) + } else { + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + }; + if msg == WM_NCDESTROY { + unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; + unsafe { drop(Box::from_raw(ptr)) }; + } + r +} + +pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option> { + if hwnd == HWND(0) { + return None; + } + + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + if !ptr.is_null() { + let inner = unsafe { &*ptr }; + inner.upgrade() + } else { + None + } +} + +fn get_module_handle() -> HMODULE { + unsafe { + let mut h_module = std::mem::zeroed(); + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + windows::core::w!("ZedModule"), + &mut h_module, + ) + .expect("Unable to get module handle"); // this should never fail + + h_module + } +} + +fn register_drag_drop(state_ptr: Rc) { + let window_handle = state_ptr.hwnd; + let handler = WindowsDragDropHandler(state_ptr); + // The lifetime of `IDropTarget` is handled by Windows, it wont release untill + // we call `RevokeDragDrop`. + // So, it's safe to drop it here. + let drag_drop_handler: IDropTarget = handler.into(); + unsafe { + RegisterDragDrop(window_handle, &drag_drop_handler) + .expect("unable to register drag-drop event") + }; +} + +// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew +const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF; +// https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN +const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500); +// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics +const DOUBLE_CLICK_SPATIAL_TOLERANCE: i32 = 4; + +mod windows_renderer { + use std::{num::NonZeroIsize, sync::Arc}; + + use blade_graphics as gpu; + use raw_window_handle as rwh; + use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::GWLP_HINSTANCE}; + + use crate::{ + get_window_long, + platform::blade::{BladeRenderer, BladeSurfaceConfig}, + }; + + pub(super) fn windows_renderer(hwnd: HWND, transparent: bool) -> BladeRenderer { + let raw = RawWindow { hwnd: hwnd.0 }; + let gpu: Arc = Arc::new( + unsafe { + gpu::Context::init_windowed( + &raw, + gpu::ContextDesc { + validation: false, + capture: false, + overlay: false, + }, + ) + } + .unwrap(), + ); + let config = BladeSurfaceConfig { + size: gpu::Extent::default(), + transparent, + }; + + BladeRenderer::new(gpu, config) + } + + struct RawWindow { + hwnd: isize, + } + + impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + Ok(unsafe { + let hwnd = NonZeroIsize::new_unchecked(self.hwnd); + let mut handle = rwh::Win32WindowHandle::new(hwnd); + let hinstance = get_window_long(HWND(self.hwnd), GWLP_HINSTANCE); + handle.hinstance = NonZeroIsize::new(hinstance); + rwh::WindowHandle::borrow_raw(handle.into()) + }) + } + } + + impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let handle = rwh::WindowsDisplayHandle::new(); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } + } +} + +#[cfg(test)] +mod tests { + use super::ClickState; + use crate::{point, DevicePixels, MouseButton}; + use std::time::Duration; + + #[test] + fn test_double_click_interval() { + let mut state = ClickState::new(); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Right, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 2 + ); + state.last_click -= Duration::from_millis(700); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), + 1 + ); + } + + #[test] + fn test_double_click_spatial_tolerance() { + let mut state = ClickState::new(); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(-3), DevicePixels(0))), + 1 + ); + assert_eq!( + state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(3))), + 2 + ); + assert_eq!( + state.update(MouseButton::Right, point(DevicePixels(3), DevicePixels(2))), + 1 + ); + assert_eq!( + state.update(MouseButton::Right, point(DevicePixels(10), DevicePixels(0))), + 1 + ); + } +} diff --git a/crates/ming/src/prelude.rs b/crates/ming/src/prelude.rs new file mode 100644 index 0000000..2ab115f --- /dev/null +++ b/crates/ming/src/prelude.rs @@ -0,0 +1,9 @@ +//! The GPUI prelude is a collection of traits and types that are widely used +//! throughout the library. It is recommended to import this prelude into your +//! application to avoid having to import each trait individually. + +pub use crate::{ + util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, + InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce, + StatefulInteractiveElement, Styled, VisualContext, +}; diff --git a/crates/ming/src/scene.rs b/crates/ming/src/scene.rs new file mode 100644 index 0000000..9d27c8f --- /dev/null +++ b/crates/ming/src/scene.rs @@ -0,0 +1,866 @@ +// todo("windows"): remove +#![cfg_attr(windows, allow(dead_code))] + +use crate::{ + bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, + Hsla, Pixels, Point, Radians, ScaledPixels, Size, +}; +use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; + +#[allow(non_camel_case_types, unused)] +pub(crate) type PathVertex_ScaledPixels = PathVertex; + +pub(crate) type DrawOrder = u32; + +#[derive(Default)] +pub(crate) struct Scene { + pub(crate) paint_operations: Vec, + primitive_bounds: BoundsTree, + layer_stack: Vec, + pub(crate) shadows: Vec, + pub(crate) quads: Vec, + pub(crate) paths: Vec>, + pub(crate) underlines: Vec, + pub(crate) monochrome_sprites: Vec, + pub(crate) polychrome_sprites: Vec, + pub(crate) surfaces: Vec, +} + +impl Scene { + pub fn clear(&mut self) { + self.paint_operations.clear(); + self.primitive_bounds.clear(); + self.layer_stack.clear(); + self.paths.clear(); + self.shadows.clear(); + self.quads.clear(); + self.underlines.clear(); + self.monochrome_sprites.clear(); + self.polychrome_sprites.clear(); + self.surfaces.clear(); + } + + pub fn paths(&self) -> &[Path] { + &self.paths + } + + pub fn len(&self) -> usize { + self.paint_operations.len() + } + + pub fn push_layer(&mut self, bounds: Bounds) { + let order = self.primitive_bounds.insert(bounds); + self.layer_stack.push(order); + self.paint_operations + .push(PaintOperation::StartLayer(bounds)); + } + + pub fn pop_layer(&mut self) { + self.layer_stack.pop(); + self.paint_operations.push(PaintOperation::EndLayer); + } + + pub fn insert_primitive(&mut self, primitive: impl Into) { + let mut primitive = primitive.into(); + let clipped_bounds = primitive + .bounds() + .intersect(&primitive.content_mask().bounds); + + if clipped_bounds.is_empty() { + return; + } + + let order = self + .layer_stack + .last() + .copied() + .unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds)); + match &mut primitive { + Primitive::Shadow(shadow) => { + shadow.order = order; + self.shadows.push(shadow.clone()); + } + Primitive::Quad(quad) => { + quad.order = order; + self.quads.push(quad.clone()); + } + Primitive::Path(path) => { + path.order = order; + path.id = PathId(self.paths.len()); + self.paths.push(path.clone()); + } + Primitive::Underline(underline) => { + underline.order = order; + self.underlines.push(underline.clone()); + } + Primitive::MonochromeSprite(sprite) => { + sprite.order = order; + self.monochrome_sprites.push(sprite.clone()); + } + Primitive::PolychromeSprite(sprite) => { + sprite.order = order; + self.polychrome_sprites.push(sprite.clone()); + } + Primitive::Surface(surface) => { + surface.order = order; + self.surfaces.push(surface.clone()); + } + } + self.paint_operations + .push(PaintOperation::Primitive(primitive)); + } + + pub fn replay(&mut self, range: Range, prev_scene: &Scene) { + for operation in &prev_scene.paint_operations[range] { + match operation { + PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()), + PaintOperation::StartLayer(bounds) => self.push_layer(*bounds), + PaintOperation::EndLayer => self.pop_layer(), + } + } + } + + pub fn finish(&mut self) { + self.shadows.sort(); + self.quads.sort(); + self.paths.sort(); + self.underlines.sort(); + self.monochrome_sprites.sort(); + self.polychrome_sprites.sort(); + self.surfaces.sort(); + } + + pub(crate) fn batches(&self) -> impl Iterator { + BatchIterator { + shadows: &self.shadows, + shadows_start: 0, + shadows_iter: self.shadows.iter().peekable(), + quads: &self.quads, + quads_start: 0, + quads_iter: self.quads.iter().peekable(), + paths: &self.paths, + paths_start: 0, + paths_iter: self.paths.iter().peekable(), + underlines: &self.underlines, + underlines_start: 0, + underlines_iter: self.underlines.iter().peekable(), + monochrome_sprites: &self.monochrome_sprites, + monochrome_sprites_start: 0, + monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(), + polychrome_sprites: &self.polychrome_sprites, + polychrome_sprites_start: 0, + polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), + surfaces: &self.surfaces, + surfaces_start: 0, + surfaces_iter: self.surfaces.iter().peekable(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] +pub(crate) enum PrimitiveKind { + Shadow, + #[default] + Quad, + Path, + Underline, + MonochromeSprite, + PolychromeSprite, + Surface, +} + +pub(crate) enum PaintOperation { + Primitive(Primitive), + StartLayer(Bounds), + EndLayer, +} + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)] +pub(crate) enum Primitive { + Shadow(Shadow), + Quad(Quad), + Path(Path), + Underline(Underline), + MonochromeSprite(MonochromeSprite), + PolychromeSprite(PolychromeSprite), + Surface(Surface), +} + +impl Primitive { + pub fn bounds(&self) -> &Bounds { + match self { + Primitive::Shadow(shadow) => &shadow.bounds, + Primitive::Quad(quad) => &quad.bounds, + Primitive::Path(path) => &path.bounds, + Primitive::Underline(underline) => &underline.bounds, + Primitive::MonochromeSprite(sprite) => &sprite.bounds, + Primitive::PolychromeSprite(sprite) => &sprite.bounds, + Primitive::Surface(surface) => &surface.bounds, + } + } + + pub fn content_mask(&self) -> &ContentMask { + match self { + Primitive::Shadow(shadow) => &shadow.content_mask, + Primitive::Quad(quad) => &quad.content_mask, + Primitive::Path(path) => &path.content_mask, + Primitive::Underline(underline) => &underline.content_mask, + Primitive::MonochromeSprite(sprite) => &sprite.content_mask, + Primitive::PolychromeSprite(sprite) => &sprite.content_mask, + Primitive::Surface(surface) => &surface.content_mask, + } + } +} + +struct BatchIterator<'a> { + shadows: &'a [Shadow], + shadows_start: usize, + shadows_iter: Peekable>, + quads: &'a [Quad], + quads_start: usize, + quads_iter: Peekable>, + paths: &'a [Path], + paths_start: usize, + paths_iter: Peekable>>, + underlines: &'a [Underline], + underlines_start: usize, + underlines_iter: Peekable>, + monochrome_sprites: &'a [MonochromeSprite], + monochrome_sprites_start: usize, + monochrome_sprites_iter: Peekable>, + polychrome_sprites: &'a [PolychromeSprite], + polychrome_sprites_start: usize, + polychrome_sprites_iter: Peekable>, + surfaces: &'a [Surface], + surfaces_start: usize, + surfaces_iter: Peekable>, +} + +impl<'a> Iterator for BatchIterator<'a> { + type Item = PrimitiveBatch<'a>; + + fn next(&mut self) -> Option { + let mut orders_and_kinds = [ + ( + self.shadows_iter.peek().map(|s| s.order), + PrimitiveKind::Shadow, + ), + (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad), + (self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path), + ( + self.underlines_iter.peek().map(|u| u.order), + PrimitiveKind::Underline, + ), + ( + self.monochrome_sprites_iter.peek().map(|s| s.order), + PrimitiveKind::MonochromeSprite, + ), + ( + self.polychrome_sprites_iter.peek().map(|s| s.order), + PrimitiveKind::PolychromeSprite, + ), + ( + self.surfaces_iter.peek().map(|s| s.order), + PrimitiveKind::Surface, + ), + ]; + orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); + + let first = orders_and_kinds[0]; + let second = orders_and_kinds[1]; + let (batch_kind, max_order_and_kind) = if first.0.is_some() { + (first.1, (second.0.unwrap_or(u32::MAX), second.1)) + } else { + return None; + }; + + match batch_kind { + PrimitiveKind::Shadow => { + let shadows_start = self.shadows_start; + let mut shadows_end = shadows_start + 1; + self.shadows_iter.next(); + while self + .shadows_iter + .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind) + .is_some() + { + shadows_end += 1; + } + self.shadows_start = shadows_end; + Some(PrimitiveBatch::Shadows( + &self.shadows[shadows_start..shadows_end], + )) + } + PrimitiveKind::Quad => { + let quads_start = self.quads_start; + let mut quads_end = quads_start + 1; + self.quads_iter.next(); + while self + .quads_iter + .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind) + .is_some() + { + quads_end += 1; + } + self.quads_start = quads_end; + Some(PrimitiveBatch::Quads(&self.quads[quads_start..quads_end])) + } + PrimitiveKind::Path => { + let paths_start = self.paths_start; + let mut paths_end = paths_start + 1; + self.paths_iter.next(); + while self + .paths_iter + .next_if(|path| (path.order, batch_kind) < max_order_and_kind) + .is_some() + { + paths_end += 1; + } + self.paths_start = paths_end; + Some(PrimitiveBatch::Paths(&self.paths[paths_start..paths_end])) + } + PrimitiveKind::Underline => { + let underlines_start = self.underlines_start; + let mut underlines_end = underlines_start + 1; + self.underlines_iter.next(); + while self + .underlines_iter + .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind) + .is_some() + { + underlines_end += 1; + } + self.underlines_start = underlines_end; + Some(PrimitiveBatch::Underlines( + &self.underlines[underlines_start..underlines_end], + )) + } + PrimitiveKind::MonochromeSprite => { + let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id; + let sprites_start = self.monochrome_sprites_start; + let mut sprites_end = sprites_start + 1; + self.monochrome_sprites_iter.next(); + while self + .monochrome_sprites_iter + .next_if(|sprite| { + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id + }) + .is_some() + { + sprites_end += 1; + } + self.monochrome_sprites_start = sprites_end; + Some(PrimitiveBatch::MonochromeSprites { + texture_id, + sprites: &self.monochrome_sprites[sprites_start..sprites_end], + }) + } + PrimitiveKind::PolychromeSprite => { + let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id; + let sprites_start = self.polychrome_sprites_start; + let mut sprites_end = self.polychrome_sprites_start + 1; + self.polychrome_sprites_iter.next(); + while self + .polychrome_sprites_iter + .next_if(|sprite| { + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id + }) + .is_some() + { + sprites_end += 1; + } + self.polychrome_sprites_start = sprites_end; + Some(PrimitiveBatch::PolychromeSprites { + texture_id, + sprites: &self.polychrome_sprites[sprites_start..sprites_end], + }) + } + PrimitiveKind::Surface => { + let surfaces_start = self.surfaces_start; + let mut surfaces_end = surfaces_start + 1; + self.surfaces_iter.next(); + while self + .surfaces_iter + .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind) + .is_some() + { + surfaces_end += 1; + } + self.surfaces_start = surfaces_end; + Some(PrimitiveBatch::Surfaces( + &self.surfaces[surfaces_start..surfaces_end], + )) + } + } + } +} + +#[derive(Debug)] +pub(crate) enum PrimitiveBatch<'a> { + Shadows(&'a [Shadow]), + Quads(&'a [Quad]), + Paths(&'a [Path]), + Underlines(&'a [Underline]), + MonochromeSprites { + texture_id: AtlasTextureId, + sprites: &'a [MonochromeSprite], + }, + PolychromeSprites { + texture_id: AtlasTextureId, + sprites: &'a [PolychromeSprite], + }, + Surfaces(&'a [Surface]), +} + +#[derive(Default, Debug, Clone, Eq, PartialEq)] +#[repr(C)] +pub(crate) struct Quad { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub background: Hsla, + pub border_color: Hsla, + pub corner_radii: Corners, + pub border_widths: Edges, +} + +impl Ord for Quad { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.order.cmp(&other.order) + } +} + +impl PartialOrd for Quad { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(quad: Quad) -> Self { + Primitive::Quad(quad) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[repr(C)] +pub(crate) struct Underline { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub thickness: ScaledPixels, + pub wavy: bool, +} + +impl Ord for Underline { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.order.cmp(&other.order) + } +} + +impl PartialOrd for Underline { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(underline: Underline) -> Self { + Primitive::Underline(underline) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[repr(C)] +pub(crate) struct Shadow { + pub order: DrawOrder, + pub blur_radius: ScaledPixels, + pub bounds: Bounds, + pub corner_radii: Corners, + pub content_mask: ContentMask, + pub color: Hsla, +} + +impl Ord for Shadow { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.order.cmp(&other.order) + } +} + +impl PartialOrd for Shadow { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(shadow: Shadow) -> Self { + Primitive::Shadow(shadow) + } +} + +/// A data type representing a 2 dimensional transformation that can be applied to an element. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct TransformationMatrix { + /// 2x2 matrix containing rotation and scale, + /// stored row-major + pub rotation_scale: [[f32; 2]; 2], + /// translation vector + pub translation: [f32; 2], +} + +impl Eq for TransformationMatrix {} + +impl TransformationMatrix { + /// The unit matrix, has no effect. + pub fn unit() -> Self { + Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [0.0, 0.0], + } + } + + /// Move the origin by a given point + pub fn translate(mut self, point: Point) -> Self { + self.compose(Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [point.x.0, point.y.0], + }) + } + + /// Clockwise rotation in radians around the origin + pub fn rotate(self, angle: Radians) -> Self { + self.compose(Self { + rotation_scale: [ + [angle.0.cos(), -angle.0.sin()], + [angle.0.sin(), angle.0.cos()], + ], + translation: [0.0, 0.0], + }) + } + + /// Scale around the origin + pub fn scale(self, size: Size) -> Self { + self.compose(Self { + rotation_scale: [[size.width, 0.0], [0.0, size.height]], + translation: [0.0, 0.0], + }) + } + + /// Perform matrix multiplication with another transformation + /// to produce a new transformation that is the result of + /// applying both transformations: first, `other`, then `self`. + #[inline] + pub fn compose(self, other: TransformationMatrix) -> TransformationMatrix { + if other == Self::unit() { + return self; + } + // Perform matrix multiplication + TransformationMatrix { + rotation_scale: [ + [ + self.rotation_scale[0][0] * other.rotation_scale[0][0] + + self.rotation_scale[0][1] * other.rotation_scale[1][0], + self.rotation_scale[0][0] * other.rotation_scale[0][1] + + self.rotation_scale[0][1] * other.rotation_scale[1][1], + ], + [ + self.rotation_scale[1][0] * other.rotation_scale[0][0] + + self.rotation_scale[1][1] * other.rotation_scale[1][0], + self.rotation_scale[1][0] * other.rotation_scale[0][1] + + self.rotation_scale[1][1] * other.rotation_scale[1][1], + ], + ], + translation: [ + self.translation[0] + + self.rotation_scale[0][0] * other.translation[0] + + self.rotation_scale[0][1] * other.translation[1], + self.translation[1] + + self.rotation_scale[1][0] * other.translation[0] + + self.rotation_scale[1][1] * other.translation[1], + ], + } + } + + /// Apply transformation to a point, mainly useful for debugging + pub fn apply(&self, point: Point) -> Point { + let input = [point.x.0, point.y.0]; + let mut output = self.translation; + for i in 0..2 { + for k in 0..2 { + output[i] += self.rotation_scale[i][k] * input[k]; + } + } + Point::new(output[0].into(), output[1].into()) + } +} + +impl Default for TransformationMatrix { + fn default() -> Self { + Self::unit() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub(crate) struct MonochromeSprite { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub tile: AtlasTile, + pub transformation: TransformationMatrix, +} + +impl Ord for MonochromeSprite { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.order.cmp(&other.order) { + std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id), + order => order, + } + } +} + +impl PartialOrd for MonochromeSprite { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(sprite: MonochromeSprite) -> Self { + Primitive::MonochromeSprite(sprite) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub(crate) struct PolychromeSprite { + pub order: DrawOrder, + pub grayscale: bool, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub corner_radii: Corners, + pub tile: AtlasTile, +} + +impl Ord for PolychromeSprite { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.order.cmp(&other.order) { + std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id), + order => order, + } + } +} + +impl PartialOrd for PolychromeSprite { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(sprite: PolychromeSprite) -> Self { + Primitive::PolychromeSprite(sprite) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct Surface { + pub order: DrawOrder, + pub bounds: Bounds, + pub content_mask: ContentMask, + #[cfg(target_os = "macos")] + pub image_buffer: media::core_video::CVImageBuffer, +} + +impl Ord for Surface { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.order.cmp(&other.order) + } +} + +impl PartialOrd for Surface { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(surface: Surface) -> Self { + Primitive::Surface(surface) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct PathId(pub(crate) usize); + +/// A line made up of a series of vertices and control points. +#[derive(Clone, Debug)] +pub struct Path { + pub(crate) id: PathId, + order: DrawOrder, + pub(crate) bounds: Bounds

, + pub(crate) content_mask: ContentMask

, + pub(crate) vertices: Vec>, + pub(crate) color: Hsla, + start: Point

, + current: Point

, + contour_count: usize, +} + +impl Path { + /// Create a new path with the given starting point. + pub fn new(start: Point) -> Self { + Self { + id: PathId(0), + order: DrawOrder::default(), + vertices: Vec::new(), + start, + current: start, + bounds: Bounds { + origin: start, + size: Default::default(), + }, + content_mask: Default::default(), + color: Default::default(), + contour_count: 0, + } + } + + /// Scale this path by the given factor. + pub fn scale(&self, factor: f32) -> Path { + Path { + id: self.id, + order: self.order, + bounds: self.bounds.scale(factor), + content_mask: self.content_mask.scale(factor), + vertices: self + .vertices + .iter() + .map(|vertex| vertex.scale(factor)) + .collect(), + start: self.start.map(|start| start.scale(factor)), + current: self.current.scale(factor), + contour_count: self.contour_count, + color: self.color, + } + } + + /// Draw a straight line from the current point to the given point. + pub fn line_to(&mut self, to: Point) { + self.contour_count += 1; + if self.contour_count > 1 { + self.push_triangle( + (self.start, self.current, to), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } + self.current = to; + } + + /// Draw a curve from the current point to the given point, using the given control point. + pub fn curve_to(&mut self, to: Point, ctrl: Point) { + self.contour_count += 1; + if self.contour_count > 1 { + self.push_triangle( + (self.start, self.current, to), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } + + self.push_triangle( + (self.current, ctrl, to), + (point(0., 0.), point(0.5, 0.), point(1., 1.)), + ); + self.current = to; + } + + fn push_triangle( + &mut self, + xy: (Point, Point, Point), + st: (Point, Point, Point), + ) { + self.bounds = self + .bounds + .union(&Bounds { + origin: xy.0, + size: Default::default(), + }) + .union(&Bounds { + origin: xy.1, + size: Default::default(), + }) + .union(&Bounds { + origin: xy.2, + size: Default::default(), + }); + + self.vertices.push(PathVertex { + xy_position: xy.0, + st_position: st.0, + content_mask: Default::default(), + }); + self.vertices.push(PathVertex { + xy_position: xy.1, + st_position: st.1, + content_mask: Default::default(), + }); + self.vertices.push(PathVertex { + xy_position: xy.2, + st_position: st.2, + content_mask: Default::default(), + }); + } +} + +impl Eq for Path {} + +impl PartialEq for Path { + fn eq(&self, other: &Self) -> bool { + self.order == other.order + } +} + +impl Ord for Path { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.order.cmp(&other.order) + } +} + +impl PartialOrd for Path { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From> for Primitive { + fn from(path: Path) -> Self { + Primitive::Path(path) + } +} + +#[derive(Clone, Debug)] +#[repr(C)] +pub(crate) struct PathVertex { + pub(crate) xy_position: Point

, + pub(crate) st_position: Point, + pub(crate) content_mask: ContentMask

, +} + +impl PathVertex { + pub fn scale(&self, factor: f32) -> PathVertex { + PathVertex { + xy_position: self.xy_position.scale(factor), + st_position: self.st_position, + content_mask: self.content_mask.scale(factor), + } + } +} diff --git a/crates/ming/src/shared_string.rs b/crates/ming/src/shared_string.rs new file mode 100644 index 0000000..1aa1bca --- /dev/null +++ b/crates/ming/src/shared_string.rs @@ -0,0 +1,103 @@ +use derive_more::{Deref, DerefMut}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Borrow, sync::Arc}; +use util::arc_cow::ArcCow; + +/// A shared string is an immutable string that can be cheaply cloned in GPUI +/// tasks. Essentially an abstraction over an `Arc` and `&'static str`, +#[derive(Deref, DerefMut, Eq, PartialEq, PartialOrd, Ord, Hash, Clone)] +pub struct SharedString(ArcCow<'static, str>); + +impl Default for SharedString { + fn default() -> Self { + Self(ArcCow::Owned("".into())) + } +} + +impl AsRef for SharedString { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Borrow for SharedString { + fn borrow(&self) -> &str { + self.as_ref() + } +} + +impl std::fmt::Debug for SharedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Display for SharedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} + +impl PartialEq for SharedString { + fn eq(&self, other: &String) -> bool { + self.as_ref() == other + } +} + +impl PartialEq for String { + fn eq(&self, other: &SharedString) -> bool { + self == other.as_ref() + } +} + +impl PartialEq for SharedString { + fn eq(&self, other: &str) -> bool { + self.as_ref() == other + } +} + +impl<'a> PartialEq<&'a str> for SharedString { + fn eq(&self, other: &&'a str) -> bool { + self.as_ref() == *other + } +} + +impl From for Arc { + fn from(val: SharedString) -> Self { + match val.0 { + ArcCow::Borrowed(borrowed) => Arc::from(borrowed), + ArcCow::Owned(owned) => owned.clone(), + } + } +} + +impl>> From for SharedString { + fn from(value: T) -> Self { + Self(value.into()) + } +} + +impl From for String { + fn from(val: SharedString) -> Self { + val.0.to_string() + } +} + +impl Serialize for SharedString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} + +impl<'de> Deserialize<'de> for SharedString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(SharedString::from(s)) + } +} diff --git a/crates/ming/src/shared_uri.rs b/crates/ming/src/shared_uri.rs new file mode 100644 index 0000000..e257aaf --- /dev/null +++ b/crates/ming/src/shared_uri.rs @@ -0,0 +1,25 @@ +use derive_more::{Deref, DerefMut}; + +use crate::SharedString; + +/// A [`SharedString`] containing a URI. +#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)] +pub struct SharedUri(SharedString); + +impl std::fmt::Debug for SharedUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Display for SharedUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} + +impl> From for SharedUri { + fn from(value: T) -> Self { + Self(value.into()) + } +} diff --git a/crates/ming/src/style.rs b/crates/ming/src/style.rs new file mode 100644 index 0000000..49111f4 --- /dev/null +++ b/crates/ming/src/style.rs @@ -0,0 +1,853 @@ +use std::{iter, mem, ops::Range}; + +use crate::{ + black, phi, point, quad, rems, AbsoluteLength, Bounds, ContentMask, Corners, CornersRefinement, + CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, + Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, + TextRun, WindowContext, +}; +use collections::HashSet; +use refineable::Refineable; +use smallvec::SmallVec; +pub use taffy::style::{ + AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent, + Overflow, Position, +}; + +/// Use this struct for interfacing with the 'debug_below' styling from your own elements. +/// If a parent element has this style set on it, then this struct will be set as a global in +/// GPUI. +#[cfg(debug_assertions)] +pub struct DebugBelow; + +#[cfg(debug_assertions)] +impl crate::Global for DebugBelow {} + +/// The CSS styling that can be applied to an element via the `Styled` trait +#[derive(Clone, Refineable, Debug)] +#[refineable(Debug)] +pub struct Style { + /// What layout strategy should be used? + pub display: Display, + + /// Should the element be painted on screen? + pub visibility: Visibility, + + // Overflow properties + /// How children overflowing their container should affect layout + #[refineable] + pub overflow: Point, + /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. + pub scrollbar_width: f32, + + // Position properties + /// What should the `position` value of this struct use as a base offset? + pub position: Position, + /// How should the position of this element be tweaked relative to the layout defined? + #[refineable] + pub inset: Edges, + + // Size properties + /// Sets the initial size of the item + #[refineable] + pub size: Size, + /// Controls the minimum size of the item + #[refineable] + pub min_size: Size, + /// Controls the maximum size of the item + #[refineable] + pub max_size: Size, + /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height. + pub aspect_ratio: Option, + + // Spacing Properties + /// How large should the margin be on each side? + #[refineable] + pub margin: Edges, + /// How large should the padding be on each side? + #[refineable] + pub padding: Edges, + /// How large should the border be on each side? + #[refineable] + pub border_widths: Edges, + + // Alignment properties + /// How this node's children aligned in the cross/block axis? + pub align_items: Option, + /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set + pub align_self: Option, + /// How should content contained within this item be aligned in the cross/block axis + pub align_content: Option, + /// How should contained within this item be aligned in the main/inline axis + pub justify_content: Option, + /// How large should the gaps between items in a flex container be? + #[refineable] + pub gap: Size, + + // Flexbox properties + /// Which direction does the main axis flow in? + pub flex_direction: FlexDirection, + /// Should elements wrap, or stay in a single line? + pub flex_wrap: FlexWrap, + /// Sets the initial main axis size of the item + pub flex_basis: Length, + /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive. + pub flex_grow: f32, + /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive. + pub flex_shrink: f32, + + /// The fill color of this element + pub background: Option, + + /// The border color of this element + pub border_color: Option, + + /// The radius of the corners of this element + #[refineable] + pub corner_radii: Corners, + + /// Box Shadow of the element + pub box_shadow: SmallVec<[BoxShadow; 2]>, + + /// The text style of this element + pub text: TextStyleRefinement, + + /// The mouse cursor style shown when the mouse pointer is over an element. + pub mouse_cursor: Option, + + /// Whether to draw a red debugging outline around this element + #[cfg(debug_assertions)] + pub debug: bool, + + /// Whether to draw a red debugging outline around this element and all of its conforming children + #[cfg(debug_assertions)] + pub debug_below: bool, +} + +impl Styled for StyleRefinement { + fn style(&mut self) -> &mut StyleRefinement { + self + } +} + +/// The value of the visibility property, similar to the CSS property `visibility` +#[derive(Default, Clone, Copy, Debug, Eq, PartialEq)] +pub enum Visibility { + /// The element should be drawn as normal. + #[default] + Visible, + /// The element should not be drawn, but should still take up space in the layout. + Hidden, +} + +/// The possible values of the box-shadow property +#[derive(Clone, Debug)] +pub struct BoxShadow { + /// What color should the shadow have? + pub color: Hsla, + /// How should it be offset from its element? + pub offset: Point, + /// How much should the shadow be blurred? + pub blur_radius: Pixels, + /// How much should the shadow spread? + pub spread_radius: Pixels, +} + +/// How to handle whitespace in text +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum WhiteSpace { + /// Normal line wrapping when text overflows the width of the element + #[default] + Normal, + /// No line wrapping, text will overflow the width of the element + Nowrap, +} + +/// The properties that can be used to style text in GPUI +#[derive(Refineable, Clone, Debug, PartialEq)] +#[refineable(Debug)] +pub struct TextStyle { + /// The color of the text + pub color: Hsla, + + /// The font family to use + pub font_family: SharedString, + + /// The font features to use + pub font_features: FontFeatures, + + /// The font size to use, in pixels or rems. + pub font_size: AbsoluteLength, + + /// The line height to use, in pixels or fractions + pub line_height: DefiniteLength, + + /// The font weight, e.g. bold + pub font_weight: FontWeight, + + /// The font style, e.g. italic + pub font_style: FontStyle, + + /// The background color of the text + pub background_color: Option, + + /// The underline style of the text + pub underline: Option, + + /// The strikethrough style of the text + pub strikethrough: Option, + + /// How to handle whitespace in the text + pub white_space: WhiteSpace, +} + +impl Default for TextStyle { + fn default() -> Self { + TextStyle { + color: black(), + // todo(linux) make this configurable or choose better default + font_family: if cfg!(target_os = "linux") { + "FreeMono".into() + } else { + "Helvetica".into() + }, + font_features: FontFeatures::default(), + font_size: rems(1.).into(), + line_height: phi(), + font_weight: FontWeight::default(), + font_style: FontStyle::default(), + background_color: None, + underline: None, + strikethrough: None, + white_space: WhiteSpace::Normal, + } + } +} + +impl TextStyle { + /// Create a new text style with the given highlighting applied. + pub fn highlight(mut self, style: impl Into) -> Self { + let style = style.into(); + if let Some(weight) = style.font_weight { + self.font_weight = weight; + } + if let Some(style) = style.font_style { + self.font_style = style; + } + + if let Some(color) = style.color { + self.color = self.color.blend(color); + } + + if let Some(factor) = style.fade_out { + self.color.fade_out(factor); + } + + if let Some(background_color) = style.background_color { + self.background_color = Some(background_color); + } + + if let Some(underline) = style.underline { + self.underline = Some(underline); + } + + if let Some(strikethrough) = style.strikethrough { + self.strikethrough = Some(strikethrough); + } + + self + } + + /// Get the font configured for this text style. + pub fn font(&self) -> Font { + Font { + family: self.font_family.clone(), + features: self.font_features.clone(), + weight: self.font_weight, + style: self.font_style, + } + } + + /// Returns the rounded line height in pixels. + pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels { + self.line_height.to_pixels(self.font_size, rem_size).round() + } + + /// Convert this text style into a [`TextRun`], for the given length of the text. + pub fn to_run(&self, len: usize) -> TextRun { + TextRun { + len, + font: Font { + family: self.font_family.clone(), + features: Default::default(), + weight: self.font_weight, + style: self.font_style, + }, + color: self.color, + background_color: self.background_color, + underline: self.underline, + strikethrough: self.strikethrough, + } + } +} + +/// A highlight style to apply, similar to a `TextStyle` except +/// for a single font, uniformly sized and spaced text. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct HighlightStyle { + /// The color of the text + pub color: Option, + + /// The font weight, e.g. bold + pub font_weight: Option, + + /// The font style, e.g. italic + pub font_style: Option, + + /// The background color of the text + pub background_color: Option, + + /// The underline style of the text + pub underline: Option, + + /// The underline style of the text + pub strikethrough: Option, + + /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant. + pub fade_out: Option, +} + +impl Eq for HighlightStyle {} + +impl Style { + /// Returns true if the style is visible and the background is opaque. + pub fn has_opaque_background(&self) -> bool { + self.background + .as_ref() + .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) + } + + /// Get the text style in this element style. + pub fn text_style(&self) -> Option<&TextStyleRefinement> { + if self.text.is_some() { + Some(&self.text) + } else { + None + } + } + + /// Get the content mask for this element style, based on the given bounds. + /// If the element does not hide its overflow, this will return `None`. + pub fn overflow_mask( + &self, + bounds: Bounds, + rem_size: Pixels, + ) -> Option> { + match self.overflow { + Point { + x: Overflow::Visible, + y: Overflow::Visible, + } => None, + _ => { + let mut min = bounds.origin; + let mut max = bounds.lower_right(); + + if self + .border_color + .map_or(false, |color| !color.is_transparent()) + { + min.x += self.border_widths.left.to_pixels(rem_size); + max.x -= self.border_widths.right.to_pixels(rem_size); + min.y += self.border_widths.top.to_pixels(rem_size); + max.y -= self.border_widths.bottom.to_pixels(rem_size); + } + + let bounds = match ( + self.overflow.x == Overflow::Visible, + self.overflow.y == Overflow::Visible, + ) { + // x and y both visible + (true, true) => return None, + // x visible, y hidden + (true, false) => Bounds::from_corners( + point(min.x, bounds.origin.y), + point(max.x, bounds.lower_right().y), + ), + // x hidden, y visible + (false, true) => Bounds::from_corners( + point(bounds.origin.x, min.y), + point(bounds.lower_right().x, max.y), + ), + // both hidden + (false, false) => Bounds::from_corners(min, max), + }; + + Some(ContentMask { bounds }) + } + } + } + + /// Paints the background of an element styled with this style. + pub fn paint( + &self, + bounds: Bounds, + cx: &mut WindowContext, + continuation: impl FnOnce(&mut WindowContext), + ) { + #[cfg(debug_assertions)] + if self.debug_below { + cx.set_global(DebugBelow) + } + + #[cfg(debug_assertions)] + if self.debug || cx.has_global::() { + cx.paint_quad(crate::outline(bounds, crate::red())); + } + + let rem_size = cx.rem_size(); + + cx.paint_shadows( + bounds, + self.corner_radii.to_pixels(bounds.size, rem_size), + &self.box_shadow, + ); + + let background_color = self.background.as_ref().and_then(Fill::color); + if background_color.map_or(false, |color| !color.is_transparent()) { + let mut border_color = background_color.unwrap_or_default(); + border_color.a = 0.; + cx.paint_quad(quad( + bounds, + self.corner_radii.to_pixels(bounds.size, rem_size), + background_color.unwrap_or_default(), + Edges::default(), + border_color, + )); + } + + continuation(cx); + + if self.is_border_visible() { + let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size); + let border_widths = self.border_widths.to_pixels(rem_size); + let max_border_width = border_widths.max(); + let max_corner_radius = corner_radii.max(); + + let top_bounds = Bounds::from_corners( + bounds.origin, + bounds.upper_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + ); + let bottom_bounds = Bounds::from_corners( + bounds.lower_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + bounds.lower_right(), + ); + let left_bounds = Bounds::from_corners( + top_bounds.lower_left(), + bottom_bounds.origin + point(max_border_width, Pixels::ZERO), + ); + let right_bounds = Bounds::from_corners( + top_bounds.lower_right() - point(max_border_width, Pixels::ZERO), + bottom_bounds.upper_right(), + ); + + let mut background = self.border_color.unwrap_or_default(); + background.a = 0.; + let quad = quad( + bounds, + corner_radii, + background, + border_widths, + self.border_color.unwrap_or_default(), + ); + + cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| { + cx.paint_quad(quad.clone()); + }); + cx.with_content_mask( + Some(ContentMask { + bounds: right_bounds, + }), + |cx| { + cx.paint_quad(quad.clone()); + }, + ); + cx.with_content_mask( + Some(ContentMask { + bounds: bottom_bounds, + }), + |cx| { + cx.paint_quad(quad.clone()); + }, + ); + cx.with_content_mask( + Some(ContentMask { + bounds: left_bounds, + }), + |cx| { + cx.paint_quad(quad); + }, + ); + } + + #[cfg(debug_assertions)] + if self.debug_below { + cx.remove_global::(); + } + } + + fn is_border_visible(&self) -> bool { + self.border_color + .map_or(false, |color| !color.is_transparent()) + && self.border_widths.any(|length| !length.is_zero()) + } +} + +impl Default for Style { + fn default() -> Self { + Style { + display: Display::Block, + visibility: Visibility::Visible, + overflow: Point { + x: Overflow::Visible, + y: Overflow::Visible, + }, + scrollbar_width: 0.0, + position: Position::Relative, + inset: Edges::auto(), + margin: Edges::::zero(), + padding: Edges::::zero(), + border_widths: Edges::::zero(), + size: Size::auto(), + min_size: Size::auto(), + max_size: Size::auto(), + aspect_ratio: None, + gap: Size::default(), + // Alignment + align_items: None, + align_self: None, + align_content: None, + justify_content: None, + // Flexbox + flex_direction: FlexDirection::Row, + flex_wrap: FlexWrap::NoWrap, + flex_grow: 0.0, + flex_shrink: 1.0, + flex_basis: Length::Auto, + background: None, + border_color: None, + corner_radii: Corners::default(), + box_shadow: Default::default(), + text: TextStyleRefinement::default(), + mouse_cursor: None, + + #[cfg(debug_assertions)] + debug: false, + #[cfg(debug_assertions)] + debug_below: false, + } + } +} + +/// The properties that can be applied to an underline. +#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq)] +#[refineable(Debug)] +pub struct UnderlineStyle { + /// The thickness of the underline. + pub thickness: Pixels, + + /// The color of the underline. + pub color: Option, + + /// Whether the underline should be wavy, like in a spell checker. + pub wavy: bool, +} + +/// The properties that can be applied to a strikethrough. +#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq)] +#[refineable(Debug)] +pub struct StrikethroughStyle { + /// The thickness of the strikethrough. + pub thickness: Pixels, + + /// The color of the strikethrough. + pub color: Option, +} + +/// The kinds of fill that can be applied to a shape. +#[derive(Clone, Debug)] +pub enum Fill { + /// A solid color fill. + Color(Hsla), +} + +impl Fill { + /// Unwrap this fill into a solid color, if it is one. + pub fn color(&self) -> Option { + match self { + Fill::Color(color) => Some(*color), + } + } +} + +impl Default for Fill { + fn default() -> Self { + Self::Color(Hsla::default()) + } +} + +impl From for Fill { + fn from(color: Hsla) -> Self { + Self::Color(color) + } +} + +impl From for Fill { + fn from(color: Rgba) -> Self { + Self::Color(color.into()) + } +} + +impl From for HighlightStyle { + fn from(other: TextStyle) -> Self { + Self::from(&other) + } +} + +impl From<&TextStyle> for HighlightStyle { + fn from(other: &TextStyle) -> Self { + Self { + color: Some(other.color), + font_weight: Some(other.font_weight), + font_style: Some(other.font_style), + background_color: other.background_color, + underline: other.underline, + strikethrough: other.strikethrough, + fade_out: None, + } + } +} + +impl HighlightStyle { + /// Create a highlight style with just a color + pub fn color(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } + /// Blend this highlight style with another. + /// Non-continuous properties, like font_weight and font_style, are overwritten. + pub fn highlight(&mut self, other: HighlightStyle) { + match (self.color, other.color) { + (Some(self_color), Some(other_color)) => { + self.color = Some(Hsla::blend(other_color, self_color)); + } + (None, Some(other_color)) => { + self.color = Some(other_color); + } + _ => {} + } + + if other.font_weight.is_some() { + self.font_weight = other.font_weight; + } + + if other.font_style.is_some() { + self.font_style = other.font_style; + } + + if other.background_color.is_some() { + self.background_color = other.background_color; + } + + if other.underline.is_some() { + self.underline = other.underline; + } + + if other.strikethrough.is_some() { + self.strikethrough = other.strikethrough; + } + + match (other.fade_out, self.fade_out) { + (Some(source_fade), None) => self.fade_out = Some(source_fade), + (Some(source_fade), Some(dest_fade)) => { + self.fade_out = Some((dest_fade * (1. + source_fade)).clamp(0., 1.)); + } + _ => {} + } + } +} + +impl From for HighlightStyle { + fn from(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(font_weight: FontWeight) -> Self { + Self { + font_weight: Some(font_weight), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(font_style: FontStyle) -> Self { + Self { + font_style: Some(font_style), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(color: Rgba) -> Self { + Self { + color: Some(color.into()), + ..Default::default() + } + } +} + +/// Combine and merge the highlights and ranges in the two iterators. +pub fn combine_highlights( + a: impl IntoIterator, HighlightStyle)>, + b: impl IntoIterator, HighlightStyle)>, +) -> impl Iterator, HighlightStyle)> { + let mut endpoints = Vec::new(); + let mut highlights = Vec::new(); + for (range, highlight) in a.into_iter().chain(b) { + if !range.is_empty() { + let highlight_id = highlights.len(); + endpoints.push((range.start, highlight_id, true)); + endpoints.push((range.end, highlight_id, false)); + highlights.push(highlight); + } + } + endpoints.sort_unstable_by_key(|(position, _, _)| *position); + let mut endpoints = endpoints.into_iter().peekable(); + + let mut active_styles = HashSet::default(); + let mut ix = 0; + iter::from_fn(move || { + while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { + let prev_index = mem::replace(&mut ix, *endpoint_ix); + if ix > prev_index && !active_styles.is_empty() { + let mut current_style = HighlightStyle::default(); + for highlight_id in &active_styles { + current_style.highlight(highlights[*highlight_id]); + } + return Some((prev_index..ix, current_style)); + } + + if *is_start { + active_styles.insert(*highlight_id); + } else { + active_styles.remove(highlight_id); + } + endpoints.next(); + } + None + }) +} + +#[cfg(test)] +mod tests { + use crate::{blue, green, red, yellow}; + + use super::*; + + #[test] + fn test_combine_highlights() { + assert_eq!( + combine_highlights( + [ + (0..5, green().into()), + (4..10, FontWeight::BOLD.into()), + (15..20, yellow().into()), + ], + [ + (2..6, FontStyle::Italic.into()), + (1..3, blue().into()), + (21..23, red().into()), + ] + ) + .collect::>(), + [ + ( + 0..1, + HighlightStyle { + color: Some(green()), + ..Default::default() + } + ), + ( + 1..2, + HighlightStyle { + color: Some(green()), + ..Default::default() + } + ), + ( + 2..3, + HighlightStyle { + color: Some(green()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 3..4, + HighlightStyle { + color: Some(green()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 4..5, + HighlightStyle { + color: Some(green()), + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 5..6, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 6..10, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + } + ), + ( + 15..20, + HighlightStyle { + color: Some(yellow()), + ..Default::default() + } + ), + ( + 21..23, + HighlightStyle { + color: Some(red()), + ..Default::default() + } + ) + ] + ); + } +} diff --git a/crates/ming/src/styled.rs b/crates/ming/src/styled.rs new file mode 100644 index 0000000..c5ddd1f --- /dev/null +++ b/crates/ming/src/styled.rs @@ -0,0 +1,835 @@ +use crate::{ + self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, + DefiniteLength, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, + JustifyContent, Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace, +}; +use crate::{BoxShadow, TextStyleRefinement}; +use smallvec::{smallvec, SmallVec}; +use taffy::style::{AlignContent, Display, Overflow}; + +/// A trait for elements that can be styled. +/// Use this to opt-in to a CSS-like styling API. +pub trait Styled: Sized { + /// Returns a reference to the style memory of this element. + fn style(&mut self) -> &mut StyleRefinement; + + gpui_macros::style_helpers!(); + + /// Sets the position of the element to `relative`. + /// [Docs](https://tailwindcss.com/docs/position) + fn relative(mut self) -> Self { + self.style().position = Some(Position::Relative); + self + } + + /// Sets the position of the element to `absolute`. + /// [Docs](https://tailwindcss.com/docs/position) + fn absolute(mut self) -> Self { + self.style().position = Some(Position::Absolute); + self + } + + /// Sets the display type of the element to `block`. + /// [Docs](https://tailwindcss.com/docs/display) + fn block(mut self) -> Self { + self.style().display = Some(Display::Block); + self + } + + /// Sets the display type of the element to `flex`. + /// [Docs](https://tailwindcss.com/docs/display) + fn flex(mut self) -> Self { + self.style().display = Some(Display::Flex); + self + } + + /// Sets the visibility of the element to `visible`. + /// [Docs](https://tailwindcss.com/docs/visibility) + fn visible(mut self) -> Self { + self.style().visibility = Some(Visibility::Visible); + self + } + + /// Sets the visibility of the element to `hidden`. + /// [Docs](https://tailwindcss.com/docs/visibility) + fn invisible(mut self) -> Self { + self.style().visibility = Some(Visibility::Hidden); + self + } + + /// Sets the behavior of content that overflows the container to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + fn overflow_hidden(mut self) -> Self { + self.style().overflow.x = Some(Overflow::Hidden); + self.style().overflow.y = Some(Overflow::Hidden); + self + } + + /// Sets the behavior of content that overflows the container on the X axis to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + fn overflow_x_hidden(mut self) -> Self { + self.style().overflow.x = Some(Overflow::Hidden); + self + } + + /// Sets the behavior of content that overflows the container on the Y axis to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + fn overflow_y_hidden(mut self) -> Self { + self.style().overflow.y = Some(Overflow::Hidden); + self + } + + /// Set the cursor style when hovering over this element + fn cursor(mut self, cursor: CursorStyle) -> Self { + self.style().mouse_cursor = Some(cursor); + self + } + + /// Sets the cursor style when hovering an element to `default`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_default(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::Arrow); + self + } + + /// Sets the cursor style when hovering an element to `pointer`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_pointer(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::PointingHand); + self + } + + /// Sets cursor style when hovering over an element to `text`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_text(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::IBeam); + self + } + + /// Sets cursor style when hovering over an element to `move`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_move(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ClosedHand); + self + } + + /// Sets cursor style when hovering over an element to `not-allowed`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_not_allowed(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed); + self + } + + /// Sets cursor style when hovering over an element to `context-menu`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_context_menu(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ContextualMenu); + self + } + + /// Sets cursor style when hovering over an element to `crosshair`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_crosshair(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::Crosshair); + self + } + + /// Sets cursor style when hovering over an element to `vertical-text`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_vertical_text(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::IBeamCursorForVerticalLayout); + self + } + + /// Sets cursor style when hovering over an element to `alias`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_alias(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::DragLink); + self + } + + /// Sets cursor style when hovering over an element to `copy`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_copy(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::DragCopy); + self + } + + /// Sets cursor style when hovering over an element to `no-drop`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_no_drop(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed); + self + } + + /// Sets cursor style when hovering over an element to `grab`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_grab(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::OpenHand); + self + } + + /// Sets cursor style when hovering over an element to `grabbing`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_grabbing(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ClosedHand); + self + } + + /// Sets cursor style when hovering over an element to `ew-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_ew_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeLeftRight); + self + } + + /// Sets cursor style when hovering over an element to `ns-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_ns_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeUpDown); + self + } + + /// Sets cursor style when hovering over an element to `col-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_col_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeColumn); + self + } + + /// Sets cursor style when hovering over an element to `row-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_row_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeRow); + self + } + + /// Sets cursor style when hovering over an element to `n-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_n_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeUp); + self + } + + /// Sets cursor style when hovering over an element to `e-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_e_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeRight); + self + } + + /// Sets cursor style when hovering over an element to `s-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_s_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeDown); + self + } + + /// Sets cursor style when hovering over an element to `w-resize`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_w_resize(mut self) -> Self { + self.style().mouse_cursor = Some(CursorStyle::ResizeLeft); + self + } + + /// Sets the whitespace of the element to `normal`. + /// [Docs](https://tailwindcss.com/docs/whitespace#normal) + fn whitespace_normal(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .white_space = Some(WhiteSpace::Normal); + self + } + + /// Sets the whitespace of the element to `nowrap`. + /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap) + fn whitespace_nowrap(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .white_space = Some(WhiteSpace::Nowrap); + self + } + + /// Sets the flex direction of the element to `column`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#column) + fn flex_col(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::Column); + self + } + + /// Sets the flex direction of the element to `column-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#column-reverse) + fn flex_col_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::ColumnReverse); + self + } + + /// Sets the flex direction of the element to `row`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#row) + fn flex_row(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::Row); + self + } + + /// Sets the flex direction of the element to `row-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#row-reverse) + fn flex_row_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::RowReverse); + self + } + + /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#flex-1) + fn flex_1(mut self) -> Self { + self.style().flex_grow = Some(1.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(relative(0.).into()); + self + } + + /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#auto) + fn flex_auto(mut self) -> Self { + self.style().flex_grow = Some(1.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(Length::Auto); + self + } + + /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#initial) + fn flex_initial(mut self) -> Self { + self.style().flex_grow = Some(0.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(Length::Auto); + self + } + + /// Sets the element to prevent a flex item from growing or shrinking. + /// [Docs](https://tailwindcss.com/docs/flex#none) + fn flex_none(mut self) -> Self { + self.style().flex_grow = Some(0.); + self.style().flex_shrink = Some(0.); + self + } + + /// Sets the initial size of flex items for this element. + /// [Docs](https://tailwindcss.com/docs/flex-basis) + fn flex_basis(mut self, basis: impl Into) -> Self { + self.style().flex_basis = Some(basis.into()); + self + } + + /// Sets the element to allow a flex item to grow to fill any available space. + /// [Docs](https://tailwindcss.com/docs/flex-grow) + fn flex_grow(mut self) -> Self { + self.style().flex_grow = Some(1.); + self + } + + /// Sets the element to allow a flex item to shrink if needed. + /// [Docs](https://tailwindcss.com/docs/flex-shrink) + fn flex_shrink(mut self) -> Self { + self.style().flex_shrink = Some(1.); + self + } + + /// Sets the element to prevent a flex item from shrinking. + /// [Docs](https://tailwindcss.com/docs/flex-shrink#dont-shrink) + fn flex_shrink_0(mut self) -> Self { + self.style().flex_shrink = Some(0.); + self + } + + /// Sets the element to allow flex items to wrap. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-normally) + fn flex_wrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::Wrap); + self + } + + /// Sets the element wrap flex items in the reverse direction. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-reversed) + fn flex_wrap_reverse(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::WrapReverse); + self + } + + /// Sets the element to prevent flex items from wrapping, causing inflexible items to overflow the container if necessary. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#dont-wrap) + fn flex_nowrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::NoWrap); + self + } + + /// Sets the element to align flex items to the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#start) + fn items_start(mut self) -> Self { + self.style().align_items = Some(AlignItems::FlexStart); + self + } + + /// Sets the element to align flex items to the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#end) + fn items_end(mut self) -> Self { + self.style().align_items = Some(AlignItems::FlexEnd); + self + } + + /// Sets the element to align flex items along the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#center) + fn items_center(mut self) -> Self { + self.style().align_items = Some(AlignItems::Center); + self + } + + /// Sets the element to justify flex items along the container's main axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/justify-content#space-between) + fn justify_between(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::SpaceBetween); + self + } + + /// Sets the element to justify flex items along the center of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#center) + fn justify_center(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::Center); + self + } + + /// Sets the element to justify flex items against the start of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#start) + fn justify_start(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::Start); + self + } + + /// Sets the element to justify flex items against the end of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#end) + fn justify_end(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::End); + self + } + + /// Sets the element to justify items along the container's main axis such + /// that there is an equal amount of space on each side of each item. + /// [Docs](https://tailwindcss.com/docs/justify-content#space-around) + fn justify_around(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::SpaceAround); + self + } + + /// Sets the element to pack content items in their default position as if no align-content value was set. + /// [Docs](https://tailwindcss.com/docs/align-content#normal) + fn content_normal(mut self) -> Self { + self.style().align_content = None; + self + } + + /// Sets the element to pack content items in the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#center) + fn content_center(mut self) -> Self { + self.style().align_content = Some(AlignContent::Center); + self + } + + /// Sets the element to pack content items against the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#start) + fn content_start(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexStart); + self + } + + /// Sets the element to pack content items against the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#end) + fn content_end(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexEnd); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-between) + fn content_between(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceBetween); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space on each side of each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-around) + fn content_around(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceAround); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-evenly) + fn content_evenly(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceEvenly); + self + } + + /// Sets the element to allow content items to fill the available space along the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#stretch) + fn content_stretch(mut self) -> Self { + self.style().align_content = Some(AlignContent::Stretch); + self + } + + /// Sets the background color of the element. + fn bg(mut self, fill: F) -> Self + where + F: Into, + Self: Sized, + { + self.style().background = Some(fill.into()); + self + } + + /// Sets the border color of the element. + fn border_color(mut self, border_color: C) -> Self + where + C: Into, + Self: Sized, + { + self.style().border_color = Some(border_color.into()); + self + } + + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self { + self.style().box_shadow = Some(shadows); + self + } + + /// Clears the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + fn shadow_none(mut self) -> Self { + self.style().box_shadow = Some(Default::default()); + self + } + + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + fn shadow_sm(mut self) -> Self { + self.style().box_shadow = Some(smallvec::smallvec![BoxShadow { + color: hsla(0., 0., 0., 0.05), + offset: point(px(0.), px(1.)), + blur_radius: px(2.), + spread_radius: px(0.), + }]); + self + } + + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + fn shadow_md(mut self) -> Self { + self.style().box_shadow = Some(smallvec![ + BoxShadow { + color: hsla(0.5, 0., 0., 0.1), + offset: point(px(0.), px(4.)), + blur_radius: px(6.), + spread_radius: px(-1.), + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(2.)), + blur_radius: px(4.), + spread_radius: px(-2.), + } + ]); + self + } + + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + fn shadow_lg(mut self) -> Self { + self.style().box_shadow = Some(smallvec![ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(10.)), + blur_radius: px(15.), + spread_radius: px(-3.), + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(4.)), + blur_radius: px(6.), + spread_radius: px(-4.), + } + ]); + self + } + + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + fn shadow_xl(mut self) -> Self { + self.style().box_shadow = Some(smallvec![ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(20.)), + blur_radius: px(25.), + spread_radius: px(-5.), + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(8.)), + blur_radius: px(10.), + spread_radius: px(-6.), + } + ]); + self + } + + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + fn shadow_2xl(mut self) -> Self { + self.style().box_shadow = Some(smallvec![BoxShadow { + color: hsla(0., 0., 0., 0.25), + offset: point(px(0.), px(25.)), + blur_radius: px(50.), + spread_radius: px(-12.), + }]); + self + } + + /// Get the text style that has been configured on this element. + fn text_style(&mut self) -> &mut Option { + let style: &mut StyleRefinement = self.style(); + &mut style.text + } + + /// Set the text color of this element, this value cascades to its child elements. + fn text_color(mut self, color: impl Into) -> Self { + self.text_style().get_or_insert_with(Default::default).color = Some(color.into()); + self + } + + /// Set the font weight of this element, this value cascades to its child elements. + fn font_weight(mut self, weight: FontWeight) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_weight = Some(weight); + self + } + + /// Set the background color of this element, this value cascades to its child elements. + fn text_bg(mut self, bg: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .background_color = Some(bg.into()); + self + } + + /// Set the text size of this element, this value cascades to its child elements. + fn text_size(mut self, size: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(size.into()); + self + } + + /// Set the text size to 'extra small', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_xs(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(0.75).into()); + self + } + + /// Set the text size to 'small', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_sm(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(0.875).into()); + self + } + + /// Reset the text styling for this element and its children. + fn text_base(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.0).into()); + self + } + + /// Set the text size to 'large', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_lg(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.125).into()); + self + } + + /// Set the text size to 'extra large', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_xl(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.25).into()); + self + } + + /// Set the text size to 'extra-extra large', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_2xl(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.5).into()); + self + } + + /// Set the text size to 'extra-extra-extra large', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_3xl(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_size = Some(rems(1.875).into()); + self + } + + /// Set the font style to 'non-italic', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text) + fn non_italic(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_style = Some(FontStyle::Normal); + self + } + + /// Set the font style to 'italic', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text) + fn italic(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_style = Some(FontStyle::Italic); + self + } + + /// Remove the text decoration on this element, this value cascades to its child elements. + fn text_decoration_none(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .underline = None; + self + } + + /// Set the color for the underline on this element + fn text_decoration_color(mut self, color: impl Into) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.color = Some(color.into()); + self + } + + /// Set the underline to a solid line + fn text_decoration_solid(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.wavy = false; + self + } + + /// Set the underline to a wavy line + fn text_decoration_wavy(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.wavy = true; + self + } + + /// Set the underline to be 0 thickness, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_0(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(0.); + self + } + + /// Set the underline to be 1px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_1(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(1.); + self + } + + /// Set the underline to be 2px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_2(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(2.); + self + } + + /// Set the underline to be 4px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_4(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(4.); + self + } + + /// Set the underline to be 8px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_8(mut self) -> Self { + let style = self.text_style().get_or_insert_with(Default::default); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(8.); + self + } + + /// Change the font family on this element and its children. + fn font_family(mut self, family_name: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_family = Some(family_name.into()); + self + } + + /// Change the font of this element and its children. + fn font(mut self, font: Font) -> Self { + let Font { + family, + features, + weight, + style, + } = font; + + let text_style = self.text_style().get_or_insert_with(Default::default); + text_style.font_family = Some(family); + text_style.font_features = Some(features); + text_style.font_weight = Some(weight); + text_style.font_style = Some(style); + + self + } + + /// Set the line height on this element and its children. + fn line_height(mut self, line_height: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .line_height = Some(line_height.into()); + self + } + + /// Draw a debug border around this element. + #[cfg(debug_assertions)] + fn debug(mut self) -> Self { + self.style().debug = Some(true); + self + } + + /// Draw a debug border on all conforming elements below this element. + #[cfg(debug_assertions)] + fn debug_below(mut self) -> Self { + self.style().debug_below = Some(true); + self + } +} diff --git a/crates/ming/src/subscription.rs b/crates/ming/src/subscription.rs new file mode 100644 index 0000000..c75d10e --- /dev/null +++ b/crates/ming/src/subscription.rs @@ -0,0 +1,171 @@ +use collections::{BTreeMap, BTreeSet}; +use parking_lot::Mutex; +use std::{cell::Cell, fmt::Debug, mem, rc::Rc, sync::Arc}; +use util::post_inc; + +pub(crate) struct SubscriberSet( + Arc>>, +); + +impl Clone for SubscriberSet { + fn clone(&self) -> Self { + SubscriberSet(self.0.clone()) + } +} + +struct SubscriberSetState { + subscribers: BTreeMap>>>, + dropped_subscribers: BTreeSet<(EmitterKey, usize)>, + next_subscriber_id: usize, +} + +struct Subscriber { + active: Rc>, + callback: Callback, +} + +impl SubscriberSet +where + EmitterKey: 'static + Ord + Clone + Debug, + Callback: 'static, +{ + pub fn new() -> Self { + Self(Arc::new(Mutex::new(SubscriberSetState { + subscribers: Default::default(), + dropped_subscribers: Default::default(), + next_subscriber_id: 0, + }))) + } + + /// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions + /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`. + /// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter + /// to activate the [`Subscription`]. + pub fn insert( + &self, + emitter_key: EmitterKey, + callback: Callback, + ) -> (Subscription, impl FnOnce()) { + let active = Rc::new(Cell::new(false)); + let mut lock = self.0.lock(); + let subscriber_id = post_inc(&mut lock.next_subscriber_id); + lock.subscribers + .entry(emitter_key.clone()) + .or_default() + .get_or_insert_with(Default::default) + .insert( + subscriber_id, + Subscriber { + active: active.clone(), + callback, + }, + ); + let this = self.0.clone(); + + let subscription = Subscription { + unsubscribe: Some(Box::new(move || { + let mut lock = this.lock(); + let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else { + // remove was called with this emitter_key + return; + }; + + if let Some(subscribers) = subscribers { + subscribers.remove(&subscriber_id); + if subscribers.is_empty() { + lock.subscribers.remove(&emitter_key); + } + return; + } + + // We didn't manage to remove the subscription, which means it was dropped + // while invoking the callback. Mark it as dropped so that we can remove it + // later. + lock.dropped_subscribers + .insert((emitter_key, subscriber_id)); + })), + }; + (subscription, move || active.set(true)) + } + + pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator { + let subscribers = self.0.lock().subscribers.remove(emitter); + subscribers + .unwrap_or_default() + .map(|s| s.into_values()) + .into_iter() + .flatten() + .filter_map(|subscriber| { + if subscriber.active.get() { + Some(subscriber.callback) + } else { + None + } + }) + } + + /// Call the given callback for each subscriber to the given emitter. + /// If the callback returns false, the subscriber is removed. + pub fn retain(&self, emitter: &EmitterKey, mut f: F) + where + F: FnMut(&mut Callback) -> bool, + { + let Some(mut subscribers) = self + .0 + .lock() + .subscribers + .get_mut(emitter) + .and_then(|s| s.take()) + else { + return; + }; + + subscribers.retain(|_, subscriber| { + if subscriber.active.get() { + f(&mut subscriber.callback) + } else { + true + } + }); + let mut lock = self.0.lock(); + + // Add any new subscribers that were added while invoking the callback. + if let Some(Some(new_subscribers)) = lock.subscribers.remove(emitter) { + subscribers.extend(new_subscribers); + } + + // Remove any dropped subscriptions that were dropped while invoking the callback. + for (dropped_emitter, dropped_subscription_id) in mem::take(&mut lock.dropped_subscribers) { + debug_assert_eq!(*emitter, dropped_emitter); + subscribers.remove(&dropped_subscription_id); + } + + if !subscribers.is_empty() { + lock.subscribers.insert(emitter.clone(), Some(subscribers)); + } + } +} + +/// A handle to a subscription created by GPUI. When dropped, the subscription +/// is cancelled and the callback will no longer be invoked. +#[must_use] +pub struct Subscription { + unsubscribe: Option>, +} + +impl Subscription { + /// Detaches the subscription from this handle. The callback will + /// continue to be invoked until the views or models it has been + /// subscribed to are dropped + pub fn detach(mut self) { + self.unsubscribe.take(); + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + if let Some(unsubscribe) = self.unsubscribe.take() { + unsubscribe(); + } + } +} diff --git a/crates/ming/src/svg_renderer.rs b/crates/ming/src/svg_renderer.rs new file mode 100644 index 0000000..54f52a5 --- /dev/null +++ b/crates/ming/src/svg_renderer.rs @@ -0,0 +1,70 @@ +use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size}; +use anyhow::anyhow; +use resvg::tiny_skia::Pixmap; +use std::{hash::Hash, sync::Arc}; + +#[derive(Clone, PartialEq, Hash, Eq)] +pub(crate) struct RenderSvgParams { + pub(crate) path: SharedString, + pub(crate) size: Size, +} + +#[derive(Clone)] +pub(crate) struct SvgRenderer { + asset_source: Arc, +} + +pub enum SvgSize { + Size(Size), + ScaleFactor(f32), +} + +impl SvgRenderer { + pub fn new(asset_source: Arc) -> Self { + Self { asset_source } + } + + pub fn render(&self, params: &RenderSvgParams) -> Result> { + if params.size.is_zero() { + return Err(anyhow!("can't render at a zero size")); + } + + // Load the tree. + let bytes = self.asset_source.load(¶ms.path)?; + + let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?; + + // Convert the pixmap's pixels into an alpha mask. + let alpha_mask = pixmap + .pixels() + .iter() + .map(|p| p.alpha()) + .collect::>(); + Ok(alpha_mask) + } + + pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { + let tree = usvg::Tree::from_data(&bytes, &usvg::Options::default())?; + + let size = match size { + SvgSize::Size(size) => size, + SvgSize::ScaleFactor(scale) => crate::size( + DevicePixels((tree.size().width() * scale) as i32), + DevicePixels((tree.size().height() * scale) as i32), + ), + }; + + // Render the SVG to a pixmap with the specified width and height. + let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width.into(), size.height.into()) + .ok_or(usvg::Error::InvalidSize)?; + + let transform = tree.view_box().to_transform( + resvg::tiny_skia::Size::from_wh(size.width.0 as f32, size.height.0 as f32) + .ok_or(usvg::Error::InvalidSize)?, + ); + + resvg::render(&tree, transform, &mut pixmap.as_mut()); + + Ok(pixmap) + } +} diff --git a/crates/ming/src/taffy.rs b/crates/ming/src/taffy.rs new file mode 100644 index 0000000..16f8874 --- /dev/null +++ b/crates/ming/src/taffy.rs @@ -0,0 +1,498 @@ +use crate::{ + AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style, + WindowContext, +}; +use collections::{FxHashMap, FxHashSet}; +use smallvec::SmallVec; +use std::fmt::Debug; +use taffy::{ + geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, + style::AvailableSpace as TaffyAvailableSpace, + tree::NodeId, + TaffyTree, TraversePartialTree as _, +}; + +type NodeMeasureFn = + Box>, Size, &mut WindowContext) -> Size>; + +pub struct TaffyLayoutEngine { + taffy: TaffyTree<()>, + styles: FxHashMap, + children_to_parents: FxHashMap, + absolute_layout_bounds: FxHashMap>, + computed_layouts: FxHashSet, + nodes_to_measure: FxHashMap, +} + +static EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible"; + +impl TaffyLayoutEngine { + pub fn new() -> Self { + TaffyLayoutEngine { + taffy: TaffyTree::new(), + styles: FxHashMap::default(), + children_to_parents: FxHashMap::default(), + absolute_layout_bounds: FxHashMap::default(), + computed_layouts: FxHashSet::default(), + nodes_to_measure: FxHashMap::default(), + } + } + + pub fn clear(&mut self) { + self.taffy.clear(); + self.children_to_parents.clear(); + self.absolute_layout_bounds.clear(); + self.computed_layouts.clear(); + self.nodes_to_measure.clear(); + self.styles.clear(); + } + + pub fn request_layout( + &mut self, + style: Style, + rem_size: Pixels, + children: &[LayoutId], + ) -> LayoutId { + let taffy_style = style.to_taffy(rem_size); + let layout_id = if children.is_empty() { + self.taffy + .new_leaf(taffy_style) + .expect(EXPECT_MESSAGE) + .into() + } else { + let parent_id = self + .taffy + // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. + .new_with_children(taffy_style, unsafe { std::mem::transmute(children) }) + .expect(EXPECT_MESSAGE) + .into(); + self.children_to_parents + .extend(children.into_iter().map(|child_id| (*child_id, parent_id))); + parent_id + }; + self.styles.insert(layout_id, style); + layout_id + } + + pub fn request_measured_layout( + &mut self, + style: Style, + rem_size: Pixels, + measure: impl FnMut(Size>, Size, &mut WindowContext) -> Size + + 'static, + ) -> LayoutId { + let taffy_style = style.to_taffy(rem_size); + + let layout_id = self + .taffy + .new_leaf_with_context(taffy_style, ()) + .expect(EXPECT_MESSAGE) + .into(); + self.nodes_to_measure.insert(layout_id, Box::new(measure)); + self.styles.insert(layout_id, style); + layout_id + } + + // Used to understand performance + #[allow(dead_code)] + fn count_all_children(&self, parent: LayoutId) -> anyhow::Result { + let mut count = 0; + + for child in self.taffy.children(parent.0)? { + // Count this child. + count += 1; + + // Count all of this child's children. + count += self.count_all_children(LayoutId(child))? + } + + Ok(count) + } + + // Used to understand performance + #[allow(dead_code)] + fn max_depth(&self, depth: u32, parent: LayoutId) -> anyhow::Result { + println!( + "{parent:?} at depth {depth} has {} children", + self.taffy.child_count(parent.0) + ); + + let mut max_child_depth = 0; + + for child in self.taffy.children(parent.0)? { + max_child_depth = std::cmp::max(max_child_depth, self.max_depth(0, LayoutId(child))?); + } + + Ok(depth + 1 + max_child_depth) + } + + // Used to understand performance + #[allow(dead_code)] + fn get_edges(&self, parent: LayoutId) -> anyhow::Result> { + let mut edges = Vec::new(); + + for child in self.taffy.children(parent.0)? { + edges.push((parent, LayoutId(child))); + + edges.extend(self.get_edges(LayoutId(child))?); + } + + Ok(edges) + } + + pub fn compute_layout( + &mut self, + id: LayoutId, + available_space: Size, + cx: &mut WindowContext, + ) { + // Leaving this here until we have a better instrumentation approach. + // println!("Laying out {} children", self.count_all_children(id)?); + // println!("Max layout depth: {}", self.max_depth(0, id)?); + + // Output the edges (branches) of the tree in Mermaid format for visualization. + // println!("Edges:"); + // for (a, b) in self.get_edges(id)? { + // println!("N{} --> N{}", u64::from(a), u64::from(b)); + // } + // println!(""); + // + + if !self.computed_layouts.insert(id) { + let mut stack = SmallVec::<[LayoutId; 64]>::new(); + stack.push(id); + while let Some(id) = stack.pop() { + self.absolute_layout_bounds.remove(&id); + stack.extend( + self.taffy + .children(id.into()) + .expect(EXPECT_MESSAGE) + .into_iter() + .map(Into::into), + ); + } + } + + // let started_at = std::time::Instant::now(); + self.taffy + .compute_layout_with_measure( + id.into(), + available_space.into(), + |known_dimensions, available_space, node_id, _context| { + let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else { + return taffy::geometry::Size::default(); + }; + + let known_dimensions = Size { + width: known_dimensions.width.map(Pixels), + height: known_dimensions.height.map(Pixels), + }; + + measure(known_dimensions, available_space.into(), cx).into() + }, + ) + .expect(EXPECT_MESSAGE); + + // println!("compute_layout took {:?}", started_at.elapsed()); + } + + pub fn layout_bounds(&mut self, id: LayoutId) -> Bounds { + if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() { + return layout; + } + + let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE); + let mut bounds = Bounds { + origin: layout.location.into(), + size: layout.size.into(), + }; + + if let Some(parent_id) = self.children_to_parents.get(&id).copied() { + let parent_bounds = self.layout_bounds(parent_id); + bounds.origin += parent_bounds.origin; + } + self.absolute_layout_bounds.insert(id, bounds); + + bounds + } +} + +/// A unique identifier for a layout node, generated when requesting a layout from Taffy +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(transparent)] +pub struct LayoutId(NodeId); + +impl std::hash::Hash for LayoutId { + fn hash(&self, state: &mut H) { + u64::from(self.0).hash(state); + } +} + +impl From for LayoutId { + fn from(node_id: NodeId) -> Self { + Self(node_id) + } +} + +impl From for NodeId { + fn from(layout_id: LayoutId) -> NodeId { + layout_id.0 + } +} + +trait ToTaffy { + fn to_taffy(&self, rem_size: Pixels) -> Output; +} + +impl ToTaffy for Style { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style { + taffy::style::Style { + display: self.display, + overflow: self.overflow.into(), + scrollbar_width: self.scrollbar_width, + position: self.position, + inset: self.inset.to_taffy(rem_size), + size: self.size.to_taffy(rem_size), + min_size: self.min_size.to_taffy(rem_size), + max_size: self.max_size.to_taffy(rem_size), + aspect_ratio: self.aspect_ratio, + margin: self.margin.to_taffy(rem_size), + padding: self.padding.to_taffy(rem_size), + border: self.border_widths.to_taffy(rem_size), + align_items: self.align_items, + align_self: self.align_self, + align_content: self.align_content, + justify_content: self.justify_content, + gap: self.gap.to_taffy(rem_size), + flex_direction: self.flex_direction, + flex_wrap: self.flex_wrap, + flex_basis: self.flex_basis.to_taffy(rem_size), + flex_grow: self.flex_grow, + flex_shrink: self.flex_shrink, + ..Default::default() // Ignore grid properties for now + } + } +} + +impl ToTaffy for Length { + fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { + match self { + Length::Definite(length) => length.to_taffy(rem_size), + Length::Auto => taffy::prelude::LengthPercentageAuto::Auto, + } + } +} + +impl ToTaffy for Length { + fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension { + match self { + Length::Definite(length) => length.to_taffy(rem_size), + Length::Auto => taffy::prelude::Dimension::Auto, + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => { + taffy::style::LengthPercentage::Length(pixels.into()) + } + AbsoluteLength::Rems(rems) => { + taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + } + }, + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentage::Percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentageAuto { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => { + taffy::style::LengthPercentageAuto::Length(pixels.into()) + } + AbsoluteLength::Rems(rems) => { + taffy::style::LengthPercentageAuto::Length((*rems * rem_size).into()) + } + }, + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentageAuto::Percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::Length(pixels.into()), + AbsoluteLength::Rems(rems) => { + taffy::style::Dimension::Length((*rems * rem_size).into()) + } + }, + DefiniteLength::Fraction(fraction) => taffy::style::Dimension::Percent(*fraction), + } + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage { + match self { + AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(pixels.into()), + AbsoluteLength::Rems(rems) => { + taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + } + } + } +} + +impl From> for Point +where + T: Into, + T2: Clone + Default + Debug, +{ + fn from(point: TaffyPoint) -> Point { + Point { + x: point.x.into(), + y: point.y.into(), + } + } +} + +impl From> for TaffyPoint +where + T: Into + Clone + Default + Debug, +{ + fn from(val: Point) -> Self { + TaffyPoint { + x: val.x.into(), + y: val.y.into(), + } + } +} + +impl ToTaffy> for Size +where + T: ToTaffy + Clone + Default + Debug, +{ + fn to_taffy(&self, rem_size: Pixels) -> TaffySize { + TaffySize { + width: self.width.to_taffy(rem_size), + height: self.height.to_taffy(rem_size), + } + } +} + +impl ToTaffy> for Edges +where + T: ToTaffy + Clone + Default + Debug, +{ + fn to_taffy(&self, rem_size: Pixels) -> TaffyRect { + TaffyRect { + top: self.top.to_taffy(rem_size), + right: self.right.to_taffy(rem_size), + bottom: self.bottom.to_taffy(rem_size), + left: self.left.to_taffy(rem_size), + } + } +} + +impl From> for Size +where + T: Into, + U: Clone + Default + Debug, +{ + fn from(taffy_size: TaffySize) -> Self { + Size { + width: taffy_size.width.into(), + height: taffy_size.height.into(), + } + } +} + +impl From> for TaffySize +where + T: Into + Clone + Default + Debug, +{ + fn from(size: Size) -> Self { + TaffySize { + width: size.width.into(), + height: size.height.into(), + } + } +} + +/// The space available for an element to be laid out in +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] +pub enum AvailableSpace { + /// The amount of space available is the specified number of pixels + Definite(Pixels), + /// The amount of space available is indefinite and the node should be laid out under a min-content constraint + #[default] + MinContent, + /// The amount of space available is indefinite and the node should be laid out under a max-content constraint + MaxContent, +} + +impl AvailableSpace { + /// Returns a `Size` with both width and height set to `AvailableSpace::MinContent`. + /// + /// This function is useful when you want to create a `Size` with the minimum content constraints + /// for both dimensions. + /// + /// # Examples + /// + /// ``` + /// let min_content_size = AvailableSpace::min_size(); + /// assert_eq!(min_content_size.width, AvailableSpace::MinContent); + /// assert_eq!(min_content_size.height, AvailableSpace::MinContent); + /// ``` + pub const fn min_size() -> Size { + Size { + width: Self::MinContent, + height: Self::MinContent, + } + } +} + +impl From for TaffyAvailableSpace { + fn from(space: AvailableSpace) -> TaffyAvailableSpace { + match space { + AvailableSpace::Definite(Pixels(value)) => TaffyAvailableSpace::Definite(value), + AvailableSpace::MinContent => TaffyAvailableSpace::MinContent, + AvailableSpace::MaxContent => TaffyAvailableSpace::MaxContent, + } + } +} + +impl From for AvailableSpace { + fn from(space: TaffyAvailableSpace) -> AvailableSpace { + match space { + TaffyAvailableSpace::Definite(value) => AvailableSpace::Definite(Pixels(value)), + TaffyAvailableSpace::MinContent => AvailableSpace::MinContent, + TaffyAvailableSpace::MaxContent => AvailableSpace::MaxContent, + } + } +} + +impl From for AvailableSpace { + fn from(pixels: Pixels) -> Self { + AvailableSpace::Definite(pixels) + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: AvailableSpace::Definite(size.width), + height: AvailableSpace::Definite(size.height), + } + } +} diff --git a/crates/ming/src/test.rs b/crates/ming/src/test.rs new file mode 100644 index 0000000..3f26754 --- /dev/null +++ b/crates/ming/src/test.rs @@ -0,0 +1,113 @@ +//! Test support for GPUI. +//! +//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context, +//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run +//! deterministically even in the face of arbitrary parallelism. +//! +//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test` +//! or `cargo-nextest`, or another runner of your choice. +//! +//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts +//! as you need. +//! +//! ## Example +//! +//! ``` +//! use gpui; +//! +//! #[gpui::test] +//! async fn test_example(cx: &TestAppContext) { +//! assert!(true) +//! } +//! +//! #[gpui::test] +//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) { +//! assert!(true) +//! } +//! ``` +use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; +use futures::StreamExt as _; +use rand::prelude::*; +use smol::channel; +use std::{ + env, + panic::{self, RefUnwindSafe}, +}; + +/// Run the given test function with the configured parameters. +/// This is intended for use with the `gpui::test` macro +/// and generally should not be used directly. +pub fn run_test( + mut num_iterations: u64, + max_retries: usize, + test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)), + on_fail_fn: Option, +) { + let starting_seed = env::var("SEED") + .map(|seed| seed.parse().expect("invalid SEED variable")) + .unwrap_or(0); + if let Ok(iterations) = env::var("ITERATIONS") { + num_iterations = iterations.parse().expect("invalid ITERATIONS variable"); + } + let is_randomized = num_iterations > 1; + + for seed in starting_seed..starting_seed + num_iterations { + let mut retry = 0; + loop { + if is_randomized { + eprintln!("seed = {seed}"); + } + let result = panic::catch_unwind(|| { + let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed)); + test_fn(dispatcher, seed); + }); + + match result { + Ok(_) => break, + Err(error) => { + if retry < max_retries { + println!("retrying: attempt {}", retry); + retry += 1; + } else { + if is_randomized { + eprintln!("failing seed: {}", seed); + } + if let Some(f) = on_fail_fn { + f() + } + panic::resume_unwind(error); + } + } + } + } + } +} + +/// A test struct for converting an observation callback into a stream. +pub struct Observation { + rx: channel::Receiver, + _subscription: Subscription, +} + +impl futures::Stream for Observation { + type Item = T; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } +} + +/// observe returns a stream of the change events from the given `View` or `Model` +pub fn observe(entity: &impl Entity, cx: &mut TestAppContext) -> Observation<()> { + let (tx, rx) = smol::channel::unbounded(); + let _subscription = cx.update(|cx| { + cx.observe(entity, move |_, _| { + let _ = smol::block_on(tx.send(())); + }) + }); + + Observation { rx, _subscription } +} diff --git a/crates/ming/src/text_system.rs b/crates/ming/src/text_system.rs new file mode 100644 index 0000000..a030316 --- /dev/null +++ b/crates/ming/src/text_system.rs @@ -0,0 +1,794 @@ +mod font_features; +mod line; +mod line_layout; +mod line_wrapper; + +pub use font_features::*; +pub use line::*; +pub use line_layout::*; +pub use line_wrapper::*; + +use crate::{ + px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, + StrikethroughStyle, UnderlineStyle, +}; +use anyhow::anyhow; +use collections::{BTreeSet, FxHashMap}; +use core::fmt; +use derive_more::Deref; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::{smallvec, SmallVec}; +use std::{ + borrow::Cow, + cmp, + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + ops::{Deref, DerefMut, Range}, + sync::Arc, +}; + +/// An opaque identifier for a specific font. +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] +#[repr(C)] +pub struct FontId(pub usize); + +/// An opaque identifier for a specific font family. +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] +pub struct FontFamilyId(pub usize); + +pub(crate) const SUBPIXEL_VARIANTS: u8 = 4; + +/// The GPUI text rendering sub system. +pub struct TextSystem { + platform_text_system: Arc, + font_ids_by_font: RwLock>>, + font_metrics: RwLock>, + raster_bounds: RwLock>>, + wrapper_pool: Mutex>>, + font_runs_pool: Mutex>>, + fallback_font_stack: SmallVec<[Font; 2]>, +} + +impl TextSystem { + pub(crate) fn new(platform_text_system: Arc) -> Self { + TextSystem { + platform_text_system, + font_metrics: RwLock::default(), + raster_bounds: RwLock::default(), + font_ids_by_font: RwLock::default(), + wrapper_pool: Mutex::default(), + font_runs_pool: Mutex::default(), + fallback_font_stack: smallvec![ + // TODO: This is currently Zed-specific. + // We should allow GPUI users to provide their own fallback font stack. + font("Zed Mono"), + font("Helvetica"), + font("Cantarell"), // Gnome + font("Ubuntu"), // Gnome (Ubuntu) + font("Noto Sans"), // KDE + ], + } + } + + /// Get a list of all available font names from the operating system. + pub fn all_font_names(&self) -> Vec { + let mut names: BTreeSet<_> = self + .platform_text_system + .all_font_names() + .into_iter() + .collect(); + names.extend(self.platform_text_system.all_font_families()); + names.extend( + self.fallback_font_stack + .iter() + .map(|font| font.family.to_string()), + ); + names.into_iter().collect() + } + + /// Add a font's data to the text system. + pub fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.platform_text_system.add_fonts(fonts) + } + + /// Get the FontId for the configure font family and style. + pub fn font_id(&self, font: &Font) -> Result { + fn clone_font_id_result(font_id: &Result) -> Result { + match font_id { + Ok(font_id) => Ok(*font_id), + Err(err) => Err(anyhow!("{}", err)), + } + } + + let font_id = self + .font_ids_by_font + .read() + .get(font) + .map(clone_font_id_result); + if let Some(font_id) = font_id { + font_id + } else { + let font_id = self.platform_text_system.font_id(font); + self.font_ids_by_font + .write() + .insert(font.clone(), clone_font_id_result(&font_id)); + font_id + } + } + + /// Get the Font for the Font Id. + pub fn get_font_for_id(&self, id: FontId) -> Option { + let lock = self.font_ids_by_font.read(); + lock.iter() + .filter_map(|(font, result)| match result { + Ok(font_id) if *font_id == id => Some(font.clone()), + _ => None, + }) + .next() + } + + /// Resolves the specified font, falling back to the default font stack if + /// the font fails to load. + /// + /// # Panics + /// + /// Panics if the font and none of the fallbacks can be resolved. + pub fn resolve_font(&self, font: &Font) -> FontId { + if let Ok(font_id) = self.font_id(font) { + return font_id; + } + for fallback in &self.fallback_font_stack { + if let Ok(font_id) = self.font_id(fallback) { + return font_id; + } + } + + panic!( + "failed to resolve font '{}' or any of the fallbacks: {}", + font.family, + self.fallback_font_stack + .iter() + .map(|fallback| &fallback.family) + .join(", ") + ); + } + + /// Get the bounding box for the given font and font size. + /// A font's bounding box is the smallest rectangle that could enclose all glyphs + /// in the font. superimposed over one another. + pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds { + self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size)) + } + + /// Get the typographic bounds for the given character, in the given font and size. + pub fn typographic_bounds( + &self, + font_id: FontId, + font_size: Pixels, + character: char, + ) -> Result> { + let glyph_id = self + .platform_text_system + .glyph_for_char(font_id, character) + .ok_or_else(|| anyhow!("glyph not found for character '{}'", character))?; + let bounds = self + .platform_text_system + .typographic_bounds(font_id, glyph_id)?; + Ok(self.read_metrics(font_id, |metrics| { + (bounds / metrics.units_per_em as f32 * font_size.0).map(px) + })) + } + + /// Get the advance width for the given character, in the given font and size. + pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result> { + let glyph_id = self + .platform_text_system + .glyph_for_char(font_id, ch) + .ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?; + let result = self.platform_text_system.advance(font_id, glyph_id)? + / self.units_per_em(font_id) as f32; + + Ok(result * font_size) + } + + /// Get the number of font size units per 'em square', + /// Per MDN: "an abstract square whose height is the intended distance between + /// lines of type in the same type size" + pub fn units_per_em(&self, font_id: FontId) -> u32 { + self.read_metrics(font_id, |metrics| metrics.units_per_em) + } + + /// Get the height of a capital letter in the given font and size. + pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.cap_height(font_size)) + } + + /// Get the height of the x character in the given font and size. + pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.x_height(font_size)) + } + + /// Get the recommended distance from the baseline for the given font + pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.ascent(font_size)) + } + + /// Get the recommended distance below the baseline for the given font, + /// in single spaced text. + pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.descent(font_size)) + } + + /// Get the recommended baseline offset for the given font and line height. + pub fn baseline_offset( + &self, + font_id: FontId, + font_size: Pixels, + line_height: Pixels, + ) -> Pixels { + let ascent = self.ascent(font_id, font_size); + let descent = self.descent(font_id, font_size); + let padding_top = (line_height - ascent - descent) / 2.; + padding_top + ascent + } + + fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T { + let lock = self.font_metrics.upgradable_read(); + + if let Some(metrics) = lock.get(&font_id) { + read(metrics) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let metrics = lock + .entry(font_id) + .or_insert_with(|| self.platform_text_system.font_metrics(font_id)); + read(metrics) + } + } + + /// Returns a handle to a line wrapper, for the given font and font size. + pub fn line_wrapper(self: &Arc, font: Font, font_size: Pixels) -> LineWrapperHandle { + let lock = &mut self.wrapper_pool.lock(); + let font_id = self.resolve_font(&font); + let wrappers = lock + .entry(FontIdWithSize { font_id, font_size }) + .or_default(); + let wrapper = wrappers.pop().unwrap_or_else(|| { + LineWrapper::new(font_id, font_size, self.platform_text_system.clone()) + }); + + LineWrapperHandle { + wrapper: Some(wrapper), + text_system: self.clone(), + } + } + + /// Get the rasterized size and location of a specific, rendered glyph. + pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let raster_bounds = self.raster_bounds.upgradable_read(); + if let Some(bounds) = raster_bounds.get(params) { + Ok(*bounds) + } else { + let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds); + let bounds = self.platform_text_system.glyph_raster_bounds(params)?; + raster_bounds.insert(params.clone(), bounds); + Ok(bounds) + } + } + + pub(crate) fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + ) -> Result<(Size, Vec)> { + let raster_bounds = self.raster_bounds(params)?; + self.platform_text_system + .rasterize_glyph(params, raster_bounds) + } +} + +/// The GPUI text layout subsystem. +#[derive(Deref)] +pub struct WindowTextSystem { + line_layout_cache: LineLayoutCache, + #[deref] + text_system: Arc, +} + +impl WindowTextSystem { + pub(crate) fn new(text_system: Arc) -> Self { + Self { + line_layout_cache: LineLayoutCache::new(text_system.platform_text_system.clone()), + text_system, + } + } + + pub(crate) fn layout_index(&self) -> LineLayoutIndex { + self.line_layout_cache.layout_index() + } + + pub(crate) fn reuse_layouts(&self, index: Range) { + self.line_layout_cache.reuse_layouts(index) + } + + pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) { + self.line_layout_cache.truncate_layouts(index) + } + + /// Shape the given line, at the given font_size, for painting to the screen. + /// Subsets of the line can be styled independently with the `runs` parameter. + /// + /// Note that this method can only shape a single line of text. It will panic + /// if the text contains newlines. If you need to shape multiple lines of text, + /// use `TextLayout::shape_text` instead. + pub fn shape_line( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + ) -> Result { + debug_assert!( + text.find('\n').is_none(), + "text argument should not contain newlines" + ); + + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + for run in runs { + if let Some(last_run) = decoration_runs.last_mut() { + if last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run.len as u32; + continue; + } + } + decoration_runs.push(DecorationRun { + len: run.len as u32, + color: run.color, + background_color: run.background_color, + underline: run.underline, + strikethrough: run.strikethrough, + }); + } + + let layout = self.layout_line(text.as_ref(), font_size, runs)?; + + Ok(ShapedLine { + layout, + text, + decoration_runs, + }) + } + + /// Shape a multi line string of text, at the given font_size, for painting to the screen. + /// Subsets of the text can be styled independently with the `runs` parameter. + /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width. + pub fn shape_text( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + wrap_width: Option, + ) -> Result> { + let mut runs = runs.iter().cloned().peekable(); + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + + let mut lines = SmallVec::new(); + let mut line_start = 0; + + let mut process_line = |line_text: SharedString| { + let line_end = line_start + line_text.len(); + + let mut last_font: Option = None; + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + let mut run_start = line_start; + while run_start < line_end { + let Some(run) = runs.peek_mut() else { + break; + }; + + let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start; + + if last_font == Some(run.font.clone()) { + font_runs.last_mut().unwrap().len += run_len_within_line; + } else { + last_font = Some(run.font.clone()); + font_runs.push(FontRun { + len: run_len_within_line, + font_id: self.resolve_font(&run.font), + }); + } + + if decoration_runs.last().map_or(false, |last_run| { + last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + }) { + decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; + } else { + decoration_runs.push(DecorationRun { + len: run_len_within_line as u32, + color: run.color, + background_color: run.background_color, + underline: run.underline, + strikethrough: run.strikethrough, + }); + } + + if run_len_within_line == run.len { + runs.next(); + } else { + // Preserve the remainder of the run for the next line + run.len -= run_len_within_line; + } + run_start += run_len_within_line; + } + + let layout = self + .line_layout_cache + .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width); + + lines.push(WrappedLine { + layout, + decoration_runs, + text: line_text, + }); + + // Skip `\n` character. + line_start = line_end + 1; + if let Some(run) = runs.peek_mut() { + run.len = run.len.saturating_sub(1); + if run.len == 0 { + runs.next(); + } + } + + font_runs.clear(); + }; + + let mut split_lines = text.split('\n'); + let mut processed = false; + + if let Some(first_line) = split_lines.next() { + if let Some(second_line) = split_lines.next() { + processed = true; + process_line(first_line.to_string().into()); + process_line(second_line.to_string().into()); + for line_text in split_lines { + process_line(line_text.to_string().into()); + } + } + } + + if !processed { + process_line(text); + } + + self.font_runs_pool.lock().push(font_runs); + + Ok(lines) + } + + pub(crate) fn finish_frame(&self) { + self.line_layout_cache.finish_frame() + } + + /// Layout the given line of text, at the given font_size. + /// Subsets of the line can be styled independently with the `runs` parameter. + /// Generally, you should prefer to use `TextLayout::shape_line` instead, which + /// can be painted directly. + pub fn layout_line( + &self, + text: &str, + font_size: Pixels, + runs: &[TextRun], + ) -> Result> { + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + for run in runs.iter() { + let font_id = self.resolve_font(&run.font); + if let Some(last_run) = font_runs.last_mut() { + if last_run.font_id == font_id { + last_run.len += run.len; + continue; + } + } + font_runs.push(FontRun { + len: run.len, + font_id, + }); + } + + let layout = self + .line_layout_cache + .layout_line(text, font_size, &font_runs); + + font_runs.clear(); + self.font_runs_pool.lock().push(font_runs); + + Ok(layout) + } +} + +#[derive(Hash, Eq, PartialEq)] +struct FontIdWithSize { + font_id: FontId, + font_size: Pixels, +} + +/// A handle into the text system, which can be used to compute the wrapped layout of text +pub struct LineWrapperHandle { + wrapper: Option, + text_system: Arc, +} + +impl Drop for LineWrapperHandle { + fn drop(&mut self) { + let mut state = self.text_system.wrapper_pool.lock(); + let wrapper = self.wrapper.take().unwrap(); + state + .get_mut(&FontIdWithSize { + font_id: wrapper.font_id, + font_size: wrapper.font_size, + }) + .unwrap() + .push(wrapper); + } +} + +impl Deref for LineWrapperHandle { + type Target = LineWrapper; + + fn deref(&self) -> &Self::Target { + self.wrapper.as_ref().unwrap() + } +} + +impl DerefMut for LineWrapperHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.wrapper.as_mut().unwrap() + } +} + +/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0, +/// with 400.0 as normal. +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct FontWeight(pub f32); + +impl Default for FontWeight { + #[inline] + fn default() -> FontWeight { + FontWeight::NORMAL + } +} + +impl Hash for FontWeight { + fn hash(&self, state: &mut H) { + state.write_u32(u32::from_be_bytes(self.0.to_be_bytes())); + } +} + +impl Eq for FontWeight {} + +impl FontWeight { + /// Thin weight (100), the thinnest value. + pub const THIN: FontWeight = FontWeight(100.0); + /// Extra light weight (200). + pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0); + /// Light weight (300). + pub const LIGHT: FontWeight = FontWeight(300.0); + /// Normal (400). + pub const NORMAL: FontWeight = FontWeight(400.0); + /// Medium weight (500, higher than normal). + pub const MEDIUM: FontWeight = FontWeight(500.0); + /// Semibold weight (600). + pub const SEMIBOLD: FontWeight = FontWeight(600.0); + /// Bold weight (700). + pub const BOLD: FontWeight = FontWeight(700.0); + /// Extra-bold weight (800). + pub const EXTRA_BOLD: FontWeight = FontWeight(800.0); + /// Black weight (900), the thickest value. + pub const BLACK: FontWeight = FontWeight(900.0); +} + +/// Allows italic or oblique faces to be selected. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default)] +pub enum FontStyle { + /// A face that is neither italic not obliqued. + #[default] + Normal, + /// A form that is generally cursive in nature. + Italic, + /// A typically-sloped version of the regular face. + Oblique, +} + +impl Display for FontStyle { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(self, f) + } +} + +/// A styled run of text, for use in [`TextLayout`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TextRun { + /// A number of utf8 bytes + pub len: usize, + /// The font to use for this run. + pub font: Font, + /// The color + pub color: Hsla, + /// The background color (if any) + pub background_color: Option, + /// The underline style (if any) + pub underline: Option, + /// The strikethrough style (if any) + pub strikethrough: Option, +} + +/// An identifier for a specific glyph, as returned by [`TextSystem::layout_line`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[repr(C)] +pub struct GlyphId(pub(crate) u32); + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct RenderGlyphParams { + pub(crate) font_id: FontId, + pub(crate) glyph_id: GlyphId, + pub(crate) font_size: Pixels, + pub(crate) subpixel_variant: Point, + pub(crate) scale_factor: f32, + pub(crate) is_emoji: bool, +} + +impl Eq for RenderGlyphParams {} + +impl Hash for RenderGlyphParams { + fn hash(&self, state: &mut H) { + self.font_id.0.hash(state); + self.glyph_id.0.hash(state); + self.font_size.0.to_bits().hash(state); + self.subpixel_variant.hash(state); + self.scale_factor.to_bits().hash(state); + } +} + +/// The parameters for rendering an emoji glyph. +#[derive(Clone, Debug, PartialEq)] +pub struct RenderEmojiParams { + pub(crate) font_id: FontId, + pub(crate) glyph_id: GlyphId, + pub(crate) font_size: Pixels, + pub(crate) scale_factor: f32, +} + +impl Eq for RenderEmojiParams {} + +impl Hash for RenderEmojiParams { + fn hash(&self, state: &mut H) { + self.font_id.0.hash(state); + self.glyph_id.0.hash(state); + self.font_size.0.to_bits().hash(state); + self.scale_factor.to_bits().hash(state); + } +} + +/// The configuration details for identifying a specific font. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct Font { + /// The font family name. + /// + /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform. + pub family: SharedString, + + /// The font features to use. + pub features: FontFeatures, + + /// The font weight. + pub weight: FontWeight, + + /// The font style. + pub style: FontStyle, +} + +/// Get a [`Font`] for a given name. +pub fn font(family: impl Into) -> Font { + Font { + family: family.into(), + features: FontFeatures::default(), + weight: FontWeight::default(), + style: FontStyle::default(), + } +} + +impl Font { + /// Set this Font to be bold + pub fn bold(mut self) -> Self { + self.weight = FontWeight::BOLD; + self + } + + /// Set this Font to be italic + pub fn italic(mut self) -> Self { + self.style = FontStyle::Italic; + self + } +} + +/// A struct for storing font metrics. +/// It is used to define the measurements of a typeface. +#[derive(Clone, Copy, Debug)] +pub struct FontMetrics { + /// The number of font units that make up the "em square", + /// a scalable grid for determining the size of a typeface. + pub(crate) units_per_em: u32, + + /// The vertical distance from the baseline of the font to the top of the glyph covers. + pub(crate) ascent: f32, + + /// The vertical distance from the baseline of the font to the bottom of the glyph covers. + pub(crate) descent: f32, + + /// The recommended additional space to add between lines of type. + pub(crate) line_gap: f32, + + /// The suggested position of the underline. + pub(crate) underline_position: f32, + + /// The suggested thickness of the underline. + pub(crate) underline_thickness: f32, + + /// The height of a capital letter measured from the baseline of the font. + pub(crate) cap_height: f32, + + /// The height of a lowercase x. + pub(crate) x_height: f32, + + /// The outer limits of the area that the font covers. + /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table + pub(crate) bounding_box: Bounds, +} + +impl FontMetrics { + /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels. + pub fn ascent(&self, font_size: Pixels) -> Pixels { + Pixels((self.ascent / self.units_per_em as f32) * font_size.0) + } + + /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels. + pub fn descent(&self, font_size: Pixels) -> Pixels { + Pixels((self.descent / self.units_per_em as f32) * font_size.0) + } + + /// Returns the recommended additional space to add between lines of type in pixels. + pub fn line_gap(&self, font_size: Pixels) -> Pixels { + Pixels((self.line_gap / self.units_per_em as f32) * font_size.0) + } + + /// Returns the suggested position of the underline in pixels. + pub fn underline_position(&self, font_size: Pixels) -> Pixels { + Pixels((self.underline_position / self.units_per_em as f32) * font_size.0) + } + + /// Returns the suggested thickness of the underline in pixels. + pub fn underline_thickness(&self, font_size: Pixels) -> Pixels { + Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0) + } + + /// Returns the height of a capital letter measured from the baseline of the font in pixels. + pub fn cap_height(&self, font_size: Pixels) -> Pixels { + Pixels((self.cap_height / self.units_per_em as f32) * font_size.0) + } + + /// Returns the height of a lowercase x in pixels. + pub fn x_height(&self, font_size: Pixels) -> Pixels { + Pixels((self.x_height / self.units_per_em as f32) * font_size.0) + } + + /// Returns the outer limits of the area that the font covers in pixels. + pub fn bounding_box(&self, font_size: Pixels) -> Bounds { + (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px) + } +} diff --git a/crates/ming/src/text_system/font_features.rs b/crates/ming/src/text_system/font_features.rs new file mode 100644 index 0000000..39ec18f --- /dev/null +++ b/crates/ming/src/text_system/font_features.rs @@ -0,0 +1,274 @@ +#[cfg(target_os = "windows")] +use crate::SharedString; +#[cfg(target_os = "windows")] +use itertools::Itertools; +use schemars::{ + schema::{InstanceType, Schema, SchemaObject, SingleOrVec}, + JsonSchema, +}; + +macro_rules! create_definitions { + ($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => { + + /// The OpenType features that can be configured for a given font. + #[derive(Default, Clone, Eq, PartialEq, Hash)] + pub struct FontFeatures { + enabled: u64, + disabled: u64, + #[cfg(target_os = "windows")] + other_enabled: SharedString, + #[cfg(target_os = "windows")] + other_disabled: SharedString, + } + + impl FontFeatures { + $( + /// Get the current value of the corresponding OpenType feature + pub fn $name(&self) -> Option { + if (self.enabled & (1 << $idx)) != 0 { + Some(true) + } else if (self.disabled & (1 << $idx)) != 0 { + Some(false) + } else { + None + } + } + )* + + /// Get the tag name list of the font OpenType features + /// only enabled or disabled features are returned + #[cfg(target_os = "windows")] + pub fn tag_value_list(&self) -> Vec<(String, bool)> { + let mut result = Vec::new(); + $( + { + let value = if (self.enabled & (1 << $idx)) != 0 { + Some(true) + } else if (self.disabled & (1 << $idx)) != 0 { + Some(false) + } else { + None + }; + if let Some(enable) = value { + let tag_name = stringify!($name).to_owned(); + result.push((tag_name, enable)); + } + } + )* + { + for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() { + result.push((name.collect::(), true)); + } + for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() { + result.push((name.collect::(), false)); + } + } + result + } + } + + impl std::fmt::Debug for FontFeatures { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug = f.debug_struct("FontFeatures"); + $( + if let Some(value) = self.$name() { + debug.field(stringify!($name), &value); + }; + )* + #[cfg(target_os = "windows")] + { + for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() { + debug.field(name.collect::().as_str(), &true); + } + for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() { + debug.field(name.collect::().as_str(), &false); + } + } + debug.finish() + } + } + + impl<'de> serde::Deserialize<'de> for FontFeatures { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{MapAccess, Visitor}; + use std::fmt; + + struct FontFeaturesVisitor; + + impl<'de> Visitor<'de> for FontFeaturesVisitor { + type Value = FontFeatures; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map of font features") + } + + #[cfg(not(target_os = "windows"))] + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut enabled: u64 = 0; + let mut disabled: u64 = 0; + + while let Some((key, value)) = access.next_entry::>()? { + let idx = match key.as_str() { + $(stringify!($name) => $idx,)* + _ => continue, + }; + match value { + Some(true) => enabled |= 1 << idx, + Some(false) => disabled |= 1 << idx, + None => {} + }; + } + Ok(FontFeatures { enabled, disabled }) + } + + #[cfg(target_os = "windows")] + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut enabled: u64 = 0; + let mut disabled: u64 = 0; + let mut other_enabled = "".to_owned(); + let mut other_disabled = "".to_owned(); + + while let Some((key, value)) = access.next_entry::>()? { + let idx = match key.as_str() { + $(stringify!($name) => Some($idx),)* + other_feature => { + if other_feature.len() != 4 || !other_feature.is_ascii() { + log::error!("Incorrect feature name: {}", other_feature); + continue; + } + None + }, + }; + if let Some(idx) = idx { + match value { + Some(true) => enabled |= 1 << idx, + Some(false) => disabled |= 1 << idx, + None => {} + }; + } else { + match value { + Some(true) => other_enabled.push_str(key.as_str()), + Some(false) => other_disabled.push_str(key.as_str()), + None => {} + }; + } + } + let other_enabled = if other_enabled.is_empty() { + "".into() + } else { + other_enabled.into() + }; + let other_disabled = if other_disabled.is_empty() { + "".into() + } else { + other_disabled.into() + }; + Ok(FontFeatures { enabled, disabled, other_enabled, other_disabled }) + } + } + + let features = deserializer.deserialize_map(FontFeaturesVisitor)?; + Ok(features) + } + } + + impl serde::Serialize for FontFeatures { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(None)?; + + $( + { + let feature = stringify!($name); + if let Some(value) = self.$name() { + map.serialize_entry(feature, &value)?; + } + } + )* + + #[cfg(target_os = "windows")] + { + for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() { + map.serialize_entry(name.collect::().as_str(), &true)?; + } + for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() { + map.serialize_entry(name.collect::().as_str(), &false)?; + } + } + + map.end() + } + } + + impl JsonSchema for FontFeatures { + fn schema_name() -> String { + "FontFeatures".into() + } + + fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema { + let mut schema = SchemaObject::default(); + let properties = &mut schema.object().properties; + let feature_schema = Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))), + ..Default::default() + }); + + $( + properties.insert(stringify!($name).to_owned(), feature_schema.clone()); + )* + + schema.into() + } + } + }; +} + +create_definitions!( + (calt, 0), + (case, 1), + (cpsp, 2), + (frac, 3), + (liga, 4), + (onum, 5), + (ordn, 6), + (pnum, 7), + (ss01, 8), + (ss02, 9), + (ss03, 10), + (ss04, 11), + (ss05, 12), + (ss06, 13), + (ss07, 14), + (ss08, 15), + (ss09, 16), + (ss10, 17), + (ss11, 18), + (ss12, 19), + (ss13, 20), + (ss14, 21), + (ss15, 22), + (ss16, 23), + (ss17, 24), + (ss18, 25), + (ss19, 26), + (ss20, 27), + (subs, 28), + (sups, 29), + (swsh, 30), + (titl, 31), + (tnum, 32), + (zero, 33), +); diff --git a/crates/ming/src/text_system/line.rs b/crates/ming/src/text_system/line.rs new file mode 100644 index 0000000..a9a52f0 --- /dev/null +++ b/crates/ming/src/text_system/line.rs @@ -0,0 +1,324 @@ +use crate::{ + black, fill, point, px, size, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, + StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, +}; +use derive_more::{Deref, DerefMut}; +use smallvec::SmallVec; +use std::sync::Arc; + +/// Set the text decoration for a run of text. +#[derive(Debug, Clone)] +pub struct DecorationRun { + /// The length of the run in utf-8 bytes. + pub len: u32, + + /// The color for this run + pub color: Hsla, + + /// The background color for this run + pub background_color: Option, + + /// The underline style for this run + pub underline: Option, + + /// The strikethrough style for this run + pub strikethrough: Option, +} + +/// A line of text that has been shaped and decorated. +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct ShapedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + /// The text that was shaped for this line. + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} + +impl ShapedLine { + /// The length of the line in utf-8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.layout.len + } + + /// Paint the line of text to the window. + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + cx: &mut WindowContext, + ) -> Result<()> { + paint_line( + origin, + &self.layout, + line_height, + &self.decoration_runs, + &[], + cx, + )?; + + Ok(()) + } +} + +/// A line of text that has been shaped, decorated, and wrapped by the text layout system. +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct WrappedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + /// The text that was shaped for this line. + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} + +impl WrappedLine { + /// The length of the underlying, unwrapped layout, in utf-8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.layout.len() + } + + /// Paint this line of text to the window. + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + cx: &mut WindowContext, + ) -> Result<()> { + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + &self.decoration_runs, + &self.wrap_boundaries, + cx, + )?; + + Ok(()) + } +} + +fn paint_line( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + decoration_runs: &[DecorationRun], + wrap_boundaries: &[WrapBoundary], + cx: &mut WindowContext, +) -> Result<()> { + let line_bounds = Bounds::new(origin, size(layout.width, line_height)); + cx.paint_layer(line_bounds, |cx| { + let padding_top = (line_height - layout.ascent - layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + layout.ascent); + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut color = black(); + let mut current_underline: Option<(Point, UnderlineStyle)> = None; + let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; + let mut current_background: Option<(Point, Hsla)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = origin; + let mut prev_glyph_position = Point::default(); + for (run_ix, run) in layout.runs.iter().enumerate() { + let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; + + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; + + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((background_origin, background_color)) = current_background.as_mut() + { + cx.paint_quad(fill( + Bounds { + origin: *background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + *background_color, + )); + background_origin.x = origin.x; + background_origin.y += line_height; + } + if let Some((underline_origin, underline_style)) = current_underline.as_mut() { + cx.paint_underline( + *underline_origin, + glyph_origin.x - underline_origin.x, + underline_style, + ); + underline_origin.x = origin.x; + underline_origin.y += line_height; + } + if let Some((strikethrough_origin, strikethrough_style)) = + current_strikethrough.as_mut() + { + cx.paint_strikethrough( + *strikethrough_origin, + glyph_origin.x - strikethrough_origin.x, + strikethrough_style, + ); + strikethrough_origin.x = origin.x; + strikethrough_origin.y += line_height; + } + + glyph_origin.x = origin.x; + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; + + let mut finished_background: Option<(Point, Hsla)> = None; + let mut finished_underline: Option<(Point, UnderlineStyle)> = None; + let mut finished_strikethrough: Option<(Point, StrikethroughStyle)> = None; + if glyph.index >= run_end { + if let Some(style_run) = decoration_runs.next() { + if let Some((_, background_color)) = &mut current_background { + if style_run.background_color.as_ref() != Some(background_color) { + finished_background = current_background.take(); + } + } + if let Some(run_background) = style_run.background_color { + current_background.get_or_insert(( + point(glyph_origin.x, glyph_origin.y), + run_background, + )); + } + + if let Some((_, underline_style)) = &mut current_underline { + if style_run.underline.as_ref() != Some(underline_style) { + finished_underline = current_underline.take(); + } + } + if let Some(run_underline) = style_run.underline.as_ref() { + current_underline.get_or_insert(( + point( + glyph_origin.x, + glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), + ), + UnderlineStyle { + color: Some(run_underline.color.unwrap_or(style_run.color)), + thickness: run_underline.thickness, + wavy: run_underline.wavy, + }, + )); + } + if let Some((_, strikethrough_style)) = &mut current_strikethrough { + if style_run.strikethrough.as_ref() != Some(strikethrough_style) { + finished_strikethrough = current_strikethrough.take(); + } + } + if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { + current_strikethrough.get_or_insert(( + point( + glyph_origin.x, + glyph_origin.y + + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5), + ), + StrikethroughStyle { + color: Some(run_strikethrough.color.unwrap_or(style_run.color)), + thickness: run_strikethrough.thickness, + }, + )); + } + + run_end += style_run.len as usize; + color = style_run.color; + } else { + run_end = layout.len; + finished_background = current_background.take(); + finished_underline = current_underline.take(); + finished_strikethrough = current_strikethrough.take(); + } + } + + if let Some((background_origin, background_color)) = finished_background { + cx.paint_quad(fill( + Bounds { + origin: background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + background_color, + )); + } + + if let Some((underline_origin, underline_style)) = finished_underline { + cx.paint_underline( + underline_origin, + glyph_origin.x - underline_origin.x, + &underline_style, + ); + } + + if let Some((strikethrough_origin, strikethrough_style)) = finished_strikethrough { + cx.paint_strikethrough( + strikethrough_origin, + glyph_origin.x - strikethrough_origin.x, + &strikethrough_style, + ); + } + + let max_glyph_bounds = Bounds { + origin: glyph_origin, + size: max_glyph_size, + }; + + let content_mask = cx.content_mask(); + if max_glyph_bounds.intersects(&content_mask.bounds) { + if glyph.is_emoji { + cx.paint_emoji( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + )?; + } else { + cx.paint_glyph( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + color, + )?; + } + } + } + } + + let mut last_line_end_x = origin.x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } + + if let Some((background_origin, background_color)) = current_background.take() { + cx.paint_quad(fill( + Bounds { + origin: background_origin, + size: size(last_line_end_x - background_origin.x, line_height), + }, + background_color, + )); + } + + if let Some((underline_start, underline_style)) = current_underline.take() { + cx.paint_underline( + underline_start, + last_line_end_x - underline_start.x, + &underline_style, + ); + } + + if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() { + cx.paint_strikethrough( + strikethrough_start, + last_line_end_x - strikethrough_start.x, + &strikethrough_style, + ); + } + + Ok(()) + }) +} diff --git a/crates/ming/src/text_system/line_layout.rs b/crates/ming/src/text_system/line_layout.rs new file mode 100644 index 0000000..aa1b96b --- /dev/null +++ b/crates/ming/src/text_system/line_layout.rs @@ -0,0 +1,577 @@ +use crate::{point, px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size}; +use collections::FxHashMap; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::SmallVec; +use std::{ + borrow::Borrow, + hash::{Hash, Hasher}, + ops::Range, + sync::Arc, +}; + +/// A laid out and styled line of text +#[derive(Default, Debug)] +pub struct LineLayout { + /// The font size for this line + pub font_size: Pixels, + /// The width of the line + pub width: Pixels, + /// The ascent of the line + pub ascent: Pixels, + /// The descent of the line + pub descent: Pixels, + /// The shaped runs that make up this line + pub runs: Vec, + /// The length of the line in utf-8 bytes + pub len: usize, +} + +/// A run of text that has been shaped . +#[derive(Debug)] +pub struct ShapedRun { + /// The font id for this run + pub font_id: FontId, + /// The glyphs that make up this run + pub glyphs: SmallVec<[ShapedGlyph; 8]>, +} + +/// A single glyph, ready to paint. +#[derive(Clone, Debug)] +pub struct ShapedGlyph { + /// The ID for this glyph, as determined by the text system. + pub id: GlyphId, + + /// The position of this glyph in its containing line. + pub position: Point, + + /// The index of this glyph in the original text. + pub index: usize, + + /// Whether this glyph is an emoji + pub is_emoji: bool, +} + +impl LineLayout { + /// The index for the character at the given x coordinate + pub fn index_for_x(&self, x: Pixels) -> Option { + if x >= self.width { + None + } else { + for run in self.runs.iter().rev() { + for glyph in run.glyphs.iter().rev() { + if glyph.position.x <= x { + return Some(glyph.index); + } + } + } + Some(0) + } + } + + /// closest_index_for_x returns the character boundary closest to the given x coordinate + /// (e.g. to handle aligning up/down arrow keys) + pub fn closest_index_for_x(&self, x: Pixels) -> usize { + let mut prev_index = 0; + let mut prev_x = px(0.); + + for run in self.runs.iter() { + for glyph in run.glyphs.iter() { + if glyph.position.x >= x { + if glyph.position.x - x < x - prev_x { + return glyph.index; + } else { + return prev_index; + } + } + prev_index = glyph.index; + prev_x = glyph.position.x; + } + } + + self.len + } + + /// The x position of the character at the given index + pub fn x_for_index(&self, index: usize) -> Pixels { + for run in &self.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return glyph.position.x; + } + } + } + self.width + } + + /// The corresponding Font at the given index + pub fn font_id_for_index(&self, index: usize) -> Option { + for run in &self.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return Some(run.font_id); + } + } + } + None + } + + fn compute_wrap_boundaries( + &self, + text: &str, + wrap_width: Pixels, + ) -> SmallVec<[WrapBoundary; 1]> { + let mut boundaries = SmallVec::new(); + + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = None; + let mut last_candidate_x = px(0.); + let mut last_boundary = WrapBoundary { + run_ix: 0, + glyph_ix: 0, + }; + let mut last_boundary_x = px(0.); + let mut prev_ch = '\0'; + let mut glyphs = self + .runs + .iter() + .enumerate() + .flat_map(move |(run_ix, run)| { + run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| { + let character = text[glyph.index..].chars().next().unwrap(); + ( + WrapBoundary { run_ix, glyph_ix }, + character, + glyph.position.x, + ) + }) + }) + .peekable(); + + while let Some((boundary, ch, x)) = glyphs.next() { + if ch == '\n' { + continue; + } + + if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(boundary); + last_candidate_x = x; + } + + if ch != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(boundary); + } + + let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x); + let width = next_x - last_boundary_x; + if width > wrap_width && boundary > last_boundary { + if let Some(last_candidate_ix) = last_candidate_ix.take() { + last_boundary = last_candidate_ix; + last_boundary_x = last_candidate_x; + } else { + last_boundary = boundary; + last_boundary_x = x; + } + + boundaries.push(last_boundary); + } + prev_ch = ch; + } + + boundaries + } +} + +/// A line of text that has been wrapped to fit a given width +#[derive(Default, Debug)] +pub struct WrappedLineLayout { + /// The line layout, pre-wrapping. + pub unwrapped_layout: Arc, + + /// The boundaries at which the line was wrapped + pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + + /// The width of the line, if it was wrapped + pub wrap_width: Option, +} + +/// A boundary at which a line was wrapped +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct WrapBoundary { + /// The index in the run just before the line was wrapped + pub run_ix: usize, + /// The index of the glyph just before the line was wrapped + pub glyph_ix: usize, +} + +impl WrappedLineLayout { + /// The length of the underlying text, in utf8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.unwrapped_layout.len + } + + /// The width of this line, in pixels, whether or not it was wrapped. + pub fn width(&self) -> Pixels { + self.wrap_width + .unwrap_or(Pixels::MAX) + .min(self.unwrapped_layout.width) + } + + /// The size of the whole wrapped text, for the given line_height. + /// can span multiple lines if there are multiple wrap boundaries. + pub fn size(&self, line_height: Pixels) -> Size { + Size { + width: self.width(), + height: line_height * (self.wrap_boundaries.len() + 1), + } + } + + /// The ascent of a line in this layout + pub fn ascent(&self) -> Pixels { + self.unwrapped_layout.ascent + } + + /// The descent of a line in this layout + pub fn descent(&self) -> Pixels { + self.unwrapped_layout.descent + } + + /// The wrap boundaries in this layout + pub fn wrap_boundaries(&self) -> &[WrapBoundary] { + &self.wrap_boundaries + } + + /// The font size of this layout + pub fn font_size(&self) -> Pixels { + self.unwrapped_layout.font_size + } + + /// The runs in this layout, sans wrapping + pub fn runs(&self) -> &[ShapedRun] { + &self.unwrapped_layout.runs + } + + /// The index corresponding to a given position in this layout for the given line height. + pub fn index_for_position( + &self, + mut position: Point, + line_height: Pixels, + ) -> Result { + let wrapped_line_ix = (position.y / line_height) as usize; + + let wrapped_line_start_index; + let wrapped_line_start_x; + if wrapped_line_ix > 0 { + let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else { + return Err(0); + }; + let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix]; + let glyph = &run.glyphs[line_start_boundary.glyph_ix]; + wrapped_line_start_index = glyph.index; + wrapped_line_start_x = glyph.position.x; + } else { + wrapped_line_start_index = 0; + wrapped_line_start_x = Pixels::ZERO; + }; + + let wrapped_line_end_index; + let wrapped_line_end_x; + if wrapped_line_ix < self.wrap_boundaries.len() { + let next_wrap_boundary_ix = wrapped_line_ix; + let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; + let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; + let glyph = &run.glyphs[next_wrap_boundary.glyph_ix]; + wrapped_line_end_index = glyph.index; + wrapped_line_end_x = glyph.position.x; + } else { + wrapped_line_end_index = self.unwrapped_layout.len; + wrapped_line_end_x = self.unwrapped_layout.width; + }; + + let mut position_in_unwrapped_line = position; + position_in_unwrapped_line.x += wrapped_line_start_x; + if position_in_unwrapped_line.x < wrapped_line_start_x { + Err(wrapped_line_start_index) + } else if position_in_unwrapped_line.x >= wrapped_line_end_x { + Err(wrapped_line_end_index) + } else { + Ok(self + .unwrapped_layout + .index_for_x(position_in_unwrapped_line.x) + .unwrap()) + } + } + + /// todo!() + pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option> { + let mut line_start_ix = 0; + let mut line_end_indices = self + .wrap_boundaries + .iter() + .map(|wrap_boundary| { + let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; + let glyph = &run.glyphs[wrap_boundary.glyph_ix]; + glyph.index + }) + .chain([self.len()]) + .enumerate(); + for (ix, line_end_ix) in line_end_indices { + let line_y = ix as f32 * line_height; + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_start_ix = line_end_ix; + continue; + } else { + let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix); + let x = self.unwrapped_layout.x_for_index(index) - line_start_x; + return Some(point(x, line_y)); + } + } + + None + } +} + +pub(crate) struct LineLayoutCache { + previous_frame: Mutex, + current_frame: RwLock, + platform_text_system: Arc, +} + +#[derive(Default)] +struct FrameCache { + lines: FxHashMap, Arc>, + wrapped_lines: FxHashMap, Arc>, + used_lines: Vec>, + used_wrapped_lines: Vec>, +} + +#[derive(Clone, Default)] +pub(crate) struct LineLayoutIndex { + lines_index: usize, + wrapped_lines_index: usize, +} + +impl LineLayoutCache { + pub fn new(platform_text_system: Arc) -> Self { + Self { + previous_frame: Mutex::default(), + current_frame: RwLock::default(), + platform_text_system, + } + } + + pub fn layout_index(&self) -> LineLayoutIndex { + let frame = self.current_frame.read(); + LineLayoutIndex { + lines_index: frame.used_lines.len(), + wrapped_lines_index: frame.used_wrapped_lines.len(), + } + } + + pub fn reuse_layouts(&self, range: Range) { + let mut previous_frame = &mut *self.previous_frame.lock(); + let mut current_frame = &mut *self.current_frame.write(); + + for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] { + if let Some((key, line)) = previous_frame.lines.remove_entry(key) { + current_frame.lines.insert(key, line); + } + current_frame.used_lines.push(key.clone()); + } + + for key in &previous_frame.used_wrapped_lines + [range.start.wrapped_lines_index..range.end.wrapped_lines_index] + { + if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { + current_frame.wrapped_lines.insert(key, line); + } + current_frame.used_wrapped_lines.push(key.clone()); + } + } + + pub fn truncate_layouts(&self, index: LineLayoutIndex) { + let mut current_frame = &mut *self.current_frame.write(); + current_frame.used_lines.truncate(index.lines_index); + current_frame + .used_wrapped_lines + .truncate(index.wrapped_lines_index); + } + + pub fn finish_frame(&self) { + let mut prev_frame = self.previous_frame.lock(); + let mut curr_frame = self.current_frame.write(); + std::mem::swap(&mut *prev_frame, &mut *curr_frame); + curr_frame.lines.clear(); + curr_frame.wrapped_lines.clear(); + curr_frame.used_lines.clear(); + curr_frame.used_wrapped_lines.clear(); + } + + pub fn layout_wrapped_line( + &self, + text: &str, + font_size: Pixels, + runs: &[FontRun], + wrap_width: Option, + ) -> Arc { + let key = &CacheKeyRef { + text, + font_size, + runs, + wrap_width, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.wrapped_lines.get(key) { + return layout.clone(); + } + + let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key); + if let Some((key, layout)) = previous_frame_entry { + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); + layout + } else { + drop(current_frame); + + let unwrapped_layout = self.layout_line(text, font_size, runs); + let wrap_boundaries = if let Some(wrap_width) = wrap_width { + unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width) + } else { + SmallVec::new() + }; + let layout = Arc::new(WrappedLineLayout { + unwrapped_layout, + wrap_boundaries, + wrap_width, + }); + let key = Arc::new(CacheKey { + text: text.into(), + font_size, + runs: SmallVec::from(runs), + wrap_width, + }); + + let mut current_frame = self.current_frame.write(); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); + + layout + } + } + + pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc { + let key = &CacheKeyRef { + text, + font_size, + runs, + wrap_width: None, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.lines.get(key) { + return layout.clone(); + } + + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) { + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); + layout + } else { + let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs)); + let key = Arc::new(CacheKey { + text: text.into(), + font_size, + runs: SmallVec::from(runs), + wrap_width: None, + }); + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); + layout + } + } +} + +/// A run of text with a single font. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct FontRun { + pub(crate) len: usize, + pub(crate) font_id: FontId, +} + +trait AsCacheKeyRef { + fn as_cache_key_ref(&self) -> CacheKeyRef; +} + +#[derive(Clone, Debug, Eq)] +struct CacheKey { + text: String, + font_size: Pixels, + runs: SmallVec<[FontRun; 1]>, + wrap_width: Option, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +struct CacheKeyRef<'a> { + text: &'a str, + font_size: Pixels, + runs: &'a [FontRun], + wrap_width: Option, +} + +impl<'a> PartialEq for (dyn AsCacheKeyRef + 'a) { + fn eq(&self, other: &dyn AsCacheKeyRef) -> bool { + self.as_cache_key_ref() == other.as_cache_key_ref() + } +} + +impl<'a> Eq for (dyn AsCacheKeyRef + 'a) {} + +impl<'a> Hash for (dyn AsCacheKeyRef + 'a) { + fn hash(&self, state: &mut H) { + self.as_cache_key_ref().hash(state) + } +} + +impl AsCacheKeyRef for CacheKey { + fn as_cache_key_ref(&self) -> CacheKeyRef { + CacheKeyRef { + text: &self.text, + font_size: self.font_size, + runs: self.runs.as_slice(), + wrap_width: self.wrap_width, + } + } +} + +impl PartialEq for CacheKey { + fn eq(&self, other: &Self) -> bool { + self.as_cache_key_ref().eq(&other.as_cache_key_ref()) + } +} + +impl Hash for CacheKey { + fn hash(&self, state: &mut H) { + self.as_cache_key_ref().hash(state); + } +} + +impl<'a> Borrow for Arc { + fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) { + self.as_ref() as &dyn AsCacheKeyRef + } +} + +impl<'a> AsCacheKeyRef for CacheKeyRef<'a> { + fn as_cache_key_ref(&self) -> CacheKeyRef { + *self + } +} diff --git a/crates/ming/src/text_system/line_wrapper.rs b/crates/ming/src/text_system/line_wrapper.rs new file mode 100644 index 0000000..a4f407e --- /dev/null +++ b/crates/ming/src/text_system/line_wrapper.rs @@ -0,0 +1,283 @@ +use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem}; +use collections::HashMap; +use std::{iter, sync::Arc}; + +/// The GPUI line wrapper, used to wrap lines of text to a given width. +pub struct LineWrapper { + platform_text_system: Arc, + pub(crate) font_id: FontId, + pub(crate) font_size: Pixels, + cached_ascii_char_widths: [Option; 128], + cached_other_char_widths: HashMap, +} + +impl LineWrapper { + /// The maximum indent that can be applied to a line. + pub const MAX_INDENT: u32 = 256; + + pub(crate) fn new( + font_id: FontId, + font_size: Pixels, + text_system: Arc, + ) -> Self { + Self { + platform_text_system: text_system, + font_id, + font_size, + cached_ascii_char_widths: [None; 128], + cached_other_char_widths: HashMap::default(), + } + } + + /// Wrap a line of text to the given width with this wrapper's font and font size. + pub fn wrap_line<'a>( + &'a mut self, + line: &'a str, + wrap_width: Pixels, + ) -> impl Iterator + 'a { + let mut width = px(0.); + let mut first_non_whitespace_ix = None; + let mut indent = None; + let mut last_candidate_ix = 0; + let mut last_candidate_width = px(0.); + let mut last_wrap_ix = 0; + let mut prev_c = '\0'; + let mut char_indices = line.char_indices(); + iter::from_fn(move || { + for (ix, c) in char_indices.by_ref() { + if c == '\n' { + continue; + } + + if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let char_width = self.width_for_char(c); + width += char_width; + if width > wrap_width && ix > last_wrap_ix { + if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) + { + indent = Some( + Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), + ); + } + + if last_candidate_ix > 0 { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = char_width; + } + + if let Some(indent) = indent { + width += self.width_for_char(' ') * indent as f32; + } + + return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); + } + prev_c = c; + } + + None + }) + } + + #[inline(always)] + fn width_for_char(&mut self, c: char) -> Pixels { + if (c as u32) < 128 { + if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] { + cached_width + } else { + let width = self.compute_width_for_char(c); + self.cached_ascii_char_widths[c as usize] = Some(width); + width + } + } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) { + *cached_width + } else { + let width = self.compute_width_for_char(c); + self.cached_other_char_widths.insert(c, width); + width + } + } + + fn compute_width_for_char(&self, c: char) -> Pixels { + let mut buffer = [0; 4]; + let buffer = c.encode_utf8(&mut buffer); + self.platform_text_system + .layout_line( + buffer, + self.font_size, + &[FontRun { + len: buffer.len(), + font_id: self.font_id, + }], + ) + .width + } +} + +/// A boundary between two lines of text. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Boundary { + /// The index of the last character in a line + pub ix: usize, + /// The indent of the next line. + pub next_indent: u32, +} + +impl Boundary { + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{font, TestAppContext, TestDispatcher, TextRun, WindowTextSystem, WrapBoundary}; + use rand::prelude::*; + + #[test] + fn test_wrap_line() { + let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); + let cx = TestAppContext::new(dispatcher, None); + + cx.update(|cx| { + let text_system = cx.text_system().clone(); + let mut wrapper = LineWrapper::new( + text_system.font_id(&font("Courier")).unwrap(), + px(16.), + text_system.platform_text_system.clone(), + ); + assert_eq!( + wrapper + .wrap_line("aa bbb cccc ddddd eeee", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0)) + .collect::>(), + &[ + Boundary::new(4, 0), + Boundary::new(11, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line(" aaaaaaa", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 5), + Boundary::new(9, 5), + Boundary::new(11, 5), + ] + ); + assert_eq!( + wrapper + .wrap_line(" ", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 0), + Boundary::new(21, 0) + ] + ); + assert_eq!( + wrapper + .wrap_line(" aaaaaaaaaaaaaa", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 3), + Boundary::new(18, 3), + Boundary::new(22, 3), + ] + ); + }); + } + + // For compatibility with the test macro + use crate as gpui; + + #[crate::test] + fn test_wrap_shaped_line(cx: &mut TestAppContext) { + cx.update(|cx| { + let text_system = WindowTextSystem::new(cx.text_system().clone()); + + let normal = TextRun { + len: 0, + font: font("Helvetica"), + color: Default::default(), + underline: Default::default(), + strikethrough: None, + background_color: None, + }; + let bold = TextRun { + len: 0, + font: font("Helvetica").bold(), + color: Default::default(), + underline: Default::default(), + strikethrough: None, + background_color: None, + }; + + impl TextRun { + fn with_len(&self, len: usize) -> Self { + let mut this = self.clone(); + this.len = len; + this + } + } + + let text = "aa bbb cccc ddddd eeee".into(); + let lines = text_system + .shape_text( + text, + px(16.), + &[ + normal.with_len(4), + bold.with_len(5), + normal.with_len(6), + bold.with_len(1), + normal.with_len(7), + ], + Some(px(72.)), + ) + .unwrap(); + + assert_eq!( + lines[0].layout.wrap_boundaries(), + &[ + WrapBoundary { + run_ix: 1, + glyph_ix: 3 + }, + WrapBoundary { + run_ix: 2, + glyph_ix: 3 + }, + WrapBoundary { + run_ix: 4, + glyph_ix: 2 + } + ], + ); + }); + } +} diff --git a/crates/ming/src/util.rs b/crates/ming/src/util.rs new file mode 100644 index 0000000..4bff3da --- /dev/null +++ b/crates/ming/src/util.rs @@ -0,0 +1,103 @@ +#[cfg(any(test, feature = "test-support"))] +use std::time::Duration; + +#[cfg(any(test, feature = "test-support"))] +use futures::Future; + +#[cfg(any(test, feature = "test-support"))] +use smol::future::FutureExt; + +pub use util::*; + +/// A helper trait for building complex objects with imperative conditionals in a fluent style. +pub trait FluentBuilder { + /// Imperatively modify self with the given closure. + fn map(self, f: impl FnOnce(Self) -> U) -> U + where + Self: Sized, + { + f(self) + } + + /// Conditionally modify self with the given closure. + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| if condition { then(this) } else { this }) + } + + /// Conditionally unwrap and modify self with the given closure, if the given option is Some. + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + this + } + }) + } + + /// Conditionally modify self with one closure or another + fn when_else( + self, + condition: bool, + then: impl FnOnce(Self) -> Self, + otherwise: impl FnOnce(Self) -> Self, + ) -> Self + where + Self: Sized, + { + self.map(|this| { + if condition { + then(this) + } else { + otherwise(this) + } + }) + } +} + +#[cfg(any(test, feature = "test-support"))] +pub async fn timeout(timeout: Duration, f: F) -> Result +where + F: Future, +{ + let timer = async { + smol::Timer::after(timeout).await; + Err(()) + }; + let future = async move { Ok(f.await) }; + timer.race(future).await +} + +#[cfg(any(test, feature = "test-support"))] +pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace); + +#[cfg(any(test, feature = "test-support"))] +impl<'a> std::fmt::Debug for CwdBacktrace<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use backtrace::{BacktraceFmt, BytesOrWideString}; + + let cwd = std::env::current_dir().unwrap(); + let cwd = cwd.parent().unwrap(); + let mut print_path = |fmt: &mut std::fmt::Formatter<'_>, path: BytesOrWideString<'_>| { + std::fmt::Display::fmt(&path, fmt) + }; + let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path); + for frame in self.0.frames() { + let mut formatted_frame = fmt.frame(); + if frame + .symbols() + .iter() + .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd))) + { + formatted_frame.backtrace_frame(frame)?; + } + } + fmt.finish() + } +} diff --git a/crates/ming/src/view.rs b/crates/ming/src/view.rs new file mode 100644 index 0000000..fb96f75 --- /dev/null +++ b/crates/ming/src/view.rs @@ -0,0 +1,469 @@ +use crate::Empty; +use crate::{ + seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds, ContentMask, Element, + ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, GlobalElementId, IntoElement, + LayoutId, Model, PaintIndex, Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, + TextStyle, ViewContext, VisualContext, WeakModel, WindowContext, +}; +use anyhow::{Context, Result}; +use refineable::Refineable; +use std::{ + any::{type_name, TypeId}, + fmt, + hash::{Hash, Hasher}, + ops::Range, +}; + +/// A view is a piece of state that can be presented on screen by implementing the [Render] trait. +/// Views implement [Element] and can composed with other views, and every window is created with a root view. +pub struct View { + /// A view is just a [Model] whose type implements `Render`, and the model is accessible via this field. + pub model: Model, +} + +impl Sealed for View {} + +struct AnyViewState { + prepaint_range: Range, + paint_range: Range, + cache_key: ViewCacheKey, +} + +#[derive(Default)] +struct ViewCacheKey { + bounds: Bounds, + content_mask: ContentMask, + text_style: TextStyle, +} + +impl Entity for View { + type Weak = WeakView; + + fn entity_id(&self) -> EntityId { + self.model.entity_id + } + + fn downgrade(&self) -> Self::Weak { + WeakView { + model: self.model.downgrade(), + } + } + + fn upgrade_from(weak: &Self::Weak) -> Option + where + Self: Sized, + { + let model = weak.model.upgrade()?; + Some(View { model }) + } +} + +impl View { + /// Convert this strong view reference into a weak view reference. + pub fn downgrade(&self) -> WeakView { + Entity::downgrade(self) + } + + /// Updates the view's state with the given function, which is passed a mutable reference and a context. + pub fn update( + &self, + cx: &mut C, + f: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> C::Result + where + C: VisualContext, + { + cx.update_view(self, f) + } + + /// Obtain a read-only reference to this view's state. + pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V { + self.model.read(cx) + } + + /// Gets a [FocusHandle] for this view when its state implements [FocusableView]. + pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle + where + V: FocusableView, + { + self.read(cx).focus_handle(cx) + } +} + +impl Element for View { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + Some(ElementId::View(self.entity_id())) + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element()); + let layout_id = element.request_layout(cx); + (layout_id, element) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _: Bounds, + element: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) { + cx.set_view_id(self.entity_id()); + element.prepaint(cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _: Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + element.paint(cx); + } +} + +impl Clone for View { + fn clone(&self) -> Self { + Self { + model: self.model.clone(), + } + } +} + +impl std::fmt::Debug for View { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(&format!("View<{}>", type_name::())) + .field("entity_id", &self.model.entity_id) + .finish_non_exhaustive() + } +} + +impl Hash for View { + fn hash(&self, state: &mut H) { + self.model.hash(state); + } +} + +impl PartialEq for View { + fn eq(&self, other: &Self) -> bool { + self.model == other.model + } +} + +impl Eq for View {} + +/// A weak variant of [View] which does not prevent the view from being released. +pub struct WeakView { + pub(crate) model: WeakModel, +} + +impl WeakView { + /// Gets the entity id associated with this handle. + pub fn entity_id(&self) -> EntityId { + self.model.entity_id + } + + /// Obtain a strong handle for the view if it hasn't been released. + pub fn upgrade(&self) -> Option> { + Entity::upgrade_from(self) + } + + /// Updates this view's state if it hasn't been released. + /// Returns an error if this view has been released. + pub fn update( + &self, + cx: &mut C, + f: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Result + where + C: VisualContext, + Result>: Flatten, + { + let view = self.upgrade().context("error upgrading view")?; + Ok(view.update(cx, f)).flatten() + } + + /// Assert that the view referenced by this handle has been released. + #[cfg(any(test, feature = "test-support"))] + pub fn assert_released(&self) { + self.model.assert_released() + } +} + +impl Clone for WeakView { + fn clone(&self) -> Self { + Self { + model: self.model.clone(), + } + } +} + +impl Hash for WeakView { + fn hash(&self, state: &mut H) { + self.model.hash(state); + } +} + +impl PartialEq for WeakView { + fn eq(&self, other: &Self) -> bool { + self.model == other.model + } +} + +impl Eq for WeakView {} + +/// A dynamically-typed handle to a view, which can be downcast to a [View] for a specific type. +#[derive(Clone, Debug)] +pub struct AnyView { + model: AnyModel, + render: fn(&AnyView, &mut WindowContext) -> AnyElement, + cached_style: Option, +} + +impl AnyView { + /// Indicate that this view should be cached when using it as an element. + /// When using this method, the view's previous layout and paint will be recycled from the previous frame if [ViewContext::notify] has not been called since it was rendered. + /// The one exception is when [WindowContext::refresh] is called, in which case caching is ignored. + pub fn cached(mut self, style: StyleRefinement) -> Self { + self.cached_style = Some(style); + self + } + + /// Convert this to a weak handle. + pub fn downgrade(&self) -> AnyWeakView { + AnyWeakView { + model: self.model.downgrade(), + render: self.render, + } + } + + /// Convert this to a [View] of a specific type. + /// If this handle does not contain a view of the specified type, returns itself in an `Err` variant. + pub fn downcast(self) -> Result, Self> { + match self.model.downcast() { + Ok(model) => Ok(View { model }), + Err(model) => Err(Self { + model, + render: self.render, + cached_style: self.cached_style, + }), + } + } + + /// Gets the [TypeId] of the underlying view. + pub fn entity_type(&self) -> TypeId { + self.model.entity_type + } + + /// Gets the entity id of this handle. + pub fn entity_id(&self) -> EntityId { + self.model.entity_id() + } +} + +impl From> for AnyView { + fn from(value: View) -> Self { + AnyView { + model: value.model.into_any(), + render: any_view::render::, + cached_style: None, + } + } +} + +impl Element for AnyView { + type RequestLayoutState = Option; + type PrepaintState = Option; + + fn id(&self) -> Option { + Some(ElementId::View(self.entity_id())) + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + if let Some(style) = self.cached_style.as_ref() { + let mut root_style = Style::default(); + root_style.refine(style); + let layout_id = cx.request_layout(root_style, None); + (layout_id, None) + } else { + let mut element = (self.render)(self, cx); + let layout_id = element.request_layout(cx); + (layout_id, Some(element)) + } + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + element: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Option { + cx.set_view_id(self.entity_id()); + if self.cached_style.is_some() { + cx.with_element_state::(global_id.unwrap(), |element_state, cx| { + let content_mask = cx.content_mask(); + let text_style = cx.text_style(); + + if let Some(mut element_state) = element_state { + if element_state.cache_key.bounds == bounds + && element_state.cache_key.content_mask == content_mask + && element_state.cache_key.text_style == text_style + && !cx.window.dirty_views.contains(&self.entity_id()) + && !cx.window.refreshing + { + let prepaint_start = cx.prepaint_index(); + cx.reuse_prepaint(element_state.prepaint_range.clone()); + let prepaint_end = cx.prepaint_index(); + element_state.prepaint_range = prepaint_start..prepaint_end; + return (None, element_state); + } + } + + let prepaint_start = cx.prepaint_index(); + let mut element = (self.render)(self, cx); + element.layout_as_root(bounds.size.into(), cx); + element.prepaint_at(bounds.origin, cx); + let prepaint_end = cx.prepaint_index(); + + ( + Some(element), + AnyViewState { + prepaint_range: prepaint_start..prepaint_end, + paint_range: PaintIndex::default()..PaintIndex::default(), + cache_key: ViewCacheKey { + bounds, + content_mask, + text_style, + }, + }, + ) + }) + } else { + let mut element = element.take().unwrap(); + element.prepaint(cx); + Some(element) + } + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + _bounds: Bounds, + _: &mut Self::RequestLayoutState, + element: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + if self.cached_style.is_some() { + cx.with_element_state::(global_id.unwrap(), |element_state, cx| { + let mut element_state = element_state.unwrap(); + + let paint_start = cx.paint_index(); + + if let Some(element) = element { + element.paint(cx); + } else { + cx.reuse_paint(element_state.paint_range.clone()); + } + + let paint_end = cx.paint_index(); + element_state.paint_range = paint_start..paint_end; + + ((), element_state) + }) + } else { + element.as_mut().unwrap().paint(cx); + } + } +} + +impl IntoElement for View { + type Element = View; + + fn into_element(self) -> Self::Element { + self + } +} + +impl IntoElement for AnyView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// A weak, dynamically-typed view handle that does not prevent the view from being released. +pub struct AnyWeakView { + model: AnyWeakModel, + render: fn(&AnyView, &mut WindowContext) -> AnyElement, +} + +impl AnyWeakView { + /// Convert to a strongly-typed handle if the referenced view has not yet been released. + pub fn upgrade(&self) -> Option { + let model = self.model.upgrade()?; + Some(AnyView { + model, + render: self.render, + cached_style: None, + }) + } +} + +impl From> for AnyWeakView { + fn from(view: WeakView) -> Self { + Self { + model: view.model.into(), + render: any_view::render::, + } + } +} + +impl PartialEq for AnyWeakView { + fn eq(&self, other: &Self) -> bool { + self.model == other.model + } +} + +impl std::fmt::Debug for AnyWeakView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AnyWeakView") + .field("entity_id", &self.model.entity_id) + .finish_non_exhaustive() + } +} + +mod any_view { + use crate::{AnyElement, AnyView, IntoElement, Render, WindowContext}; + + pub(crate) fn render( + view: &AnyView, + cx: &mut WindowContext, + ) -> AnyElement { + let view = view.clone().downcast::().unwrap(); + view.update(cx, |view, cx| view.render(cx).into_any_element()) + } +} + +/// A view that renders nothing +pub struct EmptyView; + +impl Render for EmptyView { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + Empty + } +} diff --git a/crates/ming/src/window.rs b/crates/ming/src/window.rs new file mode 100644 index 0000000..17236c0 --- /dev/null +++ b/crates/ming/src/window.rs @@ -0,0 +1,4620 @@ +use crate::{ + hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip, + AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, + Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId, + DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, + FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding, + KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, + LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent, + MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, + SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, + TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView, + WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams, + WindowTextSystem, SUBPIXEL_VARIANTS, +}; +use anyhow::{anyhow, Context as _, Result}; +use collections::{FxHashMap, FxHashSet}; +use derive_more::{Deref, DerefMut}; +use futures::channel::oneshot; +use futures::{future::Shared, FutureExt}; +#[cfg(target_os = "macos")] +use media::core_video::CVImageBuffer; +use parking_lot::RwLock; +use refineable::Refineable; +use slotmap::SlotMap; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + borrow::{Borrow, BorrowMut, Cow}, + cell::{Cell, RefCell}, + cmp, + fmt::{Debug, Display}, + future::Future, + hash::{Hash, Hasher}, + marker::PhantomData, + mem, + ops::Range, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, Weak, + }, + time::{Duration, Instant}, +}; +use util::post_inc; +use util::{measure, ResultExt}; + +mod prompts; + +pub use prompts::*; + +/// Represents the two different phases when dispatching events. +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] +pub enum DispatchPhase { + /// After the capture phase comes the bubble phase, in which mouse event listeners are + /// invoked front to back and keyboard event listeners are invoked from the focused element + /// to the root of the element tree. This is the phase you'll most commonly want to use when + /// registering event listeners. + #[default] + Bubble, + /// During the initial capture phase, mouse event listeners are invoked back to front, and keyboard + /// listeners are invoked from the root of the tree downward toward the focused element. This phase + /// is used for special purposes such as clearing the "pressed" state for click events. If + /// you stop event propagation during this phase, you need to know what you're doing. Handlers + /// outside of the immediate region may rely on detecting non-local events during this phase. + Capture, +} + +impl DispatchPhase { + /// Returns true if this represents the "bubble" phase. + pub fn bubble(self) -> bool { + self == DispatchPhase::Bubble + } + + /// Returns true if this represents the "capture" phase. + pub fn capture(self) -> bool { + self == DispatchPhase::Capture + } +} + +type AnyObserver = Box bool + 'static>; + +type AnyWindowFocusListener = Box bool + 'static>; + +struct FocusEvent { + previous_focus_path: SmallVec<[FocusId; 8]>, + current_focus_path: SmallVec<[FocusId; 8]>, +} + +slotmap::new_key_type! { + /// A globally unique identifier for a focusable element. + pub struct FocusId; +} + +thread_local! { + /// 8MB wasn't quite enough... + pub(crate) static ELEMENT_ARENA: RefCell = RefCell::new(Arena::new(32 * 1024 * 1024)); +} + +impl FocusId { + /// Obtains whether the element associated with this handle is currently focused. + pub fn is_focused(&self, cx: &WindowContext) -> bool { + cx.window.focus == Some(*self) + } + + /// Obtains whether the element associated with this handle contains the focused + /// element or is itself focused. + pub fn contains_focused(&self, cx: &WindowContext) -> bool { + cx.focused() + .map_or(false, |focused| self.contains(focused.id, cx)) + } + + /// Obtains whether the element associated with this handle is contained within the + /// focused element or is itself focused. + pub fn within_focused(&self, cx: &WindowContext) -> bool { + let focused = cx.focused(); + focused.map_or(false, |focused| focused.id.contains(*self, cx)) + } + + /// Obtains whether this handle contains the given handle in the most recently rendered frame. + pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool { + cx.window + .rendered_frame + .dispatch_tree + .focus_contains(*self, other) + } +} + +/// A handle which can be used to track and manipulate the focused element in a window. +pub struct FocusHandle { + pub(crate) id: FocusId, + handles: Arc>>, +} + +impl std::fmt::Debug for FocusHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("FocusHandle({:?})", self.id)) + } +} + +impl FocusHandle { + pub(crate) fn new(handles: &Arc>>) -> Self { + let id = handles.write().insert(AtomicUsize::new(1)); + Self { + id, + handles: handles.clone(), + } + } + + pub(crate) fn for_id( + id: FocusId, + handles: &Arc>>, + ) -> Option { + let lock = handles.read(); + let ref_count = lock.get(id)?; + if ref_count.load(SeqCst) == 0 { + None + } else { + ref_count.fetch_add(1, SeqCst); + Some(Self { + id, + handles: handles.clone(), + }) + } + } + + /// Converts this focus handle into a weak variant, which does not prevent it from being released. + pub fn downgrade(&self) -> WeakFocusHandle { + WeakFocusHandle { + id: self.id, + handles: Arc::downgrade(&self.handles), + } + } + + /// Moves the focus to the element associated with this handle. + pub fn focus(&self, cx: &mut WindowContext) { + cx.focus(self) + } + + /// Obtains whether the element associated with this handle is currently focused. + pub fn is_focused(&self, cx: &WindowContext) -> bool { + self.id.is_focused(cx) + } + + /// Obtains whether the element associated with this handle contains the focused + /// element or is itself focused. + pub fn contains_focused(&self, cx: &WindowContext) -> bool { + self.id.contains_focused(cx) + } + + /// Obtains whether the element associated with this handle is contained within the + /// focused element or is itself focused. + pub fn within_focused(&self, cx: &WindowContext) -> bool { + self.id.within_focused(cx) + } + + /// Obtains whether this handle contains the given handle in the most recently rendered frame. + pub fn contains(&self, other: &Self, cx: &WindowContext) -> bool { + self.id.contains(other.id, cx) + } +} + +impl Clone for FocusHandle { + fn clone(&self) -> Self { + Self::for_id(self.id, &self.handles).unwrap() + } +} + +impl PartialEq for FocusHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for FocusHandle {} + +impl Drop for FocusHandle { + fn drop(&mut self) { + self.handles + .read() + .get(self.id) + .unwrap() + .fetch_sub(1, SeqCst); + } +} + +/// A weak reference to a focus handle. +#[derive(Clone, Debug)] +pub struct WeakFocusHandle { + pub(crate) id: FocusId, + handles: Weak>>, +} + +impl WeakFocusHandle { + /// Attempts to upgrade the [WeakFocusHandle] to a [FocusHandle]. + pub fn upgrade(&self) -> Option { + let handles = self.handles.upgrade()?; + FocusHandle::for_id(self.id, &handles) + } +} + +impl PartialEq for WeakFocusHandle { + fn eq(&self, other: &WeakFocusHandle) -> bool { + self.id == other.id + } +} + +impl Eq for WeakFocusHandle {} + +impl PartialEq for WeakFocusHandle { + fn eq(&self, other: &FocusHandle) -> bool { + self.id == other.id + } +} + +impl PartialEq for FocusHandle { + fn eq(&self, other: &WeakFocusHandle) -> bool { + self.id == other.id + } +} + +/// FocusableView allows users of your view to easily +/// focus it (using cx.focus_view(view)) +pub trait FocusableView: 'static + Render { + /// Returns the focus handle associated with this view. + fn focus_handle(&self, cx: &AppContext) -> FocusHandle; +} + +/// ManagedView is a view (like a Modal, Popover, Menu, etc.) +/// where the lifecycle of the view is handled by another view. +pub trait ManagedView: FocusableView + EventEmitter {} + +impl> ManagedView for M {} + +/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal. +pub struct DismissEvent; + +type FrameCallback = Box; + +pub(crate) type AnyMouseListener = + Box; + +#[derive(Clone)] +pub(crate) struct CursorStyleRequest { + pub(crate) hitbox_id: HitboxId, + pub(crate) style: CursorStyle, +} + +/// An identifier for a [Hitbox]. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct HitboxId(usize); + +impl HitboxId { + /// Checks if the hitbox with this id is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + cx.window.mouse_hit_test.0.contains(self) + } +} + +/// A rectangular region that potentially blocks hitboxes inserted prior. +/// See [WindowContext::insert_hitbox] for more details. +#[derive(Clone, Debug, Deref)] +pub struct Hitbox { + /// A unique identifier for the hitbox. + pub id: HitboxId, + /// The bounds of the hitbox. + #[deref] + pub bounds: Bounds, + /// The content mask when the hitbox was inserted. + pub content_mask: ContentMask, + /// Whether the hitbox occludes other hitboxes inserted prior. + pub opaque: bool, +} + +impl Hitbox { + /// Checks if the hitbox is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + self.id.is_hovered(cx) + } +} + +#[derive(Default, Eq, PartialEq)] +pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>); + +/// An identifier for a tooltip. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct TooltipId(usize); + +impl TooltipId { + /// Checks if the tooltip is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + cx.window + .tooltip_bounds + .as_ref() + .map_or(false, |tooltip_bounds| { + tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&cx.mouse_position()) + }) + } +} + +pub(crate) struct TooltipBounds { + id: TooltipId, + bounds: Bounds, +} + +#[derive(Clone)] +pub(crate) struct TooltipRequest { + id: TooltipId, + tooltip: AnyTooltip, +} + +pub(crate) struct DeferredDraw { + priority: usize, + parent_node: DispatchNodeId, + element_id_stack: SmallVec<[ElementId; 32]>, + text_style_stack: Vec, + element: Option, + absolute_offset: Point, + prepaint_range: Range, + paint_range: Range, +} + +pub(crate) struct Frame { + pub(crate) focus: Option, + pub(crate) window_active: bool, + pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, + accessed_element_states: Vec<(GlobalElementId, TypeId)>, + pub(crate) mouse_listeners: Vec>, + pub(crate) dispatch_tree: DispatchTree, + pub(crate) scene: Scene, + pub(crate) hitboxes: Vec, + pub(crate) deferred_draws: Vec, + pub(crate) input_handlers: Vec>, + pub(crate) tooltip_requests: Vec>, + pub(crate) cursor_styles: Vec, + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_bounds: FxHashMap>, +} + +#[derive(Clone, Default)] +pub(crate) struct PrepaintStateIndex { + hitboxes_index: usize, + tooltips_index: usize, + deferred_draws_index: usize, + dispatch_tree_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + +#[derive(Clone, Default)] +pub(crate) struct PaintIndex { + scene_index: usize, + mouse_listeners_index: usize, + input_handlers_index: usize, + cursor_styles_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + +impl Frame { + pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { + Frame { + focus: None, + window_active: false, + element_states: FxHashMap::default(), + accessed_element_states: Vec::new(), + mouse_listeners: Vec::new(), + dispatch_tree, + scene: Scene::default(), + hitboxes: Vec::new(), + deferred_draws: Vec::new(), + input_handlers: Vec::new(), + tooltip_requests: Vec::new(), + cursor_styles: Vec::new(), + + #[cfg(any(test, feature = "test-support"))] + debug_bounds: FxHashMap::default(), + } + } + + pub(crate) fn clear(&mut self) { + self.element_states.clear(); + self.accessed_element_states.clear(); + self.mouse_listeners.clear(); + self.dispatch_tree.clear(); + self.scene.clear(); + self.input_handlers.clear(); + self.tooltip_requests.clear(); + self.cursor_styles.clear(); + self.hitboxes.clear(); + self.deferred_draws.clear(); + } + + pub(crate) fn hit_test(&self, position: Point) -> HitTest { + let mut hit_test = HitTest::default(); + for hitbox in self.hitboxes.iter().rev() { + let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); + if bounds.contains(&position) { + hit_test.0.push(hitbox.id); + if hitbox.opaque { + break; + } + } + } + hit_test + } + + pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { + self.focus + .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) + .unwrap_or_default() + } + + pub(crate) fn finish(&mut self, prev_frame: &mut Self) { + for element_state_key in &self.accessed_element_states { + if let Some((element_state_key, element_state)) = + prev_frame.element_states.remove_entry(element_state_key) + { + self.element_states.insert(element_state_key, element_state); + } + } + + self.scene.finish(); + } +} + +// Holds the state for a specific window. +#[doc(hidden)] +pub struct Window { + pub(crate) handle: AnyWindowHandle, + pub(crate) removed: bool, + pub(crate) platform_window: Box, + display_id: DisplayId, + sprite_atlas: Arc, + text_system: Arc, + pub(crate) rem_size: Pixels, + pub(crate) viewport_size: Size, + layout_engine: Option, + pub(crate) root_view: Option, + pub(crate) element_id_stack: SmallVec<[ElementId; 32]>, + pub(crate) text_style_stack: Vec, + pub(crate) element_offset_stack: Vec>, + pub(crate) content_mask_stack: Vec>, + pub(crate) requested_autoscroll: Option>, + pub(crate) rendered_frame: Frame, + pub(crate) next_frame: Frame, + pub(crate) next_hitbox_id: HitboxId, + pub(crate) next_tooltip_id: TooltipId, + pub(crate) tooltip_bounds: Option, + next_frame_callbacks: Rc>>, + pub(crate) dirty_views: FxHashSet, + pub(crate) focus_handles: Arc>>, + focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, + focus_lost_listeners: SubscriberSet<(), AnyObserver>, + default_prevented: bool, + mouse_position: Point, + mouse_hit_test: HitTest, + modifiers: Modifiers, + scale_factor: f32, + bounds_observers: SubscriberSet<(), AnyObserver>, + appearance: WindowAppearance, + appearance_observers: SubscriberSet<(), AnyObserver>, + active: Rc>, + pub(crate) dirty: Rc>, + pub(crate) needs_present: Rc>, + pub(crate) last_input_timestamp: Rc>, + pub(crate) refreshing: bool, + pub(crate) draw_phase: DrawPhase, + activation_observers: SubscriberSet<(), AnyObserver>, + pub(crate) focus: Option, + focus_enabled: bool, + pending_input: Option, + prompt: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DrawPhase { + None, + Prepaint, + Paint, + Focus, +} + +#[derive(Default, Debug)] +struct PendingInput { + keystrokes: SmallVec<[Keystroke; 1]>, + bindings: SmallVec<[KeyBinding; 1]>, + focus: Option, + timer: Option>, +} + +impl PendingInput { + fn input(&self) -> String { + self.keystrokes + .iter() + .flat_map(|k| k.ime_key.clone()) + .collect::>() + .join("") + } + + fn used_by_binding(&self, binding: &KeyBinding) -> bool { + if self.keystrokes.is_empty() { + return true; + } + let keystroke = &self.keystrokes[0]; + for candidate in keystroke.match_candidates() { + if binding.match_keystrokes(&[candidate]) == KeyMatch::Pending { + return true; + } + } + false + } +} + +pub(crate) struct ElementStateBox { + pub(crate) inner: Box, + #[cfg(debug_assertions)] + pub(crate) type_name: &'static str, +} + +fn default_bounds(display_id: Option, cx: &mut AppContext) -> Bounds { + const DEFAULT_WINDOW_SIZE: Size = size(DevicePixels(1024), DevicePixels(700)); + const DEFAULT_WINDOW_OFFSET: Point = point(DevicePixels(0), DevicePixels(35)); + + cx.active_window() + .and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok()) + .map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET)) + .unwrap_or_else(|| { + let display = display_id + .map(|id| cx.find_display(id)) + .unwrap_or_else(|| cx.primary_display()); + + display + .map(|display| { + let center = display.bounds().center(); + let offset = DEFAULT_WINDOW_SIZE / 2; + let origin = point(center.x - offset.width, center.y - offset.height); + Bounds::new(origin, DEFAULT_WINDOW_SIZE) + }) + .unwrap_or_else(|| { + Bounds::new(point(DevicePixels(0), DevicePixels(0)), DEFAULT_WINDOW_SIZE) + }) + }) +} + +impl Window { + pub(crate) fn new( + handle: AnyWindowHandle, + options: WindowOptions, + cx: &mut AppContext, + ) -> Self { + let WindowOptions { + window_bounds, + titlebar, + focus, + show, + kind, + is_movable, + display_id, + window_background, + app_id, + } = options; + + let bounds = window_bounds + .map(|bounds| bounds.get_bounds()) + .unwrap_or_else(|| default_bounds(display_id, cx)); + let mut platform_window = cx.platform.open_window( + handle, + WindowParams { + bounds, + titlebar, + kind, + is_movable, + focus, + show, + display_id, + window_background, + }, + ); + let display_id = platform_window.display().id(); + let sprite_atlas = platform_window.sprite_atlas(); + let mouse_position = platform_window.mouse_position(); + let modifiers = platform_window.modifiers(); + let content_size = platform_window.content_size(); + let scale_factor = platform_window.scale_factor(); + let appearance = platform_window.appearance(); + let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone())); + let dirty = Rc::new(Cell::new(true)); + let active = Rc::new(Cell::new(platform_window.is_active())); + let needs_present = Rc::new(Cell::new(false)); + let next_frame_callbacks: Rc>> = Default::default(); + let last_input_timestamp = Rc::new(Cell::new(Instant::now())); + + if let Some(ref window_open_state) = window_bounds { + match window_open_state { + WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), + WindowBounds::Maximized(_) => platform_window.zoom(), + WindowBounds::Windowed(_) => {} + } + } + + platform_window.on_close(Box::new({ + let mut cx = cx.to_async(); + move || { + let _ = handle.update(&mut cx, |_, cx| cx.remove_window()); + } + })); + platform_window.on_request_frame(Box::new({ + let mut cx = cx.to_async(); + let dirty = dirty.clone(); + let active = active.clone(); + let needs_present = needs_present.clone(); + let next_frame_callbacks = next_frame_callbacks.clone(); + let last_input_timestamp = last_input_timestamp.clone(); + move || { + let next_frame_callbacks = next_frame_callbacks.take(); + if !next_frame_callbacks.is_empty() { + handle + .update(&mut cx, |_, cx| { + for callback in next_frame_callbacks { + callback(cx); + } + }) + .log_err(); + } + + // Keep presenting the current scene for 1 extra second since the + // last input to prevent the display from underclocking the refresh rate. + let needs_present = needs_present.get() + || (active.get() + && last_input_timestamp.get().elapsed() < Duration::from_secs(1)); + + if dirty.get() { + measure("frame duration", || { + handle + .update(&mut cx, |_, cx| { + cx.draw(); + cx.present(); + }) + .log_err(); + }) + } else if needs_present { + handle.update(&mut cx, |_, cx| cx.present()).log_err(); + } + + handle + .update(&mut cx, |_, cx| { + cx.complete_frame(); + }) + .log_err(); + } + })); + platform_window.on_resize(Box::new({ + let mut cx = cx.to_async(); + move |_, _| { + handle + .update(&mut cx, |_, cx| cx.bounds_changed()) + .log_err(); + } + })); + platform_window.on_moved(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, cx| cx.bounds_changed()) + .log_err(); + } + })); + platform_window.on_appearance_changed(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, cx| cx.appearance_changed()) + .log_err(); + } + })); + platform_window.on_active_status_change(Box::new({ + let mut cx = cx.to_async(); + move |active| { + handle + .update(&mut cx, |_, cx| { + cx.window.active.set(active); + cx.window + .activation_observers + .clone() + .retain(&(), |callback| callback(cx)); + cx.refresh(); + }) + .log_err(); + } + })); + + platform_window.on_input({ + let mut cx = cx.to_async(); + Box::new(move |event| { + handle + .update(&mut cx, |_, cx| cx.dispatch_event(event)) + .log_err() + .unwrap_or(DispatchEventResult::default()) + }) + }); + + if let Some(app_id) = app_id { + platform_window.set_app_id(&app_id); + } + + Window { + handle, + removed: false, + platform_window, + display_id, + sprite_atlas, + text_system, + rem_size: px(16.), + viewport_size: content_size, + layout_engine: Some(TaffyLayoutEngine::new()), + root_view: None, + element_id_stack: SmallVec::default(), + text_style_stack: Vec::new(), + element_offset_stack: Vec::new(), + content_mask_stack: Vec::new(), + requested_autoscroll: None, + rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), + next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), + next_frame_callbacks, + next_hitbox_id: HitboxId::default(), + next_tooltip_id: TooltipId::default(), + tooltip_bounds: None, + dirty_views: FxHashSet::default(), + focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), + focus_listeners: SubscriberSet::new(), + focus_lost_listeners: SubscriberSet::new(), + default_prevented: true, + mouse_position, + mouse_hit_test: HitTest::default(), + modifiers, + scale_factor, + bounds_observers: SubscriberSet::new(), + appearance, + appearance_observers: SubscriberSet::new(), + active, + dirty, + needs_present, + last_input_timestamp, + refreshing: false, + draw_phase: DrawPhase::None, + activation_observers: SubscriberSet::new(), + focus: None, + focus_enabled: true, + pending_input: None, + prompt: None, + } + } + fn new_focus_listener( + &mut self, + value: AnyWindowFocusListener, + ) -> (Subscription, impl FnOnce()) { + self.focus_listeners.insert((), value) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct DispatchEventResult { + pub propagate: bool, + pub default_prevented: bool, +} + +/// Indicates which region of the window is visible. Content falling outside of this mask will not be +/// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type +/// to leave room to support more complex shapes in the future. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[repr(C)] +pub struct ContentMask { + /// The bounds + pub bounds: Bounds

, +} + +impl ContentMask { + /// Scale the content mask's pixel units by the given scaling factor. + pub fn scale(&self, factor: f32) -> ContentMask { + ContentMask { + bounds: self.bounds.scale(factor), + } + } + + /// Intersect the content mask with the given content mask. + pub fn intersect(&self, other: &Self) -> Self { + let bounds = self.bounds.intersect(&other.bounds); + ContentMask { bounds } + } +} + +/// Provides access to application state in the context of a single window. Derefs +/// to an [`AppContext`], so you can also pass a [`WindowContext`] to any method that takes +/// an [`AppContext`] and call any [`AppContext`] methods. +pub struct WindowContext<'a> { + pub(crate) app: &'a mut AppContext, + pub(crate) window: &'a mut Window, +} + +impl<'a> WindowContext<'a> { + pub(crate) fn new(app: &'a mut AppContext, window: &'a mut Window) -> Self { + Self { app, window } + } + + /// Obtain a handle to the window that belongs to this context. + pub fn window_handle(&self) -> AnyWindowHandle { + self.window.handle + } + + /// Mark the window as dirty, scheduling it to be redrawn on the next frame. + pub fn refresh(&mut self) { + if self.window.draw_phase == DrawPhase::None { + self.window.refreshing = true; + self.window.dirty.set(true); + } + } + + /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. + /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. + pub fn notify(&mut self, view_id: EntityId) { + for view_id in self + .window + .rendered_frame + .dispatch_tree + .view_path(view_id) + .into_iter() + .rev() + { + if !self.window.dirty_views.insert(view_id) { + break; + } + } + + if self.window.draw_phase == DrawPhase::None { + self.window.dirty.set(true); + self.app.push_effect(Effect::Notify { emitter: view_id }); + } + } + + /// Close this window. + pub fn remove_window(&mut self) { + self.window.removed = true; + } + + /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus + /// for elements rendered within this window. + pub fn focus_handle(&mut self) -> FocusHandle { + FocusHandle::new(&self.window.focus_handles) + } + + /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`. + pub fn focused(&self) -> Option { + self.window + .focus + .and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles)) + } + + /// Move focus to the element associated with the given [`FocusHandle`]. + pub fn focus(&mut self, handle: &FocusHandle) { + if !self.window.focus_enabled || self.window.focus == Some(handle.id) { + return; + } + + self.window.focus = Some(handle.id); + self.window + .rendered_frame + .dispatch_tree + .clear_pending_keystrokes(); + self.refresh(); + } + + /// Remove focus from all elements within this context's window. + pub fn blur(&mut self) { + if !self.window.focus_enabled { + return; + } + + self.window.focus = None; + self.refresh(); + } + + /// Blur the window and don't allow anything in it to be focused again. + pub fn disable_focus(&mut self) { + self.blur(); + self.window.focus_enabled = false; + } + + /// Accessor for the text system. + pub fn text_system(&self) -> &Arc { + &self.window.text_system + } + + /// The current text style. Which is composed of all the style refinements provided to `with_text_style`. + pub fn text_style(&self) -> TextStyle { + let mut style = TextStyle::default(); + for refinement in &self.window.text_style_stack { + style.refine(refinement); + } + style + } + + /// Check if the platform window is maximized + /// On some platforms (namely Windows) this is different than the bounds being the size of the display + pub fn is_maximized(&self) -> bool { + self.window.platform_window.is_maximized() + } + + /// Return the `WindowBounds` to indicate that how a window should be opened + /// after it has been closed + pub fn window_bounds(&self) -> WindowBounds { + self.window.platform_window.window_bounds() + } + + /// Dispatch the given action on the currently focused element. + pub fn dispatch_action(&mut self, action: Box) { + let focus_handle = self.focused(); + + let window = self.window.handle; + self.app.defer(move |cx| { + window + .update(cx, |_, cx| { + let node_id = focus_handle + .and_then(|handle| { + cx.window + .rendered_frame + .dispatch_tree + .focusable_node_id(handle.id) + }) + .unwrap_or_else(|| cx.window.rendered_frame.dispatch_tree.root_node_id()); + + cx.dispatch_action_on_node(node_id, action.as_ref()); + }) + .log_err(); + }) + } + + pub(crate) fn dispatch_keystroke_observers( + &mut self, + event: &dyn Any, + action: Option>, + ) { + let Some(key_down_event) = event.downcast_ref::() else { + return; + }; + + self.keystroke_observers + .clone() + .retain(&(), move |callback| { + (callback)( + &KeystrokeEvent { + keystroke: key_down_event.keystroke.clone(), + action: action.as_ref().map(|action| action.boxed_clone()), + }, + self, + ); + true + }); + } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + self.window + .rendered_frame + .dispatch_tree + .clear_pending_keystrokes(); + self.window + .next_frame + .dispatch_tree + .clear_pending_keystrokes(); + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { + let handle = self.window.handle; + self.app.defer(move |cx| { + handle.update(cx, |_, cx| f(cx)).ok(); + }); + } + + /// Subscribe to events emitted by a model or view. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window. + pub fn subscribe( + &mut self, + entity: &E, + mut on_event: impl FnMut(E, &Evt, &mut WindowContext<'_>) + 'static, + ) -> Subscription + where + Emitter: EventEmitter, + E: Entity, + Evt: 'static, + { + let entity_id = entity.entity_id(); + let entity = entity.downgrade(); + let window_handle = self.window.handle; + self.app.new_subscription( + entity_id, + ( + TypeId::of::(), + Box::new(move |event, cx| { + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&entity) { + let event = event.downcast_ref().expect("invalid event type"); + on_event(handle, event, cx); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ), + ) + } + + /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across + /// await points in async code. + pub fn to_async(&self) -> AsyncWindowContext { + AsyncWindowContext::new(self.app.to_async(), self.window.handle) + } + + /// Schedule the given closure to be run directly after the current frame is rendered. + pub fn on_next_frame(&mut self, callback: impl FnOnce(&mut WindowContext) + 'static) { + RefCell::borrow_mut(&self.window.next_frame_callbacks).push(Box::new(callback)); + } + + /// Spawn the future returned by the given closure on the application thread pool. + /// The closure is provided a handle to the current window and an `AsyncWindowContext` for + /// use within your future. + pub fn spawn(&mut self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task + where + R: 'static, + Fut: Future + 'static, + { + self.app + .spawn(|app| f(AsyncWindowContext::new(app, self.window.handle))) + } + + fn bounds_changed(&mut self) { + self.window.scale_factor = self.window.platform_window.scale_factor(); + self.window.viewport_size = self.window.platform_window.content_size(); + self.window.display_id = self.window.platform_window.display().id(); + self.refresh(); + + self.window + .bounds_observers + .clone() + .retain(&(), |callback| callback(self)); + } + + /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays. + pub fn bounds(&self) -> Bounds { + self.window.platform_window.bounds() + } + + /// Returns whether or not the window is currently fullscreen + pub fn is_fullscreen(&self) -> bool { + self.window.platform_window.is_fullscreen() + } + + fn appearance_changed(&mut self) { + self.window.appearance = self.window.platform_window.appearance(); + + self.window + .appearance_observers + .clone() + .retain(&(), |callback| callback(self)); + } + + /// Returns the appearance of the current window. + pub fn appearance(&self) -> WindowAppearance { + self.window.appearance + } + + /// Returns the size of the drawable area within the window. + pub fn viewport_size(&self) -> Size { + self.window.viewport_size + } + + /// Returns whether this window is focused by the operating system (receiving key events). + pub fn is_window_active(&self) -> bool { + self.window.active.get() + } + + /// Toggle zoom on the window. + pub fn zoom_window(&self) { + self.window.platform_window.zoom(); + } + + /// Updates the window's title at the platform level. + pub fn set_window_title(&mut self, title: &str) { + self.window.platform_window.set_title(title); + } + + /// Sets the application identifier. + pub fn set_app_id(&mut self, app_id: &str) { + self.window.platform_window.set_app_id(app_id); + } + + /// Sets the window background appearance. + pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + self.window + .platform_window + .set_background_appearance(background_appearance); + } + + /// Mark the window as dirty at the platform level. + pub fn set_window_edited(&mut self, edited: bool) { + self.window.platform_window.set_edited(edited); + } + + /// Determine the display on which the window is visible. + pub fn display(&self) -> Option> { + self.platform + .displays() + .into_iter() + .find(|display| display.id() == self.window.display_id) + } + + /// Show the platform character palette. + pub fn show_character_palette(&self) { + self.window.platform_window.show_character_palette(); + } + + /// The scale factor of the display associated with the window. For example, it could + /// return 2.0 for a "retina" display, indicating that each logical pixel should actually + /// be rendered as two pixels on screen. + pub fn scale_factor(&self) -> f32 { + self.window.scale_factor + } + + /// The size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn rem_size(&self) -> Pixels { + self.window.rem_size + } + + /// Sets the size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn set_rem_size(&mut self, rem_size: impl Into) { + self.window.rem_size = rem_size.into(); + } + + /// The line height associated with the current text style. + pub fn line_height(&self) -> Pixels { + let rem_size = self.rem_size(); + let text_style = self.text_style(); + text_style + .line_height + .to_pixels(text_style.font_size, rem_size) + } + + /// Call to prevent the default action of an event. Currently only used to prevent + /// parent elements from becoming focused on mouse down. + pub fn prevent_default(&mut self) { + self.window.default_prevented = true; + } + + /// Obtain whether default has been prevented for the event currently being dispatched. + pub fn default_prevented(&self) -> bool { + self.window.default_prevented + } + + /// Determine whether the given action is available along the dispatch path to the currently focused element. + pub fn is_action_available(&self, action: &dyn Action) -> bool { + let target = self + .focused() + .and_then(|focused_handle| { + self.window + .rendered_frame + .dispatch_tree + .focusable_node_id(focused_handle.id) + }) + .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); + self.window + .rendered_frame + .dispatch_tree + .is_action_available(action, target) + } + + /// The position of the mouse relative to the window. + pub fn mouse_position(&self) -> Point { + self.window.mouse_position + } + + /// The current state of the keyboard's modifiers + pub fn modifiers(&self) -> Modifiers { + self.window.modifiers + } + + fn complete_frame(&self) { + self.window.platform_window.completed_frame(); + } + + /// Produces a new frame and assigns it to `rendered_frame`. To actually show + /// the contents of the new [Scene], use [present]. + #[profiling::function] + pub fn draw(&mut self) { + self.window.dirty.set(false); + self.window.requested_autoscroll = None; + + // Restore the previously-used input handler. + if let Some(input_handler) = self.window.platform_window.take_input_handler() { + self.window + .rendered_frame + .input_handlers + .push(Some(input_handler)); + } + + self.draw_roots(); + self.window.dirty_views.clear(); + + self.window + .next_frame + .dispatch_tree + .preserve_pending_keystrokes( + &mut self.window.rendered_frame.dispatch_tree, + self.window.focus, + ); + self.window.next_frame.focus = self.window.focus; + self.window.next_frame.window_active = self.window.active.get(); + + // Register requested input handler with the platform window. + if let Some(input_handler) = self.window.next_frame.input_handlers.pop() { + self.window + .platform_window + .set_input_handler(input_handler.unwrap()); + } + + self.window.layout_engine.as_mut().unwrap().clear(); + self.text_system().finish_frame(); + self.window + .next_frame + .finish(&mut self.window.rendered_frame); + ELEMENT_ARENA.with_borrow_mut(|element_arena| { + let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.; + if percentage >= 80. { + log::warn!("elevated element arena occupation: {}.", percentage); + } + element_arena.clear(); + }); + + self.window.draw_phase = DrawPhase::Focus; + let previous_focus_path = self.window.rendered_frame.focus_path(); + let previous_window_active = self.window.rendered_frame.window_active; + mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame); + self.window.next_frame.clear(); + let current_focus_path = self.window.rendered_frame.focus_path(); + let current_window_active = self.window.rendered_frame.window_active; + + if previous_focus_path != current_focus_path + || previous_window_active != current_window_active + { + if !previous_focus_path.is_empty() && current_focus_path.is_empty() { + self.window + .focus_lost_listeners + .clone() + .retain(&(), |listener| listener(self)); + } + + let event = FocusEvent { + previous_focus_path: if previous_window_active { + previous_focus_path + } else { + Default::default() + }, + current_focus_path: if current_window_active { + current_focus_path + } else { + Default::default() + }, + }; + self.window + .focus_listeners + .clone() + .retain(&(), |listener| listener(&event, self)); + } + + self.reset_cursor_style(); + self.window.refreshing = false; + self.window.draw_phase = DrawPhase::None; + self.window.needs_present.set(true); + } + + #[profiling::function] + fn present(&self) { + self.window + .platform_window + .draw(&self.window.rendered_frame.scene); + self.window.needs_present.set(false); + profiling::finish_frame!(); + } + + fn draw_roots(&mut self) { + self.window.draw_phase = DrawPhase::Prepaint; + self.window.tooltip_bounds.take(); + + // Layout all root elements. + let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any(); + root_element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); + + let mut sorted_deferred_draws = + (0..self.window.next_frame.deferred_draws.len()).collect::>(); + sorted_deferred_draws.sort_by_key(|ix| self.window.next_frame.deferred_draws[*ix].priority); + self.prepaint_deferred_draws(&sorted_deferred_draws); + + let mut prompt_element = None; + let mut active_drag_element = None; + let mut tooltip_element = None; + if let Some(prompt) = self.window.prompt.take() { + let mut element = prompt.view.any_view().into_any(); + element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); + prompt_element = Some(element); + self.window.prompt = Some(prompt); + } else if let Some(active_drag) = self.app.active_drag.take() { + let mut element = active_drag.view.clone().into_any(); + let offset = self.mouse_position() - active_drag.cursor_offset; + element.prepaint_as_root(offset, AvailableSpace::min_size(), self); + active_drag_element = Some(element); + self.app.active_drag = Some(active_drag); + } else { + tooltip_element = self.prepaint_tooltip(); + } + + self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position); + + // Now actually paint the elements. + self.window.draw_phase = DrawPhase::Paint; + root_element.paint(self); + + self.paint_deferred_draws(&sorted_deferred_draws); + + if let Some(mut prompt_element) = prompt_element { + prompt_element.paint(self) + } else if let Some(mut drag_element) = active_drag_element { + drag_element.paint(self); + } else if let Some(mut tooltip_element) = tooltip_element { + tooltip_element.paint(self); + } + } + + fn prepaint_tooltip(&mut self) -> Option { + let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?; + let tooltip_request = tooltip_request.unwrap(); + let mut element = tooltip_request.tooltip.view.clone().into_any(); + let mouse_position = tooltip_request.tooltip.mouse_position; + let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self); + + let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); + let window_bounds = Bounds { + origin: Point::default(), + size: self.viewport_size(), + }; + + if tooltip_bounds.right() > window_bounds.right() { + let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); + if new_x >= Pixels::ZERO { + tooltip_bounds.origin.x = new_x; + } else { + tooltip_bounds.origin.x = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), + ); + } + } + + if tooltip_bounds.bottom() > window_bounds.bottom() { + let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); + if new_y >= Pixels::ZERO { + tooltip_bounds.origin.y = new_y; + } else { + tooltip_bounds.origin.y = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), + ); + } + } + + self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx)); + + self.window.tooltip_bounds = Some(TooltipBounds { + id: tooltip_request.id, + bounds: tooltip_bounds, + }); + Some(element) + } + + fn prepaint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { + assert_eq!(self.window.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.window + .element_id_stack + .clone_from(&deferred_draw.element_id_stack); + self.window + .text_style_stack + .clone_from(&deferred_draw.text_style_stack); + self.window + .next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let prepaint_start = self.prepaint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + self.with_absolute_element_offset(deferred_draw.absolute_offset, |cx| { + element.prepaint(cx) + }); + } else { + self.reuse_prepaint(deferred_draw.prepaint_range.clone()); + } + let prepaint_end = self.prepaint_index(); + deferred_draw.prepaint_range = prepaint_start..prepaint_end; + } + assert_eq!( + self.window.next_frame.deferred_draws.len(), + 0, + "cannot call defer_draw during deferred drawing" + ); + self.window.next_frame.deferred_draws = deferred_draws; + self.window.element_id_stack.clear(); + self.window.text_style_stack.clear(); + } + + fn paint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { + assert_eq!(self.window.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let mut deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.window + .element_id_stack + .clone_from(&deferred_draw.element_id_stack); + self.window + .next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let paint_start = self.paint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + element.paint(self); + } else { + self.reuse_paint(deferred_draw.paint_range.clone()); + } + let paint_end = self.paint_index(); + deferred_draw.paint_range = paint_start..paint_end; + } + self.window.next_frame.deferred_draws = deferred_draws; + self.window.element_id_stack.clear(); + } + + pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { + PrepaintStateIndex { + hitboxes_index: self.window.next_frame.hitboxes.len(), + tooltips_index: self.window.next_frame.tooltip_requests.len(), + deferred_draws_index: self.window.next_frame.deferred_draws.len(), + dispatch_tree_index: self.window.next_frame.dispatch_tree.len(), + accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), + line_layout_index: self.window.text_system.layout_index(), + } + } + + pub(crate) fn reuse_prepaint(&mut self, range: Range) { + let window = &mut self.window; + window.next_frame.hitboxes.extend( + window.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] + .iter() + .cloned(), + ); + window.next_frame.tooltip_requests.extend( + window.rendered_frame.tooltip_requests + [range.start.tooltips_index..range.end.tooltips_index] + .iter_mut() + .map(|request| request.take()), + ); + window.next_frame.accessed_element_states.extend( + window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + ); + window + .text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + + let reused_subtree = window.next_frame.dispatch_tree.reuse_subtree( + range.start.dispatch_tree_index..range.end.dispatch_tree_index, + &mut window.rendered_frame.dispatch_tree, + ); + window.next_frame.deferred_draws.extend( + window.rendered_frame.deferred_draws + [range.start.deferred_draws_index..range.end.deferred_draws_index] + .iter() + .map(|deferred_draw| DeferredDraw { + parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), + element_id_stack: deferred_draw.element_id_stack.clone(), + text_style_stack: deferred_draw.text_style_stack.clone(), + priority: deferred_draw.priority, + element: None, + absolute_offset: deferred_draw.absolute_offset, + prepaint_range: deferred_draw.prepaint_range.clone(), + paint_range: deferred_draw.paint_range.clone(), + }), + ); + } + + pub(crate) fn paint_index(&self) -> PaintIndex { + PaintIndex { + scene_index: self.window.next_frame.scene.len(), + mouse_listeners_index: self.window.next_frame.mouse_listeners.len(), + input_handlers_index: self.window.next_frame.input_handlers.len(), + cursor_styles_index: self.window.next_frame.cursor_styles.len(), + accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), + line_layout_index: self.window.text_system.layout_index(), + } + } + + pub(crate) fn reuse_paint(&mut self, range: Range) { + let window = &mut self.window; + + window.next_frame.cursor_styles.extend( + window.rendered_frame.cursor_styles + [range.start.cursor_styles_index..range.end.cursor_styles_index] + .iter() + .cloned(), + ); + window.next_frame.input_handlers.extend( + window.rendered_frame.input_handlers + [range.start.input_handlers_index..range.end.input_handlers_index] + .iter_mut() + .map(|handler| handler.take()), + ); + window.next_frame.mouse_listeners.extend( + window.rendered_frame.mouse_listeners + [range.start.mouse_listeners_index..range.end.mouse_listeners_index] + .iter_mut() + .map(|listener| listener.take()), + ); + window.next_frame.accessed_element_states.extend( + window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + ); + window + .text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + window.next_frame.scene.replay( + range.start.scene_index..range.end.scene_index, + &window.rendered_frame.scene, + ); + } + + /// Push a text style onto the stack, and call a function with that style active. + /// Use [`AppContext::text_style`] to get the current, combined text style. This method + /// should only be called as part of element drawing. + pub fn with_text_style(&mut self, style: Option, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + if let Some(style) = style { + self.window.text_style_stack.push(style); + let result = f(self); + self.window.text_style_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the cursor style at the platform level. This method should only be called + /// during the prepaint phase of element drawing. + pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + self.window + .next_frame + .cursor_styles + .push(CursorStyleRequest { + hitbox_id: hitbox.id, + style, + }); + } + + /// Sets a tooltip to be rendered for the upcoming frame. This method should only be called + /// during the paint phase of element drawing. + pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + let id = TooltipId(post_inc(&mut self.window.next_tooltip_id.0)); + self.window + .next_frame + .tooltip_requests + .push(Some(TooltipRequest { id, tooltip })); + id + } + + /// Invoke the given function with the given content mask after intersecting it + /// with the current mask. This method should only be called during element drawing. + pub fn with_content_mask( + &mut self, + mask: Option>, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + if let Some(mask) = mask { + let mask = mask.intersect(&self.content_mask()); + self.window_mut().content_mask_stack.push(mask); + let result = f(self); + self.window_mut().content_mask_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the global element offset relative to the current offset. This is used to implement + /// scrolling. This method should only be called during the prepaint phase of element drawing. + pub fn with_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + if offset.is_zero() { + return f(self); + }; + + let abs_offset = self.element_offset() + offset; + self.with_absolute_element_offset(abs_offset, f) + } + + /// Updates the global element offset based on the given offset. This is used to implement + /// drag handles and other manual painting of elements. This method should only be called during + /// the prepaint phase of element drawing. + pub fn with_absolute_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + self.window_mut().element_offset_stack.push(offset); + let result = f(self); + self.window_mut().element_offset_stack.pop(); + result + } + + /// Perform prepaint on child elements in a "retryable" manner, so that any side effects + /// of prepaints can be discarded before prepainting again. This is used to support autoscroll + /// where we need to prepaint children to detect the autoscroll bounds, then adjust the + /// element offset and prepaint again. See [`List`] for an example. This method should only be + /// called during the prepaint phase of element drawing. + pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + let index = self.prepaint_index(); + let result = f(self); + if result.is_err() { + self.window + .next_frame + .hitboxes + .truncate(index.hitboxes_index); + self.window + .next_frame + .tooltip_requests + .truncate(index.tooltips_index); + self.window + .next_frame + .deferred_draws + .truncate(index.deferred_draws_index); + self.window + .next_frame + .dispatch_tree + .truncate(index.dispatch_tree_index); + self.window + .next_frame + .accessed_element_states + .truncate(index.accessed_element_states_index); + self.window + .text_system + .truncate_layouts(index.line_layout_index); + } + result + } + + /// When you call this method during [`prepaint`], containing elements will attempt to + /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call + /// [`prepaint`] again with a new set of bounds. See [`List`] for an example of an element + /// that supports this method being called on the elements it contains. This method should only be + /// called during the prepaint phase of element drawing. + pub fn request_autoscroll(&mut self, bounds: Bounds) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window.requested_autoscroll = Some(bounds); + } + + /// This method can be called from a containing element such as [`List`] to support the autoscroll behavior + /// described in [`request_autoscroll`]. + pub fn take_autoscroll(&mut self) -> Option> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window.requested_autoscroll.take() + } + + /// Remove an asset from GPUI's cache + pub fn remove_cached_asset( + &mut self, + source: &A::Source, + ) -> Option { + self.asset_cache.remove::(source) + } + + /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. + /// Your view will be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call. + /// The results of that call will be cached, and returned on subsequent uses of this API. + /// + /// Use [Self::remove_cached_asset] to reload your asset. + pub fn use_cached_asset( + &mut self, + source: &A::Source, + ) -> Option { + self.asset_cache.get::(source).or_else(|| { + if let Some(asset) = self.use_asset::(source) { + self.asset_cache + .insert::(source.to_owned(), asset.clone()); + Some(asset) + } else { + None + } + }) + } + + /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. + /// Your view will be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time. + /// + /// This asset will not be cached by default, see [Self::use_cached_asset] + pub fn use_asset(&mut self, source: &A::Source) -> Option { + let asset_id = (TypeId::of::(), hash(source)); + let mut is_first = false; + let task = self + .loading_assets + .remove(&asset_id) + .map(|boxed_task| *boxed_task.downcast::>>().unwrap()) + .unwrap_or_else(|| { + is_first = true; + let future = A::load(source.clone(), self); + let task = self.background_executor().spawn(future).shared(); + task + }); + + task.clone().now_or_never().or_else(|| { + if is_first { + let parent_id = self.parent_view_id(); + self.spawn({ + let task = task.clone(); + |mut cx| async move { + task.await; + + cx.on_next_frame(move |cx| { + if let Some(parent_id) = parent_id { + cx.notify(parent_id) + } else { + cx.refresh() + } + }); + } + }) + .detach(); + } + + self.loading_assets.insert(asset_id, Box::new(task)); + + None + }) + } + + /// Obtain the current element offset. This method should only be called during the + /// prepaint phase of element drawing. + pub fn element_offset(&self) -> Point { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window() + .element_offset_stack + .last() + .copied() + .unwrap_or_default() + } + + /// Obtain the current content mask. This method should only be called during element drawing. + pub fn content_mask(&self) -> ContentMask { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during prepaint, or paint" + ); + self.window() + .content_mask_stack + .last() + .cloned() + .unwrap_or_else(|| ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.window().viewport_size, + }, + }) + } + + /// Provide elements in the called function with a new namespace in which their identiers must be unique. + /// This can be used within a custom element to distinguish multiple sets of child elements. + pub fn with_element_namespace( + &mut self, + element_id: impl Into, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.window.element_id_stack.push(element_id.into()); + let result = f(self); + self.window.element_id_stack.pop(); + result + } + + /// Updates or initializes state for an element with the given id that lives across multiple + /// frames. If an element with this ID existed in the rendered frame, its state will be passed + /// to the given closure. The state returned by the closure will be stored so it can be referenced + /// when drawing the next frame. This method should only be called as part of element drawing. + pub fn with_element_state( + &mut self, + global_id: &GlobalElementId, + f: impl FnOnce(Option, &mut Self) -> (R, S), + ) -> R + where + S: 'static, + { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + + let key = (GlobalElementId(global_id.0.clone()), TypeId::of::()); + self.window + .next_frame + .accessed_element_states + .push((GlobalElementId(key.0.clone()), TypeId::of::())); + + if let Some(any) = self + .window + .next_frame + .element_states + .remove(&key) + .or_else(|| self.window.rendered_frame.element_states.remove(&key)) + { + let ElementStateBox { + inner, + #[cfg(debug_assertions)] + type_name, + } = any; + // Using the extra inner option to avoid needing to reallocate a new box. + let mut state_box = inner + .downcast::>() + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow::anyhow!( + "invalid element state type for id, requested {:?}, actual: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow::anyhow!( + "invalid element state type for id, requested {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + let state = state_box.take().expect( + "reentrant call to with_element_state for the same state type and element id", + ); + let (result, state) = f(Some(state), self); + state_box.replace(state); + self.window.next_frame.element_states.insert( + key, + ElementStateBox { + inner: state_box, + #[cfg(debug_assertions)] + type_name, + }, + ); + result + } else { + let (result, state) = f(None, self); + self.window.next_frame.element_states.insert( + key, + ElementStateBox { + inner: Box::new(Some(state)), + #[cfg(debug_assertions)] + type_name: std::any::type_name::(), + }, + ); + result + } + } + + /// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience + /// method for elements where the element id may or may not be assigned. Prefer using `with_element_state` + /// when the element is guaranteed to have an id. + pub fn with_optional_element_state( + &mut self, + global_id: Option<&GlobalElementId>, + f: impl FnOnce(Option>, &mut Self) -> (R, Option), + ) -> R + where + S: 'static, + { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + + if let Some(global_id) = global_id { + self.with_element_state(global_id, |state, cx| { + let (result, state) = f(Some(state), cx); + let state = + state.expect("you must return some state when you pass some element id"); + (result, state) + }) + } else { + let (result, state) = f(None, self); + debug_assert!( + state.is_none(), + "you must not return an element state when passing None for the global id" + ); + result + } + } + + /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree + /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn defer_draw( + &mut self, + element: AnyElement, + absolute_offset: Point, + priority: usize, + ) { + let window = &mut self.window; + debug_assert_eq!( + window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout or prepaint" + ); + let parent_node = window.next_frame.dispatch_tree.active_node_id().unwrap(); + window.next_frame.deferred_draws.push(DeferredDraw { + parent_node, + element_id_stack: window.element_id_stack.clone(), + text_style_stack: window.text_style_stack.clone(), + priority, + element: Some(element), + absolute_offset, + prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), + paint_range: PaintIndex::default()..PaintIndex::default(), + }); + } + + /// Creates a new painting layer for the specified bounds. A "layer" is a batch + /// of geometry that are non-overlapping and have the same draw order. This is typically used + /// for performance reasons. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let clipped_bounds = bounds.intersect(&content_mask.bounds); + if !clipped_bounds.is_empty() { + self.window + .next_frame + .scene + .push_layer(clipped_bounds.scale(scale_factor)); + } + + let result = f(self); + + if !clipped_bounds.is_empty() { + self.window.next_frame.scene.pop_layer(); + } + + result + } + + /// Paint one or more drop shadows into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_shadows( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + for shadow in shadows { + let mut shadow_bounds = bounds; + shadow_bounds.origin += shadow.offset; + shadow_bounds.dilate(shadow.spread_radius); + self.window.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: shadow_bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color, + }); + } + } + + /// Paint one or more quads into the scene for the next frame at the current stacking context. + /// Quads are colored rectangular regions with an optional background, border, and corner radius. + /// see [`fill`](crate::fill), [`outline`](crate::outline), and [`quad`](crate::quad) to construct this type. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_quad(&mut self, quad: PaintQuad) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + self.window.next_frame.scene.insert_primitive(Quad { + order: 0, + pad: 0, + bounds: quad.bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + background: quad.background, + border_color: quad.border_color, + corner_radii: quad.corner_radii.scale(scale_factor), + border_widths: quad.border_widths.scale(scale_factor), + }); + } + + /// Paint the given `Path` into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_path(&mut self, mut path: Path, color: impl Into) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + path.content_mask = content_mask; + path.color = color.into(); + self.window + .next_frame + .scene + .insert_primitive(path.scale(scale_factor)); + } + + /// Paint an underline into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_underline( + &mut self, + origin: Point, + width: Pixels, + style: &UnderlineStyle, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let height = if style.wavy { + style.thickness * 3. + } else { + style.thickness + }; + let bounds = Bounds { + origin, + size: size(width, height), + }; + let content_mask = self.content_mask(); + + self.window.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + color: style.color.unwrap_or_default(), + thickness: style.thickness.scale(scale_factor), + wavy: style.wavy, + }); + } + + /// Paint a strikethrough into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_strikethrough( + &mut self, + origin: Point, + width: Pixels, + style: &StrikethroughStyle, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let height = style.thickness; + let bounds = Bounds { + origin, + size: size(width, height), + }; + let content_mask = self.content_mask(); + + self.window.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + thickness: style.thickness.scale(scale_factor), + color: style.color.unwrap_or_default(), + wavy: false, + }); + } + + /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single glyph that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_glyph( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + color: Hsla, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let subpixel_variant = Point { + x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, + y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, + }; + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + subpixel_variant, + scale_factor, + is_emoji: false, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok((size, Cow::Owned(bytes))) + })?; + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + self.window + .next_frame + .scene + .insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds, + content_mask, + color, + tile, + transformation: TransformationMatrix::unit(), + }); + } + Ok(()) + } + + /// Paints an emoji glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single emoji that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_emoji( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + // We don't render emojis with subpixel variants. + subpixel_variant: Default::default(), + scale_factor, + is_emoji: true, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok((size, Cow::Owned(bytes))) + })?; + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + + self.window + .next_frame + .scene + .insert_primitive(PolychromeSprite { + order: 0, + grayscale: false, + bounds, + corner_radii: Default::default(), + content_mask, + tile, + }); + } + Ok(()) + } + + /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_svg( + &mut self, + bounds: Bounds, + path: SharedString, + transformation: TransformationMatrix, + color: Hsla, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + // Render the SVG at twice the size to get a higher quality result. + let params = RenderSvgParams { + path, + size: bounds + .size + .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)), + }; + + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let bytes = self.svg_renderer.render(¶ms)?; + Ok((params.size, Cow::Owned(bytes))) + })?; + let content_mask = self.content_mask().scale(scale_factor); + + self.window + .next_frame + .scene + .insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds, + content_mask, + color, + tile, + transformation, + }); + + Ok(()) + } + + /// Paint an image into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_image( + &mut self, + bounds: Bounds, + corner_radii: Corners, + data: Arc, + grayscale: bool, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let params = RenderImageParams { image_id: data.id }; + + let tile = self + .window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + Ok((data.size(), Cow::Borrowed(data.as_bytes()))) + })?; + let content_mask = self.content_mask().scale(scale_factor); + let corner_radii = corner_radii.scale(scale_factor); + + self.window + .next_frame + .scene + .insert_primitive(PolychromeSprite { + order: 0, + grayscale, + bounds, + content_mask, + corner_radii, + tile, + }); + Ok(()) + } + + /// Paint a surface into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + #[cfg(target_os = "macos")] + pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + self.window + .next_frame + .scene + .insert_primitive(crate::Surface { + order: 0, + bounds, + content_mask, + image_buffer, + }); + } + + #[must_use] + /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which + /// layout is being requested, along with the layout ids of any children. This method is called during + /// calls to the [`Element::request_layout`] trait method and enables any element to participate in layout. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + pub fn request_layout( + &mut self, + style: Style, + children: impl IntoIterator, + ) -> LayoutId { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + self.app.layout_id_buffer.clear(); + self.app.layout_id_buffer.extend(children); + let rem_size = self.rem_size(); + + self.window.layout_engine.as_mut().unwrap().request_layout( + style, + rem_size, + &self.app.layout_id_buffer, + ) + } + + /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, + /// this variant takes a function that is invoked during layout so you can use arbitrary logic to + /// determine the element's size. One place this is used internally is when measuring text. + /// + /// The given closure is invoked at layout time with the known dimensions and available space and + /// returns a `Size`. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + pub fn request_measured_layout< + F: FnMut(Size>, Size, &mut WindowContext) -> Size + + 'static, + >( + &mut self, + style: Style, + measure: F, + ) -> LayoutId { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + let rem_size = self.rem_size(); + self.window + .layout_engine + .as_mut() + .unwrap() + .request_measured_layout(style, rem_size, measure) + } + + /// Compute the layout for the given id within the given available space. + /// This method is called for its side effect, typically by the framework prior to painting. + /// After calling it, you can request the bounds of the given layout node id or any descendant. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + let mut layout_engine = self.window.layout_engine.take().unwrap(); + layout_engine.compute_layout(layout_id, available_space, self); + self.window.layout_engine = Some(layout_engine); + } + + /// Obtain the bounds computed for the given LayoutId relative to the window. This method will usually be invoked by + /// GPUI itself automatically in order to pass your element its `Bounds` automatically. + /// + /// This method should only be called as part of element drawing. + pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, prepaint, or paint" + ); + + let mut bounds = self + .window + .layout_engine + .as_mut() + .unwrap() + .layout_bounds(layout_id) + .map(Into::into); + bounds.origin += self.element_offset(); + bounds + } + + /// This method should be called during `prepaint`. You can use + /// the returned [Hitbox] during `paint` or in an event handler + /// to determine whether the inserted hitbox was the topmost. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn insert_hitbox(&mut self, bounds: Bounds, opaque: bool) -> Hitbox { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + + let content_mask = self.content_mask(); + let window = &mut self.window; + let id = window.next_hitbox_id; + window.next_hitbox_id.0 += 1; + let hitbox = Hitbox { + id, + bounds, + content_mask, + opaque, + }; + window.next_frame.hitboxes.push(hitbox.clone()); + hitbox + } + + /// Sets the key context for the current element. This context will be used to translate + /// keybindings into actions. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn set_key_context(&mut self, context: KeyContext) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + self.window + .next_frame + .dispatch_tree + .set_key_context(context); + } + + /// Sets the focus handle for the current element. This handle will be used to manage focus state + /// and keyboard event dispatch for the element. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + self.window + .next_frame + .dispatch_tree + .set_focus_id(focus_handle.id); + } + + /// Sets the view id for the current element, which will be used to manage view caching. + /// + /// This method should only be called as part of element prepaint. We plan on removing this + /// method eventually when we solve some issues that require us to construct editor elements + /// directly instead of always using editors via views. + pub fn set_view_id(&mut self, view_id: EntityId) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window.next_frame.dispatch_tree.set_view_id(view_id); + } + + /// Get the last view id for the current element + pub fn parent_view_id(&mut self) -> Option { + self.window.next_frame.dispatch_tree.parent_view_id() + } + + /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the + /// platform to receive textual input with proper integration with concerns such + /// as IME interactions. This handler will be active for the upcoming frame until the following frame is + /// rendered. + /// + /// This method should only be called as part of the paint phase of element drawing. + /// + /// [element_input_handler]: crate::ElementInputHandler + pub fn handle_input(&mut self, focus_handle: &FocusHandle, input_handler: impl InputHandler) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + if focus_handle.is_focused(self) { + let cx = self.to_async(); + self.window + .next_frame + .input_handlers + .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); + } + } + + /// Register a mouse event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_mouse_event( + &mut self, + mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + self.window.next_frame.mouse_listeners.push(Some(Box::new( + move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_>| { + if let Some(event) = event.downcast_ref() { + handler(event, phase, cx) + } + }, + ))); + } + + /// Register a key event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_key_event( + &mut self, + listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + self.window.next_frame.dispatch_tree.on_key_event(Rc::new( + move |event: &dyn Any, phase, cx: &mut WindowContext<'_>| { + if let Some(event) = event.downcast_ref::() { + listener(event, phase, cx) + } + }, + )); + } + + /// Register a modifiers changed event listener on the window for the next frame. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_modifiers_changed( + &mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + self.window + .next_frame + .dispatch_tree + .on_modifiers_changed(Rc::new( + move |event: &ModifiersChangedEvent, cx: &mut WindowContext<'_>| { + listener(event, cx) + }, + )); + } + + fn reset_cursor_style(&self) { + // Set the cursor only if we're the active window. + if self.is_window_active() { + let style = self + .window + .rendered_frame + .cursor_styles + .iter() + .rev() + .find(|request| request.hitbox_id.is_hovered(self)) + .map(|request| request.style) + .unwrap_or(CursorStyle::Arrow); + self.platform.set_cursor_style(style); + } + } + + /// Dispatch a given keystroke as though the user had typed it. + /// You can create a keystroke with Keystroke::parse(""). + pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool { + let keystroke = keystroke.with_simulated_ime(); + let result = self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + })); + if !result.propagate { + return true; + } + + if let Some(input) = keystroke.ime_key { + if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { + input_handler.dispatch_input(&input, self); + self.window.platform_window.set_input_handler(input_handler); + return true; + } + } + + false + } + + /// Represent this action as a key binding string, to display in the UI. + pub fn keystroke_text_for(&self, action: &dyn Action) -> String { + self.bindings_for_action(action) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| action.name().to_string()) + } + + /// Dispatch a mouse or keyboard event on the window. + #[profiling::function] + pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult { + self.window.last_input_timestamp.set(Instant::now()); + // Handlers may set this to false by calling `stop_propagation`. + self.app.propagate_event = true; + // Handlers may set this to true by calling `prevent_default`. + self.window.default_prevented = false; + + let event = match event { + // Track the mouse position with our own state, since accessing the platform + // API for the mouse position can only occur on the main thread. + PlatformInput::MouseMove(mouse_move) => { + self.window.mouse_position = mouse_move.position; + self.window.modifiers = mouse_move.modifiers; + PlatformInput::MouseMove(mouse_move) + } + PlatformInput::MouseDown(mouse_down) => { + self.window.mouse_position = mouse_down.position; + self.window.modifiers = mouse_down.modifiers; + PlatformInput::MouseDown(mouse_down) + } + PlatformInput::MouseUp(mouse_up) => { + self.window.mouse_position = mouse_up.position; + self.window.modifiers = mouse_up.modifiers; + PlatformInput::MouseUp(mouse_up) + } + PlatformInput::MouseExited(mouse_exited) => { + self.window.modifiers = mouse_exited.modifiers; + PlatformInput::MouseExited(mouse_exited) + } + PlatformInput::ModifiersChanged(modifiers_changed) => { + self.window.modifiers = modifiers_changed.modifiers; + PlatformInput::ModifiersChanged(modifiers_changed) + } + PlatformInput::ScrollWheel(scroll_wheel) => { + self.window.mouse_position = scroll_wheel.position; + self.window.modifiers = scroll_wheel.modifiers; + PlatformInput::ScrollWheel(scroll_wheel) + } + // Translate dragging and dropping of external files from the operating system + // to internal drag and drop events. + PlatformInput::FileDrop(file_drop) => match file_drop { + FileDropEvent::Entered { position, paths } => { + self.window.mouse_position = position; + if self.active_drag.is_none() { + self.active_drag = Some(AnyDrag { + value: Box::new(paths.clone()), + view: self.new_view(|_| paths).into(), + cursor_offset: position, + }); + } + PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::default(), + }) + } + FileDropEvent::Pending { position } => { + self.window.mouse_position = position; + PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::default(), + }) + } + FileDropEvent::Submit { position } => { + self.activate(true); + self.window.mouse_position = position; + PlatformInput::MouseUp(MouseUpEvent { + button: MouseButton::Left, + position, + modifiers: Modifiers::default(), + click_count: 1, + }) + } + FileDropEvent::Exited => { + self.active_drag.take(); + PlatformInput::FileDrop(FileDropEvent::Exited) + } + }, + PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event, + }; + + if let Some(any_mouse_event) = event.mouse_event() { + self.dispatch_mouse_event(any_mouse_event); + } else if let Some(any_key_event) = event.keyboard_event() { + self.dispatch_key_event(any_key_event); + } + + DispatchEventResult { + propagate: self.app.propagate_event, + default_prevented: self.window.default_prevented, + } + } + + fn dispatch_mouse_event(&mut self, event: &dyn Any) { + let hit_test = self.window.rendered_frame.hit_test(self.mouse_position()); + if hit_test != self.window.mouse_hit_test { + self.window.mouse_hit_test = hit_test; + self.reset_cursor_style(); + } + + let mut mouse_listeners = mem::take(&mut self.window.rendered_frame.mouse_listeners); + + // Capture phase, events bubble from back to front. Handlers for this phase are used for + // special purposes, such as detecting events outside of a given Bounds. + for listener in &mut mouse_listeners { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Capture, self); + if !self.app.propagate_event { + break; + } + } + + // Bubble phase, where most normal handlers do their work. + if self.app.propagate_event { + for listener in mouse_listeners.iter_mut().rev() { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Bubble, self); + if !self.app.propagate_event { + break; + } + } + } + + self.window.rendered_frame.mouse_listeners = mouse_listeners; + + if self.has_active_drag() { + if event.is::() { + // If this was a mouse move event, redraw the window so that the + // active drag can follow the mouse cursor. + self.refresh(); + } else if event.is::() { + // If this was a mouse up event, cancel the active drag and redraw + // the window. + self.active_drag = None; + self.refresh(); + } + } + } + + fn dispatch_key_event(&mut self, event: &dyn Any) { + if self.window.dirty.get() { + self.draw(); + } + + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .rendered_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); + + let dispatch_path = self + .window + .rendered_frame + .dispatch_tree + .dispatch_path(node_id); + + if let Some(key_down_event) = event.downcast_ref::() { + let KeymatchResult { bindings, pending } = self + .window + .rendered_frame + .dispatch_tree + .dispatch_key(&key_down_event.keystroke, &dispatch_path); + + if pending { + let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); + if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus + { + currently_pending = PendingInput::default(); + } + currently_pending.focus = self.window.focus; + currently_pending + .keystrokes + .push(key_down_event.keystroke.clone()); + for binding in bindings { + currently_pending.bindings.push(binding); + } + + currently_pending.timer = Some(self.spawn(|mut cx| async move { + cx.background_executor.timer(Duration::from_secs(1)).await; + cx.update(move |cx| { + cx.clear_pending_keystrokes(); + let Some(currently_pending) = cx.window.pending_input.take() else { + return; + }; + cx.replay_pending_input(currently_pending) + }) + .log_err(); + })); + + self.window.pending_input = Some(currently_pending); + + self.propagate_event = false; + return; + } else if let Some(currently_pending) = self.window.pending_input.take() { + if bindings + .iter() + .all(|binding| !currently_pending.used_by_binding(binding)) + { + self.replay_pending_input(currently_pending) + } + } + + if !bindings.is_empty() { + self.clear_pending_keystrokes(); + } + + self.propagate_event = true; + for binding in bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref()); + if !self.propagate_event { + self.dispatch_keystroke_observers(event, Some(binding.action)); + return; + } + } + } + + self.dispatch_key_down_up_event(event, &dispatch_path); + if !self.propagate_event { + return; + } + + self.dispatch_modifiers_changed_event(event, &dispatch_path); + if !self.propagate_event { + return; + } + + self.dispatch_keystroke_observers(event, None); + } + + fn dispatch_key_down_up_event( + &mut self, + event: &dyn Any, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) { + // Capture phase + for node_id in dispatch_path { + let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self); + if !self.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; + } + } + } + } + + fn dispatch_modifiers_changed_event( + &mut self, + event: &dyn Any, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) { + let Some(event) = event.downcast_ref::() else { + return; + }; + for node_id in dispatch_path.iter().rev() { + let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + for listener in node.modifiers_changed_listeners.clone() { + listener(event, self); + if !self.propagate_event { + return; + } + } + } + } + + /// Determine whether a potential multi-stroke key binding is in progress on this window. + pub fn has_pending_keystrokes(&self) -> bool { + self.window + .rendered_frame + .dispatch_tree + .has_pending_keystrokes() + } + + fn replay_pending_input(&mut self, currently_pending: PendingInput) { + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .rendered_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); + + if self.window.focus != currently_pending.focus { + return; + } + + let input = currently_pending.input(); + + self.propagate_event = true; + for binding in currently_pending.bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref()); + if !self.propagate_event { + return; + } + } + + let dispatch_path = self + .window + .rendered_frame + .dispatch_tree + .dispatch_path(node_id); + + for keystroke in currently_pending.keystrokes { + let event = KeyDownEvent { + keystroke, + is_held: false, + }; + + self.dispatch_key_down_up_event(&event, &dispatch_path); + if !self.propagate_event { + return; + } + } + + if !input.is_empty() { + if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { + input_handler.dispatch_input(&input, self); + self.window.platform_window.set_input_handler(input_handler) + } + } + } + + fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: &dyn Action) { + let dispatch_path = self + .window + .rendered_frame + .dispatch_tree + .dispatch_path(node_id); + + // Capture phase for global actions. + self.propagate_event = true; + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if !self.propagate_event { + return; + } + + // Capture phase for window actions. + for node_id in &dispatch_path { + let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + listener(any_action, DispatchPhase::Capture, self); + + if !self.propagate_event { + return; + } + } + } + } + + // Bubble phase for window actions. + for node_id in dispatch_path.iter().rev() { + let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + self.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(any_action, DispatchPhase::Bubble, self); + + if !self.propagate_event { + return; + } + } + } + } + + // Bubble phase for global actions. + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + self.propagate_event = false; // Actions stop propagation by default during the bubble phase + + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + + /// Register the given handler to be invoked whenever the global of the given type + /// is updated. + pub fn observe_global( + &mut self, + f: impl Fn(&mut WindowContext<'_>) + 'static, + ) -> Subscription { + let window_handle = self.window.handle; + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| window_handle.update(cx, |_, cx| f(cx)).is_ok()), + ); + self.app.defer(move |_| activate()); + subscription + } + + /// Focus the current window and bring it to the foreground at the platform level. + pub fn activate_window(&self) { + self.window.platform_window.activate(); + } + + /// Minimize the current window at the platform level. + pub fn minimize_window(&self) { + self.window.platform_window.minimize(); + } + + /// Toggle full screen status on the current window at the platform level. + pub fn toggle_fullscreen(&self) { + self.window.platform_window.toggle_fullscreen(); + } + + /// Present a platform dialog. + /// The provided message will be presented, along with buttons for each answer. + /// When a button is clicked, the returned Receiver will receive the index of the clicked button. + pub fn prompt( + &mut self, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[&str], + ) -> oneshot::Receiver { + let prompt_builder = self.app.prompt_builder.take(); + let Some(prompt_builder) = prompt_builder else { + unreachable!("Re-entrant window prompting is not supported by GPUI"); + }; + + let receiver = match &prompt_builder { + PromptBuilder::Default => self + .window + .platform_window + .prompt(level, message, detail, answers) + .unwrap_or_else(|| { + self.build_custom_prompt(&prompt_builder, level, message, detail, answers) + }), + PromptBuilder::Custom(_) => { + self.build_custom_prompt(&prompt_builder, level, message, detail, answers) + } + }; + + self.app.prompt_builder = Some(prompt_builder); + + receiver + } + + fn build_custom_prompt( + &mut self, + prompt_builder: &PromptBuilder, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[&str], + ) -> oneshot::Receiver { + let (sender, receiver) = oneshot::channel(); + let handle = PromptHandle::new(sender); + let handle = (prompt_builder)(level, message, detail, answers, handle, self); + self.window.prompt = Some(handle); + receiver + } + + /// Returns all available actions for the focused element. + pub fn available_actions(&self) -> Vec> { + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .rendered_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); + + let mut actions = self + .window + .rendered_frame + .dispatch_tree + .available_actions(node_id); + for action_type in self.global_action_listeners.keys() { + if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) { + let action = self.actions.build_action_type(action_type).ok(); + if let Some(action) = action { + actions.insert(ix, action); + } + } + } + actions + } + + /// Returns key bindings that invoke the given action on the currently focused element. + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.window + .rendered_frame + .dispatch_tree + .bindings_for_action( + action, + &self.window.rendered_frame.dispatch_tree.context_stack, + ) + } + + /// Returns any bindings that would invoke the given action on the given focus handle if it were focused. + pub fn bindings_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> Vec { + let dispatch_tree = &self.window.rendered_frame.dispatch_tree; + + let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else { + return vec![]; + }; + let context_stack: Vec<_> = dispatch_tree + .dispatch_path(node_id) + .into_iter() + .filter_map(|node_id| dispatch_tree.node(node_id).context.clone()) + .collect(); + dispatch_tree.bindings_for_action(action, &context_stack) + } + + /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle. + pub fn listener_for( + &self, + view: &View, + f: impl Fn(&mut V, &E, &mut ViewContext) + 'static, + ) -> impl Fn(&E, &mut WindowContext) + 'static { + let view = view.downgrade(); + move |e: &E, cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, e, cx)).ok(); + } + } + + /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle. + pub fn handler_for( + &self, + view: &View, + f: impl Fn(&mut V, &mut ViewContext) + 'static, + ) -> impl Fn(&mut WindowContext) { + let view = view.downgrade(); + move |cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, cx)).ok(); + } + } + + /// Register a callback that can interrupt the closing of the current window based the returned boolean. + /// If the callback returns false, the window won't be closed. + pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) { + let mut this = self.to_async(); + self.window + .platform_window + .on_should_close(Box::new(move || this.update(|cx| f(cx)).unwrap_or(true))) + } + + /// Register an action listener on the window for the next frame. The type of action + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_action( + &mut self, + action_type: TypeId, + listener: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, + ) { + self.window + .next_frame + .dispatch_tree + .on_action(action_type, Rc::new(listener)); + } +} + +#[cfg(target_os = "windows")] +impl WindowContext<'_> { + /// Returns the raw HWND handle for the window. + pub fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { + self.window.platform_window.get_raw_handle() + } +} + +impl Context for WindowContext<'_> { + type Result = T; + + fn new_model(&mut self, build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T) -> Model + where + T: 'static, + { + let slot = self.app.entities.reserve(); + let model = build_model(&mut ModelContext::new(&mut *self.app, slot.downgrade())); + self.entities.insert(slot, model) + } + + fn reserve_model(&mut self) -> Self::Result> { + self.app.reserve_model() + } + + fn insert_model( + &mut self, + reservation: crate::Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.app.insert_model(reservation, build_model) + } + + fn update_model( + &mut self, + model: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> R { + let mut entity = self.entities.lease(model); + let result = update( + &mut *entity, + &mut ModelContext::new(&mut *self.app, model.downgrade()), + ); + self.entities.end_lease(entity); + result + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + let entity = self.entities.read(handle); + read(entity, &*self.app) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + if window == self.window.handle { + let root_view = self.window.root_view.clone().unwrap(); + Ok(update(root_view, self)) + } else { + window.update(self.app, update) + } + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + if window.any_handle == self.window.handle { + let root_view = self + .window + .root_view + .clone() + .unwrap() + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + Ok(read(root_view, self)) + } else { + self.app.read_window(window, read) + } + } +} + +impl VisualContext for WindowContext<'_> { + fn new_view( + &mut self, + build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + let slot = self.app.entities.reserve(); + let view = View { + model: slot.clone(), + }; + let mut cx = ViewContext::new(&mut *self.app, &mut *self.window, &view); + let entity = build_view_state(&mut cx); + cx.entities.insert(slot, entity); + + // Non-generic part to avoid leaking SubscriberSet to invokers of `new_view`. + fn notify_observers(cx: &mut WindowContext, tid: TypeId, view: AnyView) { + cx.new_view_observers.clone().retain(&tid, |observer| { + let any_view = view.clone(); + (observer)(any_view, cx); + true + }); + } + notify_observers(self, TypeId::of::(), AnyView::from(view.clone())); + + view + } + + /// Updates the given view. Prefer calling [`View::update`] instead, which calls this method. + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut T, &mut ViewContext<'_, T>) -> R, + ) -> Self::Result { + let mut lease = self.app.entities.lease(&view.model); + let mut cx = ViewContext::new(&mut *self.app, &mut *self.window, view); + let result = update(&mut *lease, &mut cx); + cx.app.entities.end_lease(lease); + result + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + let view = self.new_view(build_view); + self.window.root_view = Some(view.clone().into()); + self.refresh(); + view + } + + fn focus_view(&mut self, view: &View) -> Self::Result<()> { + self.update_view(view, |view, cx| { + view.focus_handle(cx).clone().focus(cx); + }) + } + + fn dismiss_view(&mut self, view: &View) -> Self::Result<()> + where + V: ManagedView, + { + self.update_view(view, |_, cx| cx.emit(DismissEvent)) + } +} + +impl<'a> std::ops::Deref for WindowContext<'a> { + type Target = AppContext; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a> std::ops::DerefMut for WindowContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app + } +} + +impl<'a> Borrow for WindowContext<'a> { + fn borrow(&self) -> &AppContext { + self.app + } +} + +impl<'a> BorrowMut for WindowContext<'a> { + fn borrow_mut(&mut self) -> &mut AppContext { + self.app + } +} + +/// This trait contains functionality that is shared across [`ViewContext`] and [`WindowContext`] +pub trait BorrowWindow: BorrowMut + BorrowMut { + #[doc(hidden)] + fn app_mut(&mut self) -> &mut AppContext { + self.borrow_mut() + } + + #[doc(hidden)] + fn app(&self) -> &AppContext { + self.borrow() + } + + #[doc(hidden)] + fn window(&self) -> &Window { + self.borrow() + } + + #[doc(hidden)] + fn window_mut(&mut self) -> &mut Window { + self.borrow_mut() + } +} + +impl Borrow for WindowContext<'_> { + fn borrow(&self) -> &Window { + self.window + } +} + +impl BorrowMut for WindowContext<'_> { + fn borrow_mut(&mut self) -> &mut Window { + self.window + } +} + +impl BorrowWindow for T where T: BorrowMut + BorrowMut {} + +/// Provides access to application state that is specialized for a particular [`View`]. +/// Allows you to interact with focus, emit events, etc. +/// ViewContext also derefs to [`WindowContext`], giving you access to all of its methods as well. +/// When you call [`View::update`], you're passed a `&mut V` and an `&mut ViewContext`. +pub struct ViewContext<'a, V> { + window_cx: WindowContext<'a>, + view: &'a View, +} + +impl Borrow for ViewContext<'_, V> { + fn borrow(&self) -> &AppContext { + &*self.window_cx.app + } +} + +impl BorrowMut for ViewContext<'_, V> { + fn borrow_mut(&mut self) -> &mut AppContext { + &mut *self.window_cx.app + } +} + +impl Borrow for ViewContext<'_, V> { + fn borrow(&self) -> &Window { + &*self.window_cx.window + } +} + +impl BorrowMut for ViewContext<'_, V> { + fn borrow_mut(&mut self) -> &mut Window { + &mut *self.window_cx.window + } +} + +impl<'a, V: 'static> ViewContext<'a, V> { + pub(crate) fn new(app: &'a mut AppContext, window: &'a mut Window, view: &'a View) -> Self { + Self { + window_cx: WindowContext::new(app, window), + view, + } + } + + /// Get the entity_id of this view. + pub fn entity_id(&self) -> EntityId { + self.view.entity_id() + } + + /// Get the view pointer underlying this context. + pub fn view(&self) -> &View { + self.view + } + + /// Get the model underlying this view. + pub fn model(&self) -> &Model { + &self.view.model + } + + /// Access the underlying window context. + pub fn window_context(&mut self) -> &mut WindowContext<'a> { + &mut self.window_cx + } + + /// Sets a given callback to be run on the next frame. + pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) + where + V: 'static, + { + let view = self.view().clone(); + self.window_cx.on_next_frame(move |cx| view.update(cx, f)); + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) { + let view = self.view().downgrade(); + self.window_cx.defer(move |cx| { + view.update(cx, f).ok(); + }); + } + + /// Observe another model or view for changes to its state, as tracked by [`ModelContext::notify`]. + pub fn observe( + &mut self, + entity: &E, + mut on_notify: impl FnMut(&mut V, E, &mut ViewContext<'_, V>) + 'static, + ) -> Subscription + where + V2: 'static, + V: 'static, + E: Entity, + { + let view = self.view().downgrade(); + let entity_id = entity.entity_id(); + let entity = entity.downgrade(); + let window_handle = self.window.handle; + self.app.new_observer( + entity_id, + Box::new(move |cx| { + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&entity) { + view.update(cx, |this, cx| on_notify(this, handle, cx)) + .is_ok() + } else { + false + } + }) + .unwrap_or(false) + }), + ) + } + + /// Subscribe to events emitted by another model or view. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked with a reference to the current view, a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a view context for the current view. + pub fn subscribe( + &mut self, + entity: &E, + mut on_event: impl FnMut(&mut V, E, &Evt, &mut ViewContext<'_, V>) + 'static, + ) -> Subscription + where + V2: EventEmitter, + E: Entity, + Evt: 'static, + { + let view = self.view().downgrade(); + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + let window_handle = self.window.handle; + self.app.new_subscription( + entity_id, + ( + TypeId::of::(), + Box::new(move |event, cx| { + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&handle) { + let event = event.downcast_ref().expect("invalid event type"); + view.update(cx, |this, cx| on_event(this, handle, event, cx)) + .is_ok() + } else { + false + } + }) + .unwrap_or(false) + }), + ), + ) + } + + /// Register a callback to be invoked when the view is released. + /// + /// The callback receives a handle to the view's window. This handle may be + /// invalid, if the window was closed before the view was released. + pub fn on_release( + &mut self, + on_release: impl FnOnce(&mut V, AnyWindowHandle, &mut AppContext) + 'static, + ) -> Subscription { + let window_handle = self.window.handle; + let (subscription, activate) = self.app.release_listeners.insert( + self.view.model.entity_id, + Box::new(move |this, cx| { + let this = this.downcast_mut().expect("invalid entity type"); + on_release(this, window_handle, cx) + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when the given Model or View is released. + pub fn observe_release( + &mut self, + entity: &E, + mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, V>) + 'static, + ) -> Subscription + where + V: 'static, + V2: 'static, + E: Entity, + { + let view = self.view().downgrade(); + let entity_id = entity.entity_id(); + let window_handle = self.window.handle; + let (subscription, activate) = self.app.release_listeners.insert( + entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + let _ = window_handle.update(cx, |_, cx| { + view.update(cx, |this, cx| on_release(this, entity, cx)) + }); + }), + ); + activate(); + subscription + } + + /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. + /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. + pub fn notify(&mut self) { + self.window_cx.notify(self.view.entity_id()); + } + + /// Register a callback to be invoked when the window is resized. + pub fn observe_window_bounds( + &mut self, + mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let (subscription, activate) = self.window.bounds_observers.insert( + (), + Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when the window is activated or deactivated. + pub fn observe_window_activation( + &mut self, + mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let (subscription, activate) = self.window.activation_observers.insert( + (), + Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()), + ); + activate(); + subscription + } + + /// Registers a callback to be invoked when the window appearance changes. + pub fn observe_window_appearance( + &mut self, + mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let (subscription, activate) = self.window.appearance_observers.insert( + (), + Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()), + ); + activate(); + subscription + } + + /// Register a listener to be called when the given focus handle receives focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + let (subscription, activate) = + self.window.new_focus_listener(Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if event.previous_focus_path.last() != Some(&focus_id) + && event.current_focus_path.last() == Some(&focus_id) + { + listener(view, cx) + } + }) + .is_ok() + })); + self.app.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants receives focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_in( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + let (subscription, activate) = + self.window.new_focus_listener(Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if !event.previous_focus_path.contains(&focus_id) + && event.current_focus_path.contains(&focus_id) + { + listener(view, cx) + } + }) + .is_ok() + })); + self.app.defer(move |_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_blur( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + let (subscription, activate) = + self.window.new_focus_listener(Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if event.previous_focus_path.last() == Some(&focus_id) + && event.current_focus_path.last() != Some(&focus_id) + { + listener(view, cx) + } + }) + .is_ok() + })); + self.app.defer(move |_| activate()); + subscription + } + + /// Register a listener to be called when nothing in the window has focus. + /// This typically happens when the node that was focused is removed from the tree, + /// and this callback lets you chose a default place to restore the users focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_lost( + &mut self, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let (subscription, activate) = self.window.focus_lost_listeners.insert( + (), + Box::new(move |cx| view.update(cx, |view, cx| listener(view, cx)).is_ok()), + ); + activate(); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_out( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + let (subscription, activate) = + self.window.new_focus_listener(Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if event.previous_focus_path.contains(&focus_id) + && !event.current_focus_path.contains(&focus_id) + { + listener(view, cx) + } + }) + .is_ok() + })); + self.app.defer(move |_| activate()); + subscription + } + + /// Schedule a future to be run asynchronously. + /// The given callback is invoked with a [`WeakView`] to avoid leaking the view for a long-running process. + /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the view across await points. + /// The returned future will be polled on the main thread. + pub fn spawn( + &mut self, + f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut, + ) -> Task + where + R: 'static, + Fut: Future + 'static, + { + let view = self.view().downgrade(); + self.window_cx.spawn(|cx| f(view, cx)) + } + + /// Register a callback to be invoked when the given global state changes. + pub fn observe_global( + &mut self, + mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static, + ) -> Subscription { + let window_handle = self.window.handle; + let view = self.view().downgrade(); + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + window_handle + .update(cx, |_, cx| view.update(cx, |view, cx| f(view, cx)).is_ok()) + .unwrap_or(false) + }), + ); + self.app.defer(move |_| activate()); + subscription + } + + /// Register a callback to be invoked when the given Action type is dispatched to the window. + pub fn on_action( + &mut self, + action_type: TypeId, + listener: impl Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) + 'static, + ) { + let handle = self.view().clone(); + self.window_cx + .on_action(action_type, move |action, phase, cx| { + handle.update(cx, |view, cx| { + listener(view, action, phase, cx); + }) + }); + } + + /// Emit an event to be handled any other views that have subscribed via [ViewContext::subscribe]. + pub fn emit(&mut self, event: Evt) + where + Evt: 'static, + V: EventEmitter, + { + let emitter = self.view.model.entity_id; + self.app.push_effect(Effect::Emit { + emitter, + event_type: TypeId::of::(), + event: Box::new(event), + }); + } + + /// Move focus to the current view, assuming it implements [`FocusableView`]. + pub fn focus_self(&mut self) + where + V: FocusableView, + { + self.defer(|view, cx| view.focus_handle(cx).focus(cx)) + } + + /// Convenience method for accessing view state in an event callback. + /// + /// Many GPUI callbacks take the form of `Fn(&E, &mut WindowContext)`, + /// but it's often useful to be able to access view state in these + /// callbacks. This method provides a convenient way to do so. + pub fn listener( + &self, + f: impl Fn(&mut V, &E, &mut ViewContext) + 'static, + ) -> impl Fn(&E, &mut WindowContext) + 'static { + let view = self.view().downgrade(); + move |e: &E, cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, e, cx)).ok(); + } + } +} + +impl Context for ViewContext<'_, V> { + type Result = U; + + fn new_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Model { + self.window_cx.new_model(build_model) + } + + fn reserve_model(&mut self) -> Self::Result> { + self.window_cx.reserve_model() + } + + fn insert_model( + &mut self, + reservation: crate::Reservation, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.window_cx.insert_model(reservation, build_model) + } + + fn update_model( + &mut self, + model: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> R { + self.window_cx.update_model(model, update) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + self.window_cx.read_model(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.window_cx.update_window(window, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.window_cx.read_window(window, read) + } +} + +impl VisualContext for ViewContext<'_, V> { + fn new_view( + &mut self, + build_view_state: impl FnOnce(&mut ViewContext<'_, W>) -> W, + ) -> Self::Result> { + self.window_cx.new_view(build_view_state) + } + + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut V2, &mut ViewContext<'_, V2>) -> R, + ) -> Self::Result { + self.window_cx.update_view(view, update) + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, + ) -> Self::Result> + where + W: 'static + Render, + { + self.window_cx.replace_root_view(build_view) + } + + fn focus_view(&mut self, view: &View) -> Self::Result<()> { + self.window_cx.focus_view(view) + } + + fn dismiss_view(&mut self, view: &View) -> Self::Result<()> { + self.window_cx.dismiss_view(view) + } +} + +impl<'a, V> std::ops::Deref for ViewContext<'a, V> { + type Target = WindowContext<'a>; + + fn deref(&self) -> &Self::Target { + &self.window_cx + } +} + +impl<'a, V> std::ops::DerefMut for ViewContext<'a, V> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.window_cx + } +} + +// #[derive(Clone, Copy, Eq, PartialEq, Hash)] +slotmap::new_key_type! { + /// A unique identifier for a window. + pub struct WindowId; +} + +impl WindowId { + /// Converts this window ID to a `u64`. + pub fn as_u64(&self) -> u64 { + self.0.as_ffi() + } +} + +/// A handle to a window with a specific root view type. +/// Note that this does not keep the window alive on its own. +#[derive(Deref, DerefMut)] +pub struct WindowHandle { + #[deref] + #[deref_mut] + pub(crate) any_handle: AnyWindowHandle, + state_type: PhantomData, +} + +impl WindowHandle { + /// Creates a new handle from a window ID. + /// This does not check if the root type of the window is `V`. + pub fn new(id: WindowId) -> Self { + WindowHandle { + any_handle: AnyWindowHandle { + id, + state_type: TypeId::of::(), + }, + state_type: PhantomData, + } + } + + /// Get the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn root(&self, cx: &mut C) -> Result> + where + C: Context, + { + Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| { + root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed")) + })) + } + + /// Updates the root view of this window. + /// + /// This will fail if the window has been closed or if the root view's type does not match + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Result + where + C: Context, + { + cx.update_window(self.any_handle, |root_view, cx| { + let view = root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + Ok(cx.update_view(&view, update)) + })? + } + + /// Read the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> { + let x = cx + .windows + .get(self.id) + .and_then(|window| { + window + .as_ref() + .and_then(|window| window.root_view.clone()) + .map(|root_view| root_view.downcast::()) + }) + .ok_or_else(|| anyhow!("window not found"))? + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + + Ok(x.read(cx)) + } + + /// Read the root view out of this window, with a callback + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result + where + C: Context, + { + cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) + } + + /// Read the root view pointer off of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn root_view(&self, cx: &C) -> Result> + where + C: Context, + { + cx.read_window(self, |root_view, _cx| root_view.clone()) + } + + /// Check if this window is 'active'. + /// + /// Will return `None` if the window is closed or currently + /// borrowed. + pub fn is_active(&self, cx: &mut AppContext) -> Option { + cx.update_window(self.any_handle, |_, cx| cx.is_window_active()) + .ok() + } +} + +impl Copy for WindowHandle {} + +impl Clone for WindowHandle { + fn clone(&self) -> Self { + *self + } +} + +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + self.any_handle == other.any_handle + } +} + +impl Eq for WindowHandle {} + +impl Hash for WindowHandle { + fn hash(&self, state: &mut H) { + self.any_handle.hash(state); + } +} + +impl From> for AnyWindowHandle { + fn from(val: WindowHandle) -> Self { + val.any_handle + } +} + +/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct AnyWindowHandle { + pub(crate) id: WindowId, + state_type: TypeId, +} + +impl AnyWindowHandle { + /// Get the ID of this window. + pub fn window_id(&self) -> WindowId { + self.id + } + + /// Attempt to convert this handle to a window handle with a specific root view type. + /// If the types do not match, this will return `None`. + pub fn downcast(&self) -> Option> { + if TypeId::of::() == self.state_type { + Some(WindowHandle { + any_handle: *self, + state_type: PhantomData, + }) + } else { + None + } + } + + /// Updates the state of the root view of this window. + /// + /// This will fail if the window has been closed. + pub fn update( + self, + cx: &mut C, + update: impl FnOnce(AnyView, &mut WindowContext<'_>) -> R, + ) -> Result + where + C: Context, + { + cx.update_window(self, update) + } + + /// Read the state of the root view of this window. + /// + /// This will fail if the window has been closed. + pub fn read(self, cx: &C, read: impl FnOnce(View, &AppContext) -> R) -> Result + where + C: Context, + T: 'static, + { + let view = self + .downcast::() + .context("the type of the window's root view has changed")?; + + cx.read_window(&view, read) + } +} + +/// An identifier for an [`Element`](crate::Element). +/// +/// Can be constructed with a string, a number, or both, as well +/// as other internal representations. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum ElementId { + /// The ID of a View element + View(EntityId), + /// An integer ID. + Integer(usize), + /// A string based ID. + Name(SharedString), + /// An ID that's equated with a focus handle. + FocusHandle(FocusId), + /// A combination of a name and an integer. + NamedInteger(SharedString, usize), +} + +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ElementId::View(entity_id) => write!(f, "view-{}", entity_id)?, + ElementId::Integer(ix) => write!(f, "{}", ix)?, + ElementId::Name(name) => write!(f, "{}", name)?, + ElementId::FocusHandle(_) => write!(f, "FocusHandle")?, + ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, + } + + Ok(()) + } +} + +impl TryInto for ElementId { + type Error = anyhow::Error; + + fn try_into(self) -> anyhow::Result { + if let ElementId::Name(name) = self { + Ok(name) + } else { + Err(anyhow!("element id is not string")) + } + } +} + +impl From for ElementId { + fn from(id: usize) -> Self { + ElementId::Integer(id) + } +} + +impl From for ElementId { + fn from(id: i32) -> Self { + Self::Integer(id as usize) + } +} + +impl From for ElementId { + fn from(name: SharedString) -> Self { + ElementId::Name(name) + } +} + +impl From<&'static str> for ElementId { + fn from(name: &'static str) -> Self { + ElementId::Name(name.into()) + } +} + +impl<'a> From<&'a FocusHandle> for ElementId { + fn from(handle: &'a FocusHandle) -> Self { + ElementId::FocusHandle(handle.id) + } +} + +impl From<(&'static str, EntityId)> for ElementId { + fn from((name, id): (&'static str, EntityId)) -> Self { + ElementId::NamedInteger(name.into(), id.as_u64() as usize) + } +} + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(name.into(), id) + } +} + +impl From<(&'static str, u64)> for ElementId { + fn from((name, id): (&'static str, u64)) -> Self { + ElementId::NamedInteger(name.into(), id as usize) + } +} + +/// A rectangle to be rendered in the window at the given position and size. +/// Passed as an argument [`WindowContext::paint_quad`]. +#[derive(Clone)] +pub struct PaintQuad { + /// The bounds of the quad within the window. + pub bounds: Bounds, + /// The radii of the quad's corners. + pub corner_radii: Corners, + /// The background color of the quad. + pub background: Hsla, + /// The widths of the quad's borders. + pub border_widths: Edges, + /// The color of the quad's borders. + pub border_color: Hsla, +} + +impl PaintQuad { + /// Sets the corner radii of the quad. + pub fn corner_radii(self, corner_radii: impl Into>) -> Self { + PaintQuad { + corner_radii: corner_radii.into(), + ..self + } + } + + /// Sets the border widths of the quad. + pub fn border_widths(self, border_widths: impl Into>) -> Self { + PaintQuad { + border_widths: border_widths.into(), + ..self + } + } + + /// Sets the border color of the quad. + pub fn border_color(self, border_color: impl Into) -> Self { + PaintQuad { + border_color: border_color.into(), + ..self + } + } + + /// Sets the background color of the quad. + pub fn background(self, background: impl Into) -> Self { + PaintQuad { + background: background.into(), + ..self + } + } +} + +/// Creates a quad with the given parameters. +pub fn quad( + bounds: Bounds, + corner_radii: impl Into>, + background: impl Into, + border_widths: impl Into>, + border_color: impl Into, +) -> PaintQuad { + PaintQuad { + bounds, + corner_radii: corner_radii.into(), + background: background.into(), + border_widths: border_widths.into(), + border_color: border_color.into(), + } +} + +/// Creates a filled quad with the given bounds and background color. +pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: background.into(), + border_widths: (0.).into(), + border_color: transparent_black(), + } +} + +/// Creates a rectangle outline with the given bounds, border color, and a 1px border width +pub fn outline(bounds: impl Into>, border_color: impl Into) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: transparent_black(), + border_widths: (1.).into(), + border_color: border_color.into(), + } +} diff --git a/crates/ming/src/window/prompts.rs b/crates/ming/src/window/prompts.rs new file mode 100644 index 0000000..69e4f88 --- /dev/null +++ b/crates/ming/src/window/prompts.rs @@ -0,0 +1,222 @@ +use std::ops::Deref; + +use futures::channel::oneshot; + +use crate::{ + div, opaque_grey, white, AnyView, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + IntoElement, ParentElement, PromptLevel, Render, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext, WindowContext, +}; + +/// The event emitted when a prompt's option is selected. +/// The usize is the index of the selected option, from the actions +/// passed to the prompt. +pub struct PromptResponse(pub usize); + +/// A prompt that can be rendered in the window. +pub trait Prompt: EventEmitter + FocusableView {} + +impl + FocusableView> Prompt for V {} + +/// A handle to a prompt that can be used to interact with it. +pub struct PromptHandle { + sender: oneshot::Sender, +} + +impl PromptHandle { + pub(crate) fn new(sender: oneshot::Sender) -> Self { + Self { sender } + } + + /// Construct a new prompt handle from a view of the appropriate types + pub fn with_view( + self, + view: View, + cx: &mut WindowContext, + ) -> RenderablePromptHandle { + let mut sender = Some(self.sender); + let previous_focus = cx.focused(); + cx.subscribe(&view, move |_, e: &PromptResponse, cx| { + if let Some(sender) = sender.take() { + sender.send(e.0).ok(); + cx.window.prompt.take(); + if let Some(previous_focus) = &previous_focus { + cx.focus(&previous_focus); + } + } + }) + .detach(); + + cx.focus_view(&view); + + RenderablePromptHandle { + view: Box::new(view), + } + } +} + +/// A prompt handle capable of being rendered in a window. +pub struct RenderablePromptHandle { + pub(crate) view: Box, +} + +/// Use this function in conjunction with [AppContext::set_prompt_renderer] to force +/// GPUI to always use the fallback prompt renderer. +pub fn fallback_prompt_renderer( + level: PromptLevel, + message: &str, + detail: Option<&str>, + actions: &[&str], + handle: PromptHandle, + cx: &mut WindowContext, +) -> RenderablePromptHandle { + let renderer = cx.new_view({ + |cx| FallbackPromptRenderer { + _level: level, + message: message.to_string(), + detail: detail.map(ToString::to_string), + actions: actions.iter().map(ToString::to_string).collect(), + focus: cx.focus_handle(), + } + }); + + handle.with_view(renderer, cx) +} + +/// The default GPUI fallback for rendering prompts, when the platform doesn't support it. +pub struct FallbackPromptRenderer { + _level: PromptLevel, + message: String, + detail: Option, + actions: Vec, + focus: FocusHandle, +} + +impl Render for FallbackPromptRenderer { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let prompt = div() + .cursor_default() + .track_focus(&self.focus) + .w_72() + .bg(white()) + .rounded_lg() + .overflow_hidden() + .p_3() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .child(div().overflow_hidden().child(self.message.clone())), + ) + .children(self.detail.clone().map(|detail| { + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .text_sm() + .mb_2() + .child(div().child(detail)) + })) + .children(self.actions.iter().enumerate().map(|(ix, action)| { + div() + .flex() + .flex_row() + .justify_around() + .border_1() + .border_color(opaque_grey(0.2, 0.5)) + .mt_1() + .rounded_sm() + .cursor_pointer() + .text_sm() + .child(action.clone()) + .id(ix) + .on_click(cx.listener(move |_, _, cx| { + cx.emit(PromptResponse(ix)); + })) + })); + + div() + .size_full() + .child( + div() + .size_full() + .bg(opaque_grey(0.5, 0.6)) + .absolute() + .top_0() + .left_0(), + ) + .child( + div() + .size_full() + .absolute() + .top_0() + .left_0() + .flex() + .flex_col() + .justify_around() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .child(prompt), + ), + ) + } +} + +impl EventEmitter for FallbackPromptRenderer {} + +impl FocusableView for FallbackPromptRenderer { + fn focus_handle(&self, _: &crate::AppContext) -> FocusHandle { + self.focus.clone() + } +} + +pub(crate) trait PromptViewHandle { + fn any_view(&self) -> AnyView; +} + +impl PromptViewHandle for View { + fn any_view(&self) -> AnyView { + self.clone().into() + } +} + +pub(crate) enum PromptBuilder { + Default, + Custom( + Box< + dyn Fn( + PromptLevel, + &str, + Option<&str>, + &[&str], + PromptHandle, + &mut WindowContext, + ) -> RenderablePromptHandle, + >, + ), +} + +impl Deref for PromptBuilder { + type Target = dyn Fn( + PromptLevel, + &str, + Option<&str>, + &[&str], + PromptHandle, + &mut WindowContext, + ) -> RenderablePromptHandle; + + fn deref(&self) -> &Self::Target { + match self { + Self::Default => &fallback_prompt_renderer, + Self::Custom(f) => f.as_ref(), + } + } +} diff --git a/crates/ming/tests/action_macros.rs b/crates/ming/tests/action_macros.rs new file mode 100644 index 0000000..99572a4 --- /dev/null +++ b/crates/ming/tests/action_macros.rs @@ -0,0 +1,50 @@ +use gpui::{actions, impl_actions}; +use gpui_macros::register_action; +use serde_derive::Deserialize; + +#[test] +fn test_action_macros() { + actions!(test, [TestAction]); + + #[derive(PartialEq, Clone, Deserialize)] + struct AnotherTestAction; + + impl_actions!(test, [AnotherTestAction]); + + #[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)] + struct RegisterableAction {} + + register_action!(RegisterableAction); + + impl gpui::Action for RegisterableAction { + fn boxed_clone(&self) -> Box { + unimplemented!() + } + + fn as_any(&self) -> &dyn std::any::Any { + unimplemented!() + } + + fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { + unimplemented!() + } + + fn name(&self) -> &str { + unimplemented!() + } + + fn debug_name() -> &'static str + where + Self: Sized, + { + unimplemented!() + } + + fn build(_value: serde_json::Value) -> anyhow::Result> + where + Self: Sized, + { + unimplemented!() + } + } +} diff --git a/crates/ming_macros/Cargo.toml b/crates/ming_macros/Cargo.toml new file mode 100644 index 0000000..441f6b1 --- /dev/null +++ b/crates/ming_macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ming_macros" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.66" +quote = "1.0.9" +syn = { version = "2", features = ["full"] } diff --git a/crates/ming_macros/src/derive_into_element.rs b/crates/ming_macros/src/derive_into_element.rs new file mode 100644 index 0000000..e430c1d --- /dev/null +++ b/crates/ming_macros/src/derive_into_element.rs @@ -0,0 +1,23 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +pub fn derive_into_element(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + + let gen = quote! { + impl #impl_generics gpui::IntoElement for #type_name #type_generics + #where_clause + { + type Element = gpui::Component; + + fn into_element(self) -> Self::Element { + gpui::Component::new(self) + } + } + }; + + gen.into() +} diff --git a/crates/ming_macros/src/derive_render.rs b/crates/ming_macros/src/derive_render.rs new file mode 100644 index 0000000..2b39248 --- /dev/null +++ b/crates/ming_macros/src/derive_render.rs @@ -0,0 +1,21 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +pub fn derive_render(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + + let gen = quote! { + impl #impl_generics gpui::Render for #type_name #type_generics + #where_clause + { + fn render(&mut self, _cx: &mut gpui::ViewContext) -> impl gpui::Element { + gpui::Empty + } + } + }; + + gen.into() +} diff --git a/crates/ming_macros/src/lib.rs b/crates/ming_macros/src/lib.rs new file mode 100644 index 0000000..aef1785 --- /dev/null +++ b/crates/ming_macros/src/lib.rs @@ -0,0 +1,62 @@ +mod derive_into_element; +mod derive_render; +mod register_action; +mod style_helpers; +mod test; + +use proc_macro::TokenStream; + +#[proc_macro] +/// register_action! can be used to register an action with the GPUI runtime. +/// You should typically use `gpui::actions!` or `gpui::impl_actions!` instead, +/// but this can be used for fine grained customization. +pub fn register_action(ident: TokenStream) -> TokenStream { + register_action::register_action_macro(ident) +} + +#[proc_macro_derive(IntoElement)] +// #[derive(IntoElement)] is used to create a Component out of anything that implements +// the `RenderOnce` trait. +pub fn derive_into_element(input: TokenStream) -> TokenStream { + derive_into_element::derive_into_element(input) +} + +#[proc_macro_derive(Render)] +#[doc(hidden)] +pub fn derive_render(input: TokenStream) -> TokenStream { + derive_render::derive_render(input) +} + +// Used by gpui to generate the style helpers. +#[proc_macro] +#[doc(hidden)] +pub fn style_helpers(input: TokenStream) -> TokenStream { + style_helpers::style_helpers(input) +} + +#[proc_macro_attribute] +/// #[gpui::test] can be used to annotate test functions that run with GPUI support. +/// it supports both synchronous and asynchronous tests, and can provide you with +/// as many `TestAppContext` instances as you need. +/// The output contains a `#[test]` annotation so this can be used with any existing +/// test harness (`cargo test` or `cargo-nextest`). +/// +/// ``` +/// #[gpui::test] +/// async fn test_foo(mut cx: &TestAppContext) { } +/// ``` +/// +/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance. +/// this will be seeded with the `SEED` environment variable and is used internally by +/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests. +/// Using the same `StdRng` for behaviour in your test will allow you to exercise a wide +/// variety of scenarios and interleavings just by changing the seed. +/// +/// #[gpui::test] also takes three different arguments: +/// - `#[gpui::test(iterations=10)]` will run the test ten times with a different initial SEED. +/// - `#[gpui::test(retries=3)]` will run the test up to four times if it fails to try and make it pass. +/// - `#[gpui::test(on_failure="crate::test::report_failure")]` will call the specified function after the +/// tests fail so that you can write out more detail about the failure. +pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { + test::test(args, function) +} diff --git a/crates/ming_macros/src/register_action.rs b/crates/ming_macros/src/register_action.rs new file mode 100644 index 0000000..7ec1d6d --- /dev/null +++ b/crates/ming_macros/src/register_action.rs @@ -0,0 +1,48 @@ +use proc_macro::TokenStream; +use proc_macro2::Ident; +use quote::{format_ident, quote}; +use syn::parse_macro_input; + +pub fn register_action_macro(ident: TokenStream) -> TokenStream { + let name = parse_macro_input!(ident as Ident); + let registration = register_action(&name); + + TokenStream::from(quote! { + #registration + }) +} + +pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream { + let static_slice_name = + format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase()); + + let action_builder_fn_name = format_ident!( + "__gpui_actions_builder_{}", + type_name.to_string().to_lowercase() + ); + + quote! { + impl #type_name { + /// This is an auto generated function, do not use. + #[automatically_derived] + #[doc(hidden)] + fn __autogenerated() { + /// This is an auto generated function, do not use. + #[doc(hidden)] + fn #action_builder_fn_name() -> gpui::ActionData { + gpui::ActionData { + name: <#type_name as gpui::Action>::debug_name(), + type_id: ::std::any::TypeId::of::<#type_name>(), + build: <#type_name as gpui::Action>::build, + } + } + #[doc(hidden)] + #[gpui::private::linkme::distributed_slice(gpui::__GPUI_ACTIONS)] + #[linkme(crate = gpui::private::linkme)] + static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name; + } + } + + + } +} diff --git a/crates/ming_macros/src/style_helpers.rs b/crates/ming_macros/src/style_helpers.rs new file mode 100644 index 0000000..a4b1fe9 --- /dev/null +++ b/crates/ming_macros/src/style_helpers.rs @@ -0,0 +1,569 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream, Result}, + parse_macro_input, +}; + +struct StyleableMacroInput; + +impl Parse for StyleableMacroInput { + fn parse(_input: ParseStream) -> Result { + Ok(StyleableMacroInput) + } +} + +pub fn style_helpers(input: TokenStream) -> TokenStream { + let _ = parse_macro_input!(input as StyleableMacroInput); + let methods = generate_methods(); + let output = quote! { + #(#methods)* + }; + + output.into() +} + +fn generate_methods() -> Vec { + let mut methods = Vec::new(); + + for (prefix, auto_allowed, fields, prefix_doc_string) in box_prefixes() { + methods.push(generate_custom_value_setter( + prefix, + if auto_allowed { + quote! { Length } + } else { + quote! { DefiniteLength } + }, + &fields, + prefix_doc_string, + )); + + for (suffix, length_tokens, suffix_doc_string) in box_suffixes() { + if suffix != "auto" || auto_allowed { + methods.push(generate_predefined_setter( + prefix, + suffix, + &fields, + &length_tokens, + false, + &format!("{prefix_doc_string}\n\n{suffix_doc_string}"), + )); + } + + if suffix != "auto" { + methods.push(generate_predefined_setter( + prefix, + suffix, + &fields, + &length_tokens, + true, + &format!("{prefix_doc_string}\n\n{suffix_doc_string}"), + )); + } + } + } + + for (prefix, fields, prefix_doc_string) in corner_prefixes() { + methods.push(generate_custom_value_setter( + prefix, + quote! { AbsoluteLength }, + &fields, + prefix_doc_string, + )); + + for (suffix, radius_tokens, suffix_doc_string) in corner_suffixes() { + methods.push(generate_predefined_setter( + prefix, + suffix, + &fields, + &radius_tokens, + false, + &format!("{prefix_doc_string}\n\n{suffix_doc_string}"), + )); + } + } + + for (prefix, fields, prefix_doc_string) in border_prefixes() { + methods.push(generate_custom_value_setter( + prefix, + quote! { AbsoluteLength }, + &fields, + prefix_doc_string, + )); + + for (suffix, width_tokens, suffix_doc_string) in border_suffixes() { + methods.push(generate_predefined_setter( + prefix, + suffix, + &fields, + &width_tokens, + false, + &format!("{prefix_doc_string}\n\n{suffix_doc_string}"), + )); + } + } + methods +} + +fn generate_predefined_setter( + name: &'static str, + length: &'static str, + fields: &[TokenStream2], + length_tokens: &TokenStream2, + negate: bool, + doc_string: &str, +) -> TokenStream2 { + let (negation_qualifier, negation_token) = if negate { + ("_neg", quote! { - }) + } else { + ("", quote! {}) + }; + + let method_name = if length.is_empty() { + format_ident!("{name}{negation_qualifier}") + } else { + format_ident!("{name}{negation_qualifier}_{length}") + }; + + let field_assignments = fields + .iter() + .map(|field_tokens| { + quote! { + style.#field_tokens = Some((#negation_token gpui::#length_tokens).into()); + } + }) + .collect::>(); + + let method = quote! { + #[doc = #doc_string] + fn #method_name(mut self) -> Self { + let style = self.style(); + #(#field_assignments)* + self + } + }; + + method +} + +fn generate_custom_value_setter( + prefix: &str, + length_type: TokenStream2, + fields: &[TokenStream2], + doc_string: &str, +) -> TokenStream2 { + let method_name = format_ident!("{}", prefix); + + let mut iter = fields.iter(); + let last = iter.next_back().unwrap(); + let field_assignments = iter + .map(|field_tokens| { + quote! { + style.#field_tokens = Some(length.clone().into()); + } + }) + .chain(std::iter::once(quote! { + style.#last = Some(length.into()); + })) + .collect::>(); + + let method = quote! { + #[doc = #doc_string] + fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self { + let style = self.style(); + #(#field_assignments)* + self + } + }; + + method +} + +/// Returns a vec of (Property name, has 'auto' suffix, tokens for accessing the property, documentation) +fn box_prefixes() -> Vec<(&'static str, bool, Vec, &'static str)> { + vec![ + ( + "w", + true, + vec![quote! { size.width }], + "Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)", + ), + ("h", true, vec![quote! { size.height }], "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)"), + ( + "size", + true, + vec![quote! {size.width}, quote! {size.height}], + "Sets the width and height of the element." + ), + // TODO: These don't use the same size ramp as the others + // see https://tailwindcss.com/docs/max-width + ( + "min_w", + true, + vec![quote! { min_size.width }], + "Sets the minimum width of the element. [Docs](https://tailwindcss.com/docs/min-width)", + ), + // TODO: These don't use the same size ramp as the others + // see https://tailwindcss.com/docs/max-width + ( + "min_h", + true, + vec![quote! { min_size.height }], + "Sets the minimum height of the element. [Docs](https://tailwindcss.com/docs/min-height)", + ), + // TODO: These don't use the same size ramp as the others + // see https://tailwindcss.com/docs/max-width + ( + "max_w", + true, + vec![quote! { max_size.width }], + "Sets the maximum width of the element. [Docs](https://tailwindcss.com/docs/max-width)", + ), + // TODO: These don't use the same size ramp as the others + // see https://tailwindcss.com/docs/max-width + ( + "max_h", + true, + vec![quote! { max_size.height }], + "Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)", + ), + ( + "m", + true, + vec![ + quote! { margin.top }, + quote! { margin.bottom }, + quote! { margin.left }, + quote! { margin.right }, + ], + "Sets the margin of the element. [Docs](https://tailwindcss.com/docs/margin)" + ), + ("mt", true, vec![quote! { margin.top }], "Sets the top margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"), + ( + "mb", + true, + vec![quote! { margin.bottom }], + "Sets the bottom margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)" + ), + ( + "my", + true, + vec![quote! { margin.top }, quote! { margin.bottom }], + "Sets the vertical margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-vertical-margin)" + ), + ( + "mx", + true, + vec![quote! { margin.left }, quote! { margin.right }], + "Sets the horizontal margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-horizontal-margin)" + ), + ("ml", true, vec![quote! { margin.left }], "Sets the left margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"), + ( + "mr", + true, + vec![quote! { margin.right }], + "Sets the right margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)" + ), + ( + "p", + false, + vec![ + quote! { padding.top }, + quote! { padding.bottom }, + quote! { padding.left }, + quote! { padding.right }, + ], + "Sets the padding of the element. [Docs](https://tailwindcss.com/docs/padding)" + ), + ( + "pt", + false, + vec![quote! { padding.top }], + "Sets the top padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)" + ), + ( + "pb", + false, + vec![quote! { padding.bottom }], + "Sets the bottom padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)" + ), + ( + "px", + false, + vec![quote! { padding.left }, quote! { padding.right }], + "Sets the horizontal padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-horizontal-padding)" + ), + ( + "py", + false, + vec![quote! { padding.top }, quote! { padding.bottom }], + "Sets the vertical padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-vertical-padding)" + ), + ( + "pl", + false, + vec![quote! { padding.left }], + "Sets the left padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)" + ), + ( + "pr", + false, + vec![quote! { padding.right }], + "Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)" + ), + ( + "inset", + true, + vec![quote! { inset.top }, quote! { inset.right }, quote! { inset.bottom }, quote! { inset.left }], + "Sets the top, right, bottom, and left values of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + ), + ( + "top", + true, + vec![quote! { inset.top }], + "Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + ), + ( + "bottom", + true, + vec![quote! { inset.bottom }], + "Sets the bottom value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + ), + ( + "left", + true, + vec![quote! { inset.left }], + "Sets the left value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + ), + ( + "right", + true, + vec![quote! { inset.right }], + "Sets the right value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", + ), + ( + "gap", + false, + vec![quote! { gap.width }, quote! { gap.height }], + "Sets the gap between rows and columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap)" + ), + ( + "gap_x", + false, + vec![quote! { gap.width }], + "Sets the gap between columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)" + ), + ( + "gap_y", + false, + vec![quote! { gap.height }], + "Sets the gap between rows in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)" + ), + ] +} + +/// Returns a vec of (Suffix size, tokens that correspond to this size, documentation) +fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { + vec![ + ("0", quote! { px(0.) }, "0px"), + ("0p5", quote! { rems(0.125) }, "2px (0.125rem)"), + ("1", quote! { rems(0.25) }, "4px (0.25rem)"), + ("1p5", quote! { rems(0.375) }, "6px (0.375rem)"), + ("2", quote! { rems(0.5) }, "8px (0.5rem)"), + ("2p5", quote! { rems(0.625) }, "10px (0.625rem)"), + ("3", quote! { rems(0.75) }, "12px (0.75rem)"), + ("3p5", quote! { rems(0.875) }, "14px (0.875rem)"), + ("4", quote! { rems(1.) }, "16px (1rem)"), + ("5", quote! { rems(1.25) }, "20px (1.25rem)"), + ("6", quote! { rems(1.5) }, "24px (1.5rem)"), + ("7", quote! { rems(1.75) }, "28px (1.75rem)"), + ("8", quote! { rems(2.0) }, "32px (2rem)"), + ("9", quote! { rems(2.25) }, "36px (2.25rem)"), + ("10", quote! { rems(2.5) }, "40px (2.5rem)"), + ("11", quote! { rems(2.75) }, "44px (2.75rem)"), + ("12", quote! { rems(3.) }, "48px (3rem)"), + ("16", quote! { rems(4.) }, "64px (4rem)"), + ("20", quote! { rems(5.) }, "80px (5rem)"), + ("24", quote! { rems(6.) }, "96px (6rem)"), + ("32", quote! { rems(8.) }, "128px (8rem)"), + ("40", quote! { rems(10.) }, "160px (10rem)"), + ("48", quote! { rems(12.) }, "192px (12rem)"), + ("56", quote! { rems(14.) }, "224px (14rem)"), + ("64", quote! { rems(16.) }, "256px (16rem)"), + ("72", quote! { rems(18.) }, "288px (18rem)"), + ("80", quote! { rems(20.) }, "320px (20rem)"), + ("96", quote! { rems(24.) }, "384px (24rem)"), + ("112", quote! { rems(28.) }, "448px (28rem)"), + ("128", quote! { rems(32.) }, "512px (32rem)"), + ("auto", quote! { auto() }, "Auto"), + ("px", quote! { px(1.) }, "1px"), + ("full", quote! { relative(1.) }, "100%"), + ("1_2", quote! { relative(0.5) }, "50% (1/2)"), + ("1_3", quote! { relative(1./3.) }, "33% (1/3)"), + ("2_3", quote! { relative(2./3.) }, "66% (2/3)"), + ("1_4", quote! { relative(0.25) }, "25% (1/4)"), + ("2_4", quote! { relative(0.5) }, "50% (2/4)"), + ("3_4", quote! { relative(0.75) }, "75% (3/4)"), + ("1_5", quote! { relative(0.2) }, "20% (1/5)"), + ("2_5", quote! { relative(0.4) }, "40% (2/5)"), + ("3_5", quote! { relative(0.6) }, "60% (3/5)"), + ("4_5", quote! { relative(0.8) }, "80% (4/5)"), + ("1_6", quote! { relative(1./6.) }, "16% (1/6)"), + ("5_6", quote! { relative(5./6.) }, "80% (5/6)"), + ("1_12", quote! { relative(1./12.) }, "8% (1/12)"), + ] +} + +fn corner_prefixes() -> Vec<(&'static str, Vec, &'static str)> { + vec![ + ( + "rounded", + vec![ + quote! { corner_radii.top_left }, + quote! { corner_radii.top_right }, + quote! { corner_radii.bottom_right }, + quote! { corner_radii.bottom_left }, + ], + "Sets the border radius of the element. [Docs](https://tailwindcss.com/docs/border-radius)" + ), + ( + "rounded_t", + vec![ + quote! { corner_radii.top_left }, + quote! { corner_radii.top_right }, + ], + "Sets the border radius of the top side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)" + ), + ( + "rounded_b", + vec![ + quote! { corner_radii.bottom_left }, + quote! { corner_radii.bottom_right }, + ], + "Sets the border radius of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)" + ), + ( + "rounded_r", + vec![ + quote! { corner_radii.top_right }, + quote! { corner_radii.bottom_right }, + ], + "Sets the border radius of the right side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)" + ), + ( + "rounded_l", + vec![ + quote! { corner_radii.top_left }, + quote! { corner_radii.bottom_left }, + ], + "Sets the border radius of the left side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)" + ), + ( + "rounded_tl", + vec![quote! { corner_radii.top_left }], + "Sets the border radius of the top left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)" + ), + ( + "rounded_tr", + vec![quote! { corner_radii.top_right }], + "Sets the border radius of the top right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)" + ), + ( + "rounded_bl", + vec![quote! { corner_radii.bottom_left }], + "Sets the border radius of the bottom left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)" + ), + ( + "rounded_br", + vec![quote! { corner_radii.bottom_right }], + "Sets the border radius of the bottom right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)" + ), + ] +} + +fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { + vec![ + ("none", quote! { px(0.) }, "0px"), + ("sm", quote! { rems(0.125) }, "2px (0.125rem)"), + ("md", quote! { rems(0.25) }, "4px (0.25rem)"), + ("lg", quote! { rems(0.5) }, "8px (0.5rem)"), + ("xl", quote! { rems(0.75) }, "12px (0.75rem)"), + ("2xl", quote! { rems(1.) }, "16px (1rem)"), + ("3xl", quote! { rems(1.5) }, "24px (1.5rem)"), + ("full", quote! { px(9999.) }, "9999px"), + ] +} + +fn border_prefixes() -> Vec<(&'static str, Vec, &'static str)> { + vec![ + ( + "border", + vec![ + quote! { border_widths.top }, + quote! { border_widths.right }, + quote! { border_widths.bottom }, + quote! { border_widths.left }, + ], + "Sets the border width of the element. [Docs](https://tailwindcss.com/docs/border-width)" + ), + ( + "border_t", + vec![quote! { border_widths.top }], + "Sets the border width of the top side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)" + ), + ( + "border_b", + vec![quote! { border_widths.bottom }], + "Sets the border width of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)" + ), + ( + "border_r", + vec![quote! { border_widths.right }], + "Sets the border width of the right side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)" + ), + ( + "border_l", + vec![quote! { border_widths.left }], + "Sets the border width of the left side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)" + ), + ( + "border_x", + vec![ + quote! { border_widths.left }, + quote! { border_widths.right }, + ], + "Sets the border width of the vertical sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)" + ), + ( + "border_y", + vec![ + quote! { border_widths.top }, + quote! { border_widths.bottom }, + ], + "Sets the border width of the horizontal sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)" + ), + ] +} + +fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { + vec![ + ("0", quote! { px(0.)}, "0px"), + ("1", quote! { px(1.) }, "1px"), + ("2", quote! { px(2.) }, "2px"), + ("3", quote! { px(3.) }, "3px"), + ("4", quote! { px(4.) }, "4px"), + ("5", quote! { px(5.) }, "5px"), + ("6", quote! { px(6.) }, "6px"), + ("7", quote! { px(7.) }, "7px"), + ("8", quote! { px(8.) }, "8px"), + ("9", quote! { px(9.) }, "9px"), + ("10", quote! { px(10.) }, "10px"), + ("11", quote! { px(11.) }, "11px"), + ("12", quote! { px(12.) }, "12px"), + ("16", quote! { px(16.) }, "16px"), + ("20", quote! { px(20.) }, "20px"), + ("24", quote! { px(24.) }, "24px"), + ("32", quote! { px(32.) }, "32px"), + ] +} diff --git a/crates/ming_macros/src/test.rs b/crates/ming_macros/src/test.rs new file mode 100644 index 0000000..c3a211f --- /dev/null +++ b/crates/ming_macros/src/test.rs @@ -0,0 +1,248 @@ +use proc_macro::TokenStream as TS; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use std::mem; +use syn::{ + parse2, parse_quote, punctuated::Punctuated, spanned::Spanned as _, Expr, + FnArg, ItemFn, Lit, Meta, PatLit as ExprLit, Token, Type, +}; + +pub fn test(args: TS, function: TS) -> TS { + test_impl(args.into(), function.into()).map_or_else(|e| e.into_compile_error().into(), |ts| ts.into()) +} + +fn test_impl(args: TokenStream, function: TokenStream) -> syn::Result { + let Meta::List(args) = parse2(args)? else { + return Err(syn::Error::new(Span::call_site(), "invalid attr")) + }; + let mut max_retries = 0; + let mut num_iterations = 1; + let mut on_failure_fn_name = quote!(None); + + for arg in args.parse_args_with(Punctuated::::parse_terminated)? { + match arg { + Meta::NameValue(meta) => { + let Expr::Lit(ExprLit { lit, .. }) = meta.value else { + return Err(syn::Error::new_spanned( + meta.value, + "expected literal as argument value", + )); + }; + let key_name = meta.path.get_ident().map(|i| i.to_string()); + let result = (|| { + match key_name.as_deref() { + Some("retries") => max_retries = parse_int(&lit)?, + Some("iterations") => num_iterations = parse_int(&lit)?, + Some("on_failure") => { + if let Lit::Str(name) = lit { + let mut path = syn::Path { + leading_colon: None, + segments: Default::default(), + }; + for part in name.value().split("::") { + path.segments.push(Ident::new(part, name.span()).into()); + } + on_failure_fn_name = quote!(Some(#path)); + } else { + return Err(TokenStream::from( + syn::Error::new( + meta.path.span(), + "on_failure argument must be a string", + ) + .into_compile_error(), + )); + } + } + _ => { + return Err(TokenStream::from( + syn::Error::new(meta.path.span(), "invalid argument") + .into_compile_error(), + )) + } + } + Ok(()) + })(); + + if let Err(tokens) = result { + return Ok(tokens); + } + } + other => return Err(syn::Error::new_spanned(other, "invalid argument")), + } + } + + let mut inner_fn = parse2::(function)?; + if max_retries > 0 && num_iterations > 1 { + return Err(syn::Error::new_spanned( + inner_fn, + "retries and randomized iterations can't be mixed", + )); + } + let inner_fn_attributes = mem::take(&mut inner_fn.attrs); + let inner_fn_name = format_ident!("_{}", inner_fn.sig.ident); + let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone()); + + let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() { + // Pass to the test function the number of app contexts that it needs, + // based on its parameter list. + let mut cx_vars = proc_macro2::TokenStream::new(); + let mut cx_teardowns = proc_macro2::TokenStream::new(); + let mut inner_fn_args = proc_macro2::TokenStream::new(); + for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { + if let FnArg::Typed(arg) = arg { + if let Type::Path(ty) = &*arg.ty { + let last_segment = ty.path.segments.last(); + match last_segment.map(|s| s.ident.to_string()).as_deref() { + Some("StdRng") => { + inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); + continue; + } + Some("BackgroundExecutor") => { + inner_fn_args.extend(quote!(gpui::BackgroundExecutor::new( + std::sync::Arc::new(dispatcher.clone()), + ),)); + continue; + } + _ => {} + } + } else if let Type::Reference(ty) = &*arg.ty { + if let Type::Path(ty) = &*ty.elem { + let last_segment = ty.path.segments.last(); + if let Some("TestAppContext") = + last_segment.map(|s| s.ident.to_string()).as_deref() + { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::new( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)), + ); + )); + cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; + } + } + } + } + + return Err(syn::Error::new_spanned(arg, "invalid argument")); + } + + parse_quote! { + #[test] + fn #outer_fn_name() { + #inner_fn + + gpui::run_test( + #num_iterations as u64, + #max_retries, + &mut |dispatcher, _seed| { + let executor = gpui::BackgroundExecutor::new(std::sync::Arc::new(dispatcher.clone())); + #cx_vars + executor.block_test(#inner_fn_name(#inner_fn_args)); + #cx_teardowns + }, + #on_failure_fn_name + ); + } + } + } else { + // Pass to the test function the number of app contexts that it needs, + // based on its parameter list. + let mut cx_vars = proc_macro2::TokenStream::new(); + let mut cx_teardowns = proc_macro2::TokenStream::new(); + let mut inner_fn_args = proc_macro2::TokenStream::new(); + for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { + if let FnArg::Typed(arg) = arg { + if let Type::Path(ty) = &*arg.ty { + let last_segment = ty.path.segments.last(); + + if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() { + inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); + continue; + } + } else if let Type::Reference(ty) = &*arg.ty { + if let Type::Path(ty) = &*ty.elem { + let last_segment = ty.path.segments.last(); + match last_segment.map(|s| s.ident.to_string()).as_deref() { + Some("AppContext") => { + let cx_varname = format_ident!("cx_{}", ix); + let cx_varname_lock = format_ident!("cx_{}_lock", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::new( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); + cx_teardowns.extend(quote!( + drop(#cx_varname_lock); + dispatcher.run_until_parked(); + #cx_varname.update(|cx| { cx.quit() }); + dispatcher.run_until_parked(); + )); + continue; + } + Some("TestAppContext") => { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::new( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + )); + cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; + } + _ => {} + } + } + } + } + + return Err(syn::Error::new_spanned(arg, "invalid argument")); + } + + parse_quote! { + #[test] + fn #outer_fn_name() { + #inner_fn + + gpui::run_test( + #num_iterations as u64, + #max_retries, + &mut |dispatcher, _seed| { + #cx_vars + #inner_fn_name(#inner_fn_args); + #cx_teardowns + }, + #on_failure_fn_name, + ); + } + } + }; + outer_fn.attrs.extend(inner_fn_attributes); + + Ok(outer_fn.into_token_stream()) +} + +fn parse_int(literal: &Lit) -> Result { + let result = if let Lit::Int(int) = &literal { + int.base10_parse() + } else { + Err(syn::Error::new(literal.span(), "must be an integer")) + }; + + result.map_err(|err| TokenStream::from(err.into_compile_error())) +} diff --git a/crates/nite/Cargo.toml b/crates/nite/Cargo.toml new file mode 100644 index 0000000..7b86a3b --- /dev/null +++ b/crates/nite/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "nite" +version.workspace = true +edition.workspace = true + +[dependencies] +funnylog.workspace = true diff --git a/crates/nite/src/main.rs b/crates/nite/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/nite/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/refineable/Cargo.toml b/crates/refineable/Cargo.toml new file mode 100644 index 0000000..25a5cf7 --- /dev/null +++ b/crates/refineable/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "refineable" +version = "0.1.0" +edition = "2021" + +[lints] +workspace = true + +[lib] +doctest = false + +[dependencies] +derive_refineable = { path = "./derive_refineable" } diff --git a/crates/refineable/derive_refineable/Cargo.toml b/crates/refineable/derive_refineable/Cargo.toml new file mode 100644 index 0000000..00502ec --- /dev/null +++ b/crates/refineable/derive_refineable/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "derive_refineable" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/derive_refineable.rs" +proc-macro = true +doctest = false + +[dependencies] +syn = "1.0.72" +quote = "1.0.9" +proc-macro2 = "1.0.66" diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs new file mode 100644 index 0000000..294738b --- /dev/null +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -0,0 +1,362 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + parse_macro_input, parse_quote, DeriveInput, Field, FieldsNamed, PredicateType, TraitBound, + Type, TypeParamBound, WhereClause, WherePredicate, +}; + +#[proc_macro_derive(Refineable, attributes(refineable))] +pub fn derive_refineable(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + attrs, + .. + } = parse_macro_input!(input); + + let refineable_attr = attrs.iter().find(|attr| attr.path.is_ident("refineable")); + + let mut impl_debug_on_refinement = false; + let mut refinement_traits_to_derive = vec![]; + + if let Some(refineable_attr) = refineable_attr { + if let Ok(syn::Meta::List(meta_list)) = refineable_attr.parse_meta() { + for nested in meta_list.nested { + let syn::NestedMeta::Meta(syn::Meta::Path(path)) = nested else { + continue; + }; + + if path.is_ident("Debug") { + impl_debug_on_refinement = true; + } else { + refinement_traits_to_derive.push(path); + } + } + } + } + + let refinement_ident = format_ident!("{}Refinement", ident); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let fields = match data { + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(FieldsNamed { named, .. }), + .. + }) => named.into_iter().collect::>(), + _ => panic!("This derive macro only supports structs with named fields"), + }; + + let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect(); + let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect(); + let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect(); + + // Create trait bound that each wrapped type must implement Clone // & Default + let type_param_bounds: Vec<_> = wrapped_types + .iter() + .map(|ty| { + WherePredicate::Type(PredicateType { + lifetimes: None, + bounded_ty: ty.clone(), + colon_token: Default::default(), + bounds: { + let mut punctuated = syn::punctuated::Punctuated::new(); + punctuated.push_value(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: syn::TraitBoundModifier::None, + lifetimes: None, + path: parse_quote!(Clone), + })); + + punctuated + }, + }) + }) + .collect(); + + // Append to where_clause or create a new one if it doesn't exist + let where_clause = match where_clause.cloned() { + Some(mut where_clause) => { + where_clause.predicates.extend(type_param_bounds); + where_clause.clone() + } + None => WhereClause { + where_token: Default::default(), + predicates: type_param_bounds.into_iter().collect(), + }, + }; + + let refineable_refine_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); + + if is_refineable { + quote! { + self.#name.refine(&refinement.#name); + } + } else if is_optional { + quote! { + if let Some(ref value) = &refinement.#name { + self.#name = Some(value.clone()); + } + } + } else { + quote! { + if let Some(ref value) = &refinement.#name { + self.#name = value.clone(); + } + } + } + }) + .collect(); + + let refineable_refined_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); + + if is_refineable { + quote! { + self.#name = self.#name.refined(refinement.#name); + } + } else if is_optional { + quote! { + if let Some(value) = refinement.#name { + self.#name = Some(value); + } + } + } else { + quote! { + if let Some(value) = refinement.#name { + self.#name = value; + } + } + } + }) + .collect(); + + let refinement_refine_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + + if is_refineable { + quote! { + self.#name.refine(&refinement.#name); + } + } else { + quote! { + if let Some(ref value) = &refinement.#name { + self.#name = Some(value.clone()); + } + } + } + }) + .collect(); + + let refinement_refined_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + + if is_refineable { + quote! { + self.#name = self.#name.refined(refinement.#name); + } + } else { + quote! { + if let Some(value) = refinement.#name { + self.#name = Some(value); + } + } + } + }) + .collect(); + + let from_refinement_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); + + if is_refineable { + quote! { + #name: value.#name.into(), + } + } else if is_optional { + quote! { + #name: value.#name.map(|v| v.into()), + } + } else { + quote! { + #name: value.#name.map(|v| v.into()).unwrap_or_default(), + } + } + }) + .collect(); + + let debug_impl = if impl_debug_on_refinement { + let refinement_field_debugs: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + quote! { + if self.#name.is_some() { + debug_struct.field(stringify!(#name), &self.#name); + } else { + all_some = false; + } + } + }) + .collect(); + + quote! { + impl #impl_generics std::fmt::Debug for #refinement_ident #ty_generics + #where_clause + { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug_struct = f.debug_struct(stringify!(#refinement_ident)); + let mut all_some = true; + #( #refinement_field_debugs )* + if all_some { + debug_struct.finish() + } else { + debug_struct.finish_non_exhaustive() + } + } + } + } + } else { + quote! {} + }; + + let mut derive_stream = quote! {}; + for trait_to_derive in refinement_traits_to_derive { + derive_stream.extend(quote! { #[derive(#trait_to_derive)] }) + } + + let gen = quote! { + /// A refinable version of [`#ident`], see that documentation for details. + #[derive(Clone)] + #derive_stream + pub struct #refinement_ident #impl_generics { + #( + #[allow(missing_docs)] + #field_visibilities #field_names: #wrapped_types + ),* + } + + impl #impl_generics Refineable for #ident #ty_generics + #where_clause + { + type Refinement = #refinement_ident #ty_generics; + + fn refine(&mut self, refinement: &Self::Refinement) { + #( #refineable_refine_assignments )* + } + + fn refined(mut self, refinement: Self::Refinement) -> Self { + #( #refineable_refined_assignments )* + self + } + } + + impl #impl_generics Refineable for #refinement_ident #ty_generics + #where_clause + { + type Refinement = #refinement_ident #ty_generics; + + fn refine(&mut self, refinement: &Self::Refinement) { + #( #refinement_refine_assignments )* + } + + fn refined(mut self, refinement: Self::Refinement) -> Self { + #( #refinement_refined_assignments )* + self + } + } + + impl #impl_generics From<#refinement_ident #ty_generics> for #ident #ty_generics + #where_clause + { + fn from(value: #refinement_ident #ty_generics) -> Self { + Self { + #( #from_refinement_assignments )* + } + } + } + + impl #impl_generics ::core::default::Default for #refinement_ident #ty_generics + #where_clause + { + fn default() -> Self { + #refinement_ident { + #( #field_names: Default::default() ),* + } + } + } + + impl #impl_generics #refinement_ident #ty_generics + #where_clause + { + /// Returns `true` if all fields are `Some` + pub fn is_some(&self) -> bool { + #( + if self.#field_names.is_some() { + return true; + } + )* + false + } + } + + #debug_impl + }; + gen.into() +} + +fn is_refineable_field(f: &Field) -> bool { + f.attrs.iter().any(|attr| attr.path.is_ident("refineable")) +} + +fn is_optional_field(f: &Field) -> bool { + if let Type::Path(typepath) = &f.ty { + if typepath.qself.is_none() { + let segments = &typepath.path.segments; + if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { + return true; + } + } + } + false +} + +fn get_wrapper_type(field: &Field, ty: &Type) -> syn::Type { + if is_refineable_field(field) { + let struct_name = if let Type::Path(tp) = ty { + tp.path.segments.last().unwrap().ident.clone() + } else { + panic!("Expected struct type for a refineable field"); + }; + let refinement_struct_name = format_ident!("{}Refinement", struct_name); + let generics = if let Type::Path(tp) = ty { + &tp.path.segments.last().unwrap().arguments + } else { + &syn::PathArguments::None + }; + parse_quote!(#refinement_struct_name #generics) + } else if is_optional_field(field) { + ty.clone() + } else { + parse_quote!(Option<#ty>) + } +} diff --git a/crates/refineable/src/lib.rs b/crates/refineable/src/lib.rs new file mode 100644 index 0000000..93e2e40 --- /dev/null +++ b/crates/refineable/src/lib.rs @@ -0,0 +1,48 @@ +pub use derive_refineable::Refineable; + +pub trait Refineable: Clone { + type Refinement: Refineable + Default; + + fn refine(&mut self, refinement: &Self::Refinement); + fn refined(self, refinement: Self::Refinement) -> Self; + fn from_cascade(cascade: &Cascade) -> Self + where + Self: Default + Sized, + { + Self::default().refined(cascade.merged()) + } +} + +pub struct Cascade(Vec>); + +impl Default for Cascade { + fn default() -> Self { + Self(vec![Some(Default::default())]) + } +} + +#[derive(Copy, Clone)] +pub struct CascadeSlot(usize); + +impl Cascade { + pub fn reserve(&mut self) -> CascadeSlot { + self.0.push(None); + CascadeSlot(self.0.len() - 1) + } + + pub fn base(&mut self) -> &mut S::Refinement { + self.0[0].as_mut().unwrap() + } + + pub fn set(&mut self, slot: CascadeSlot, refinement: Option) { + self.0[slot.0] = refinement + } + + pub fn merged(&self) -> S::Refinement { + let mut merged = self.0[0].clone().unwrap(); + for refinement in self.0.iter().skip(1).flatten() { + merged.refine(refinement); + } + merged + } +} diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml new file mode 100644 index 0000000..ab21209 --- /dev/null +++ b/crates/util/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "util" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +doctest = true + +[features] +test-support = ["tempfile", "git2"] + +[dependencies] +anyhow.workspace = true +collections.workspace = true +dirs = "3.0" +futures.workspace = true +git2 = { workspace = true, optional = true } +globset.workspace = true +lazy_static.workspace = true +log.workspace = true +rand.workspace = true +regex.workspace = true +rust-embed.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +take-until = "0.2.0" +tokio-stream.workspace = true +tempfile = { workspace = true, optional = true } +unicase.workspace = true + +# [target.'cfg(windows)'.dependencies] +# tendril = "0.4.3" + +[dev-dependencies] +git2.workspace = true +tempfile.workspace = true diff --git a/crates/util/src/arc_cow.rs b/crates/util/src/arc_cow.rs new file mode 100644 index 0000000..02ad1fa --- /dev/null +++ b/crates/util/src/arc_cow.rs @@ -0,0 +1,135 @@ +use std::{ + borrow::Cow, + cmp::Ordering, + fmt::{self, Debug}, + hash::{Hash, Hasher}, + sync::Arc, +}; + +pub enum ArcCow<'a, T: ?Sized> { + Borrowed(&'a T), + Owned(Arc), +} + +impl<'a, T: ?Sized + PartialEq> PartialEq for ArcCow<'a, T> { + fn eq(&self, other: &Self) -> bool { + let a = self.as_ref(); + let b = other.as_ref(); + a == b + } +} + +impl<'a, T: ?Sized + PartialOrd> PartialOrd for ArcCow<'a, T> { + fn partial_cmp(&self, other: &Self) -> Option { + self.as_ref().partial_cmp(other.as_ref()) + } +} + +impl<'a, T: ?Sized + Ord> Ord for ArcCow<'a, T> { + fn cmp(&self, other: &Self) -> Ordering { + self.as_ref().cmp(other.as_ref()) + } +} + +impl<'a, T: ?Sized + Eq> Eq for ArcCow<'a, T> {} + +impl<'a, T: ?Sized + Hash> Hash for ArcCow<'a, T> { + fn hash(&self, state: &mut H) { + match self { + Self::Borrowed(borrowed) => Hash::hash(borrowed, state), + Self::Owned(owned) => Hash::hash(&**owned, state), + } + } +} + +impl<'a, T: ?Sized> Clone for ArcCow<'a, T> { + fn clone(&self) -> Self { + match self { + Self::Borrowed(borrowed) => Self::Borrowed(borrowed), + Self::Owned(owned) => Self::Owned(owned.clone()), + } + } +} + +impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> { + fn from(s: &'a T) -> Self { + Self::Borrowed(s) + } +} + +impl From> for ArcCow<'_, T> { + fn from(s: Arc) -> Self { + Self::Owned(s) + } +} + +impl From<&'_ Arc> for ArcCow<'_, T> { + fn from(s: &'_ Arc) -> Self { + Self::Owned(s.clone()) + } +} + +impl From for ArcCow<'_, str> { + fn from(value: String) -> Self { + Self::Owned(value.into()) + } +} + +impl<'a> From> for ArcCow<'a, str> { + fn from(value: Cow<'a, str>) -> Self { + match value { + Cow::Borrowed(borrowed) => Self::Borrowed(borrowed), + Cow::Owned(owned) => Self::Owned(owned.into()), + } + } +} + +impl From> for ArcCow<'_, [T]> { + fn from(vec: Vec) -> Self { + ArcCow::Owned(Arc::from(vec)) + } +} + +impl<'a> From<&'a str> for ArcCow<'a, [u8]> { + fn from(s: &'a str) -> Self { + ArcCow::Borrowed(s.as_bytes()) + } +} + +impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow for ArcCow<'a, T> { + fn borrow(&self) -> &T { + match self { + ArcCow::Borrowed(borrowed) => borrowed, + ArcCow::Owned(owned) => owned.as_ref(), + } + } +} + +impl std::ops::Deref for ArcCow<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + ArcCow::Borrowed(s) => s, + ArcCow::Owned(s) => s.as_ref(), + } + } +} + +impl AsRef for ArcCow<'_, T> { + fn as_ref(&self) -> &T { + match self { + ArcCow::Borrowed(borrowed) => borrowed, + ArcCow::Owned(owned) => owned.as_ref(), + } + } +} + +impl<'a, T: ?Sized + Debug> Debug for ArcCow<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f), + ArcCow::Owned(owned) => Debug::fmt(&**owned, f), + } + } +} diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs new file mode 100644 index 0000000..e221a64 --- /dev/null +++ b/crates/util/src/fs.rs @@ -0,0 +1,34 @@ +use std::path::Path; + +use crate::ResultExt; +use futures::TryStreamExt; +use tokio::fs; +use tokio_stream::{wrappers::ReadDirStream, StreamExt}; + +/// Removes all files and directories matching the given predicate +pub async fn remove_matching(dir: &Path, predicate: F) -> std::io::Result<()> +where + F: Fn(&Path) -> bool, +{ + ReadDirStream::new(fs::read_dir(dir).await?) + .try_filter_map(move |entry| { + let predicate = |e| predicate(e); + async move { + let path = entry.path(); + if predicate(path.as_path()) { + if let Ok(metadata) = fs::metadata(&path).await { + if metadata.is_file() { + fs::remove_file(&path).await?; + } else { + fs::remove_dir_all(&path).await?; + } + } + Ok(Some(())) + } else { + Ok(None) + } + } + }) + .try_collect::<()>() + .await +} diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs new file mode 100644 index 0000000..acb7259 --- /dev/null +++ b/crates/util/src/http.rs @@ -0,0 +1,242 @@ +pub mod github; + +pub use anyhow::{anyhow, Result}; +use futures::future::BoxFuture; +use futures_lite::FutureExt; +use isahc::config::{Configurable, RedirectPolicy}; +pub use isahc::{ + http::{Method, StatusCode, Uri}, + AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response, +}; +#[cfg(feature = "test-support")] +use std::fmt; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; +pub use url::Url; + +fn http_proxy_from_env() -> Option { + macro_rules! try_env { + ($($env:literal),+) => { + $( + if let Ok(env) = std::env::var($env) { + return env.parse::().ok(); + } + )+ + }; + } + + try_env!( + "ALL_PROXY", + "all_proxy", + "HTTPS_PROXY", + "https_proxy", + "HTTP_PROXY", + "http_proxy" + ); + None +} + +/// An [`HttpClient`] that has a base URL. +pub struct HttpClientWithUrl { + base_url: Mutex, + client: Arc, +} + +impl HttpClientWithUrl { + /// Returns a new [`HttpClientWithUrl`] with the given base URL. + pub fn new(base_url: impl Into) -> Self { + Self { + base_url: Mutex::new(base_url.into()), + client: client(), + } + } + + /// Returns the base URL. + pub fn base_url(&self) -> String { + self.base_url + .lock() + .map_or_else(|_| Default::default(), |url| url.clone()) + } + + /// Sets the base URL. + pub fn set_base_url(&self, base_url: impl Into) { + let base_url = base_url.into(); + self.base_url + .lock() + .map(|mut url| { + *url = base_url; + }) + .ok(); + } + + /// Builds a URL using the given path. + pub fn build_url(&self, path: &str) -> String { + format!("{}{}", self.base_url(), path) + } + + /// Builds a Zed API URL using the given path. + pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result { + let base_url = self.base_url(); + let base_api_url = match base_url.as_ref() { + "https://zed.dev" => "https://api.zed.dev", + "https://staging.zed.dev" => "https://api-staging.zed.dev", + "http://localhost:3000" => "http://localhost:8080", + other => other, + }; + + Ok(Url::parse_with_params( + &format!("{}{}", base_api_url, path), + query, + )?) + } +} + +impl HttpClient for Arc { + fn send( + &self, + req: Request, + ) -> BoxFuture<'static, Result, Error>> { + self.client.send(req) + } +} + +impl HttpClient for HttpClientWithUrl { + fn send( + &self, + req: Request, + ) -> BoxFuture<'static, Result, Error>> { + self.client.send(req) + } +} + +pub trait HttpClient: Send + Sync { + fn send( + &self, + req: Request, + ) -> BoxFuture<'static, Result, Error>>; + + fn get<'a>( + &'a self, + uri: &str, + body: AsyncBody, + follow_redirects: bool, + ) -> BoxFuture<'a, Result, Error>> { + let request = isahc::Request::builder() + .redirect_policy(if follow_redirects { + RedirectPolicy::Follow + } else { + RedirectPolicy::None + }) + .method(Method::GET) + .uri(uri) + .body(body); + match request { + Ok(request) => self.send(request), + Err(error) => async move { Err(error.into()) }.boxed(), + } + } + + fn post_json<'a>( + &'a self, + uri: &str, + body: AsyncBody, + ) -> BoxFuture<'a, Result, Error>> { + let request = isahc::Request::builder() + .method(Method::POST) + .uri(uri) + .header("Content-Type", "application/json") + .body(body); + match request { + Ok(request) => self.send(request), + Err(error) => async move { Err(error.into()) }.boxed(), + } + } +} + +pub fn client() -> Arc { + Arc::new( + isahc::HttpClient::builder() + .connect_timeout(Duration::from_secs(5)) + .low_speed_timeout(100, Duration::from_secs(5)) + .proxy(http_proxy_from_env()) + .build() + .unwrap(), + ) +} + +impl HttpClient for isahc::HttpClient { + fn send( + &self, + req: Request, + ) -> BoxFuture<'static, Result, Error>> { + let client = self.clone(); + Box::pin(async move { client.send_async(req).await }) + } +} + +#[cfg(feature = "test-support")] +type FakeHttpHandler = Box< + dyn Fn(Request) -> BoxFuture<'static, Result, Error>> + + Send + + Sync + + 'static, +>; + +#[cfg(feature = "test-support")] +pub struct FakeHttpClient { + handler: FakeHttpHandler, +} + +#[cfg(feature = "test-support")] +impl FakeHttpClient { + pub fn create(handler: F) -> Arc + where + Fut: futures::Future, Error>> + Send + 'static, + F: Fn(Request) -> Fut + Send + Sync + 'static, + { + Arc::new(HttpClientWithUrl { + base_url: Mutex::new("http://test.example".into()), + client: Arc::new(Self { + handler: Box::new(move |req| Box::pin(handler(req))), + }), + }) + } + + pub fn with_404_response() -> Arc { + Self::create(|_| async move { + Ok(Response::builder() + .status(404) + .body(Default::default()) + .unwrap()) + }) + } + + pub fn with_200_response() -> Arc { + Self::create(|_| async move { + Ok(Response::builder() + .status(200) + .body(Default::default()) + .unwrap()) + }) + } +} + +#[cfg(feature = "test-support")] +impl fmt::Debug for FakeHttpClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FakeHttpClient").finish() + } +} + +#[cfg(feature = "test-support")] +impl HttpClient for FakeHttpClient { + fn send( + &self, + req: Request, + ) -> BoxFuture<'static, Result, Error>> { + let future = (self.handler)(req); + Box::pin(async move { future.await.map(Into::into) }) + } +} diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs new file mode 100644 index 0000000..5161068 --- /dev/null +++ b/crates/util/src/lib.rs @@ -0,0 +1,711 @@ +pub mod arc_cow; +pub mod fs; +pub mod paths; +pub mod serde; +pub mod semver; +pub mod sum_tree; +#[cfg(any(test, feature = "test-support"))] +pub mod test; + +use futures::Future; +use lazy_static::lazy_static; +use rand::{seq::SliceRandom, Rng}; +use std::{ + borrow::Cow, + cmp::{self, Ordering}, + env, + ops::{AddAssign, Range, RangeInclusive}, + panic::Location, + pin::Pin, + task::{Context, Poll}, + time::Instant, +}; +use unicase::UniCase; + +pub use take_until::*; + +#[macro_export] +macro_rules! debug_panic { + ( $($fmt_arg:tt)* ) => { + if cfg!(debug_assertions) { + panic!( $($fmt_arg)* ); + } else { + let backtrace = std::backtrace::Backtrace::capture(); + log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace); + } + }; +} + +pub fn truncate(s: &str, max_chars: usize) -> &str { + match s.char_indices().nth(max_chars) { + None => s, + Some((idx, _)) => &s[..idx], + } +} + +/// Removes characters from the end of the string if its length is greater than `max_chars` and +/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars. +pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { + debug_assert!(max_chars >= 5); + + let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars); + match truncation_ix { + Some(length) => s[..length].to_string() + "…", + None => s.to_string(), + } +} + +/// Removes characters from the front of the string if its length is greater than `max_chars` and +/// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars. +pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String { + debug_assert!(max_chars >= 5); + + let truncation_ix = s.char_indices().map(|(i, _)| i).nth_back(max_chars); + match truncation_ix { + Some(length) => "…".to_string() + &s[length..], + None => s.to_string(), + } +} + +/// Takes only `max_lines` from the string and, if there were more than `max_lines-1`, appends a +/// a newline and "..." to the string, so that `max_lines` are returned. +/// Returns string unchanged if its length is smaller than max_lines. +pub fn truncate_lines_and_trailoff(s: &str, max_lines: usize) -> String { + let mut lines = s.lines().take(max_lines).collect::>(); + if lines.len() > max_lines - 1 { + lines.pop(); + lines.join("\n") + "\n…" + } else { + lines.join("\n") + } +} + +pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { + let prev = *value; + *value += T::from(1); + prev +} + +/// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and +/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this, +/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator. +pub fn extend_sorted(vec: &mut Vec, new_items: I, limit: usize, mut cmp: F) +where + I: IntoIterator, + F: FnMut(&T, &T) -> Ordering, +{ + let mut start_index = 0; + for new_item in new_items { + if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) { + let index = start_index + i; + if vec.len() < limit { + vec.insert(index, new_item); + } else if index < vec.len() { + vec.pop(); + vec.insert(index, new_item); + } + start_index = index; + } + } +} + +/// Parse the result of calling `usr/bin/env` with no arguments +pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) { + let mut current_key: Option = None; + let mut current_value: Option = None; + + for line in env.split_terminator('\n') { + if let Some(separator_index) = line.find('=') { + if &line[..separator_index] != "" { + if let Some((key, value)) = Option::zip(current_key.take(), current_value.take()) { + f(key, value) + } + current_key = Some(line[..separator_index].to_string()); + current_value = Some(line[separator_index + 1..].to_string()); + continue; + }; + } + if let Some(value) = current_value.as_mut() { + value.push('\n'); + value.push_str(line); + } + } + if let Some((key, value)) = Option::zip(current_key.take(), current_value.take()) { + f(key, value) + } +} + +pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { + use serde_json::Value; + + match (source, target) { + (Value::Object(source), Value::Object(target)) => { + for (key, value) in source { + if let Some(target) = target.get_mut(&key) { + merge_json_value_into(value, target); + } else { + target.insert(key.clone(), value); + } + } + } + + (source, target) => *target = source, + } +} + +pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { + use serde_json::Value; + if let Value::Object(source_object) = source { + let target_object = if let Value::Object(target) = target { + target + } else { + *target = Value::Object(Default::default()); + target.as_object_mut().unwrap() + }; + for (key, value) in source_object { + if let Some(target) = target_object.get_mut(&key) { + merge_non_null_json_value_into(value, target); + } else if !value.is_null() { + target_object.insert(key.clone(), value); + } + } + } else if !source.is_null() { + *target = source + } +} + +pub fn measure(label: &str, f: impl FnOnce() -> R) -> R { + lazy_static! { + pub static ref ZED_MEASUREMENTS: bool = env::var("ZED_MEASUREMENTS") + .map(|measurements| measurements == "1" || measurements == "true") + .unwrap_or(false); + } + + if *ZED_MEASUREMENTS { + let start = Instant::now(); + let result = f(); + let elapsed = start.elapsed(); + eprintln!("{}: {:?}", label, elapsed); + result + } else { + f() + } +} + +pub trait ResultExt { + type Ok; + + fn log_err(self) -> Option; + /// Assert that this result should never be an error in development or tests. + fn debug_assert_ok(self, reason: &str) -> Self; + fn warn_on_err(self) -> Option; + fn inspect_error(self, func: impl FnOnce(&E)) -> Self; +} + +impl ResultExt for Result +where + E: std::fmt::Debug, +{ + type Ok = T; + + #[track_caller] + fn log_err(self) -> Option { + match self { + Ok(value) => Some(value), + Err(error) => { + let caller = Location::caller(); + log::error!("{}:{}: {:?}", caller.file(), caller.line(), error); + None + } + } + } + + #[track_caller] + fn debug_assert_ok(self, reason: &str) -> Self { + if let Err(error) = &self { + debug_panic!("{reason} - {error:?}"); + } + self + } + + fn warn_on_err(self) -> Option { + match self { + Ok(value) => Some(value), + Err(error) => { + log::warn!("{:?}", error); + None + } + } + } + + /// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err + fn inspect_error(self, func: impl FnOnce(&E)) -> Self { + if let Err(err) = &self { + func(err); + } + + self + } +} + +pub trait TryFutureExt { + fn log_err(self) -> LogErrorFuture + where + Self: Sized; + + fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture + where + Self: Sized; + + fn warn_on_err(self) -> LogErrorFuture + where + Self: Sized; + fn unwrap(self) -> UnwrapFuture + where + Self: Sized; +} + +impl TryFutureExt for F +where + F: Future>, + E: std::fmt::Debug, +{ + #[track_caller] + fn log_err(self) -> LogErrorFuture + where + Self: Sized, + { + let location = Location::caller(); + LogErrorFuture(self, log::Level::Error, *location) + } + + fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture + where + Self: Sized, + { + LogErrorFuture(self, log::Level::Error, location) + } + + #[track_caller] + fn warn_on_err(self) -> LogErrorFuture + where + Self: Sized, + { + let location = Location::caller(); + LogErrorFuture(self, log::Level::Warn, *location) + } + + fn unwrap(self) -> UnwrapFuture + where + Self: Sized, + { + UnwrapFuture(self) + } +} + +#[must_use] +pub struct LogErrorFuture(F, log::Level, core::panic::Location<'static>); + +impl Future for LogErrorFuture +where + F: Future>, + E: std::fmt::Debug, +{ + type Output = Option; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let level = self.1; + let location = self.2; + let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; + match inner.poll(cx) { + Poll::Ready(output) => Poll::Ready(match output { + Ok(output) => Some(output), + Err(error) => { + log::log!( + level, + "{}:{}: {:?}", + location.file(), + location.line(), + error + ); + None + } + }), + Poll::Pending => Poll::Pending, + } + } +} + +pub struct UnwrapFuture(F); + +impl Future for UnwrapFuture +where + F: Future>, + E: std::fmt::Debug, +{ + type Output = T; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; + match inner.poll(cx) { + Poll::Ready(result) => Poll::Ready(result.unwrap()), + Poll::Pending => Poll::Pending, + } + } +} + +pub struct Deferred(Option); + +impl Deferred { + /// Drop without running the deferred function. + pub fn abort(mut self) { + self.0.take(); + } +} + +impl Drop for Deferred { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f() + } + } +} + +/// Run the given function when the returned value is dropped (unless it's cancelled). +#[must_use] +pub fn defer(f: F) -> Deferred { + Deferred(Some(f)) +} + +pub struct RandomCharIter { + rng: T, + simple_text: bool, +} + +impl RandomCharIter { + pub fn new(rng: T) -> Self { + Self { + rng, + simple_text: std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()), + } + } + + pub fn with_simple_text(mut self) -> Self { + self.simple_text = true; + self + } +} + +impl Iterator for RandomCharIter { + type Item = char; + + fn next(&mut self) -> Option { + if self.simple_text { + return if self.rng.gen_range(0..100) < 5 { + Some('\n') + } else { + Some(self.rng.gen_range(b'a'..b'z' + 1).into()) + }; + } + + match self.rng.gen_range(0..100) { + // whitespace + 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(), + // two-byte greek letters + 20..=32 => char::from_u32(self.rng.gen_range(('α' as u32)..('ω' as u32 + 1))), + // // three-byte characters + 33..=45 => ['✋', '✅', '❌', '❎', '⭐'] + .choose(&mut self.rng) + .copied(), + // // four-byte characters + 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(), + // ascii letters + _ => Some(self.rng.gen_range(b'a'..b'z' + 1).into()), + } + } +} + +/// Get an embedded file as a string. +pub fn asset_str(path: &str) -> Cow<'static, str> { + match A::get(path).unwrap().data { + Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()), + Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()), + } +} + +// copy unstable standard feature option unzip +// https://github.com/rust-lang/rust/issues/87800 +// Remove when this ship in Rust 1.66 or 1.67 +pub fn unzip_option(option: Option<(T, U)>) -> (Option, Option) { + match option { + Some((a, b)) => (Some(a), Some(b)), + None => (None, None), + } +} + +/// Expands to an immediately-invoked function expression. Good for using the ? operator +/// in functions which do not return an Option or Result. +/// +/// Accepts a normal block, an async block, or an async move block. +#[macro_export] +macro_rules! maybe { + ($block:block) => { + (|| $block)() + }; + (async $block:block) => { + (|| async $block)() + }; + (async move $block:block) => { + (|| async move $block)() + }; +} + +pub trait RangeExt { + fn sorted(&self) -> Self; + fn to_inclusive(&self) -> RangeInclusive; + fn overlaps(&self, other: &Range) -> bool; + fn contains_inclusive(&self, other: &Range) -> bool; +} + +impl RangeExt for Range { + fn sorted(&self) -> Self { + cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() + } + + fn to_inclusive(&self) -> RangeInclusive { + self.start.clone()..=self.end.clone() + } + + fn overlaps(&self, other: &Range) -> bool { + self.start < other.end && other.start < self.end + } + + fn contains_inclusive(&self, other: &Range) -> bool { + self.start <= other.start && other.end <= self.end + } +} + +impl RangeExt for RangeInclusive { + fn sorted(&self) -> Self { + cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone() + } + + fn to_inclusive(&self) -> RangeInclusive { + self.clone() + } + + fn overlaps(&self, other: &Range) -> bool { + self.start() < &other.end && &other.start <= self.end() + } + + fn contains_inclusive(&self, other: &Range) -> bool { + self.start() <= &other.start && &other.end <= self.end() + } +} + +/// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one, +/// case-insensitive. +/// +/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc` +/// into `1-abc, 2, 10, 11-def, .., 21-abc` +#[derive(Debug, PartialEq, Eq)] +pub struct NumericPrefixWithSuffix<'a>(i32, &'a str); + +impl<'a> NumericPrefixWithSuffix<'a> { + pub fn from_numeric_prefixed_str(str: &'a str) -> Option { + let i = str.chars().take_while(|c| c.is_ascii_digit()).count(); + let (prefix, remainder) = str.split_at(i); + + match prefix.parse::() { + Ok(prefix) => Some(NumericPrefixWithSuffix(prefix, remainder)), + Err(_) => None, + } + } +} + +impl Ord for NumericPrefixWithSuffix<'_> { + fn cmp(&self, other: &Self) -> Ordering { + let NumericPrefixWithSuffix(num_a, remainder_a) = self; + let NumericPrefixWithSuffix(num_b, remainder_b) = other; + num_a + .cmp(num_b) + .then_with(|| UniCase::new(remainder_a).cmp(&UniCase::new(remainder_b))) + } +} + +impl<'a> PartialOrd for NumericPrefixWithSuffix<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +lazy_static! { + static ref EMOJI_REGEX: regex::Regex = regex::Regex::new("(\\p{Emoji}|\u{200D})").unwrap(); +} + +/// Returns true if the given string consists of emojis only. +/// E.g. "👨‍👩‍👧‍👧👋" will return true, but "👋!" will return false. +pub fn word_consists_of_emojis(s: &str) -> bool { + let mut prev_end = 0; + for capture in EMOJI_REGEX.find_iter(s) { + if capture.start() != prev_end { + return false; + } + prev_end = capture.end(); + } + prev_end == s.len() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extend_sorted() { + let mut vec = vec![]; + + extend_sorted(&mut vec, vec![21, 17, 13, 8, 1, 0], 5, |a, b| b.cmp(a)); + assert_eq!(vec, &[21, 17, 13, 8, 1]); + + extend_sorted(&mut vec, vec![101, 19, 17, 8, 2], 8, |a, b| b.cmp(a)); + assert_eq!(vec, &[101, 21, 19, 17, 13, 8, 2, 1]); + + extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a)); + assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]); + } + + #[test] + fn test_iife() { + fn option_returning_function() -> Option<()> { + None + } + + let foo = maybe!({ + option_returning_function()?; + Some(()) + }); + + assert_eq!(foo, None); + } + + #[test] + fn test_trancate_and_trailoff() { + assert_eq!(truncate_and_trailoff("", 5), ""); + assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè"); + assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè"); + assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…"); + } + + #[test] + fn test_numeric_prefix_str_method() { + let target = "1a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(1, "a")) + ); + + let target = "12ab"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(12, "ab")) + ); + + let target = "12_ab"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(12, "_ab")) + ); + + let target = "1_2ab"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(1, "_2ab")) + ); + + let target = "1.2"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(1, ".2")) + ); + + let target = "1.2_a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(1, ".2_a")) + ); + + let target = "12.2_a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(12, ".2_a")) + ); + + let target = "12a.2_a"; + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(target), + Some(NumericPrefixWithSuffix(12, "a.2_a")) + ); + } + + #[test] + fn test_numeric_prefix_with_suffix() { + let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"]; + sorted.sort_by_key(|s| { + NumericPrefixWithSuffix::from_numeric_prefixed_str(s).unwrap_or_else(|| { + panic!("Cannot convert string `{s}` into NumericPrefixWithSuffix") + }) + }); + assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]); + + for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] { + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less), + None, + "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix" + ) + } + } + + #[test] + fn test_word_consists_of_emojis() { + let words_to_test = vec![ + ("👨‍👩‍👧‍👧👋🥒", true), + ("👋", true), + ("!👋", false), + ("👋!", false), + ("👋 ", false), + (" 👋", false), + ("Test", false), + ]; + + for (text, expected_result) in words_to_test { + assert_eq!(word_consists_of_emojis(text), expected_result); + } + } + + #[test] + fn test_truncate_lines_and_trailoff() { + let text = r#"Line 1 +Line 2 +Line 3"#; + + assert_eq!( + truncate_lines_and_trailoff(text, 2), + r#"Line 1 +…"# + ); + + assert_eq!( + truncate_lines_and_trailoff(text, 3), + r#"Line 1 +Line 2 +…"# + ); + + assert_eq!( + truncate_lines_and_trailoff(text, 4), + r#"Line 1 +Line 2 +Line 3"# + ); + } +} diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs new file mode 100644 index 0000000..e43c60a --- /dev/null +++ b/crates/util/src/paths.rs @@ -0,0 +1,622 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use globset::{Glob, GlobMatcher}; +use serde::{Deserialize, Serialize}; + +lazy_static::lazy_static! { + pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); + pub static ref CONFIG_DIR: PathBuf = if cfg!(target_os = "windows") { + dirs::config_dir() + .expect("failed to determine RoamingAppData directory") + .join("Zed") + } else if cfg!(target_os = "linux") { + dirs::config_dir() + .expect("failed to determine XDG_CONFIG_HOME directory") + .join("zed") + } else { + HOME.join(".config").join("zed") + }; + pub static ref CONVERSATIONS_DIR: PathBuf = if cfg!(target_os = "macos") { + CONFIG_DIR.join("conversations") + } else { + SUPPORT_DIR.join("conversations") + }; + pub static ref EMBEDDINGS_DIR: PathBuf = if cfg!(target_os = "macos") { + CONFIG_DIR.join("embeddings") + } else { + SUPPORT_DIR.join("embeddings") + }; + pub static ref THEMES_DIR: PathBuf = CONFIG_DIR.join("themes"); + + pub static ref SUPPORT_DIR: PathBuf = if cfg!(target_os = "macos") { + HOME.join("Library/Application Support/Zed") + } else if cfg!(target_os = "linux") { + dirs::data_local_dir() + .expect("failed to determine XDG_DATA_DIR directory") + .join("zed") + } else if cfg!(target_os = "windows") { + dirs::data_local_dir() + .expect("failed to determine LocalAppData directory") + .join("Zed") + } else { + CONFIG_DIR.clone() + }; + pub static ref LOGS_DIR: PathBuf = if cfg!(target_os = "macos") { + HOME.join("Library/Logs/Zed") + } else { + SUPPORT_DIR.join("logs") + }; + pub static ref EXTENSIONS_DIR: PathBuf = SUPPORT_DIR.join("extensions"); + pub static ref LANGUAGES_DIR: PathBuf = SUPPORT_DIR.join("languages"); + pub static ref COPILOT_DIR: PathBuf = SUPPORT_DIR.join("copilot"); + pub static ref SUPERMAVEN_DIR: PathBuf = SUPPORT_DIR.join("supermaven"); + pub static ref DEFAULT_PRETTIER_DIR: PathBuf = SUPPORT_DIR.join("prettier"); + pub static ref DB_DIR: PathBuf = SUPPORT_DIR.join("db"); + pub static ref CRASHES_DIR: Option = cfg!(target_os = "macos") + .then_some(HOME.join("Library/Logs/DiagnosticReports")); + pub static ref CRASHES_RETIRED_DIR: Option = CRASHES_DIR + .as_ref() + .map(|dir| dir.join("Retired")); + + pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); + pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); + pub static ref TASKS: PathBuf = CONFIG_DIR.join("tasks.json"); + pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); + pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); + pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); + pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json"); + pub static ref LOCAL_TASKS_RELATIVE_PATH: &'static Path = Path::new(".zed/tasks.json"); + pub static ref LOCAL_VSCODE_TASKS_RELATIVE_PATH: &'static Path = Path::new(".vscode/tasks.json"); + pub static ref TEMP_DIR: PathBuf = if cfg!(target_os = "windows") { + dirs::cache_dir() + .expect("failed to determine LocalAppData directory") + .join("Zed") + } else if cfg!(target_os = "linux") { + dirs::cache_dir() + .expect("failed to determine XDG_CACHE_HOME directory") + .join("zed") + } else { + HOME.join(".cache").join("zed") + }; +} + +pub trait PathExt { + fn compact(&self) -> PathBuf; + fn icon_stem_or_suffix(&self) -> Option<&str>; + fn extension_or_hidden_file_name(&self) -> Option<&str>; + fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result + where + Self: From<&'a Path>, + { + #[cfg(unix)] + { + use std::os::unix::prelude::OsStrExt; + Ok(Self::from(Path::new(OsStr::from_bytes(bytes)))) + } + #[cfg(windows)] + { + use anyhow::anyhow; + use tendril::fmt::{Format, WTF8}; + WTF8::validate(bytes) + .then(|| { + // Safety: bytes are valid WTF-8 sequence. + Self::from(Path::new(unsafe { + OsStr::from_encoded_bytes_unchecked(bytes) + })) + }) + .ok_or_else(|| anyhow!("Invalid WTF-8 sequence: {bytes:?}")) + } + } +} + +impl> PathExt for T { + /// Compacts a given file path by replacing the user's home directory + /// prefix with a tilde (`~`). + /// + /// # Returns + /// + /// * A `PathBuf` containing the compacted file path. If the input path + /// does not have the user's home directory prefix, or if we are not on + /// Linux or macOS, the original path is returned unchanged. + fn compact(&self) -> PathBuf { + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + match self.as_ref().strip_prefix(HOME.as_path()) { + Ok(relative_path) => { + let mut shortened_path = PathBuf::new(); + shortened_path.push("~"); + shortened_path.push(relative_path); + shortened_path + } + Err(_) => self.as_ref().to_path_buf(), + } + } else { + self.as_ref().to_path_buf() + } + } + + /// Returns either the suffix if available, or the file stem otherwise to determine which file icon to use + fn icon_stem_or_suffix(&self) -> Option<&str> { + let path = self.as_ref(); + let file_name = path.file_name()?.to_str()?; + if file_name.starts_with('.') { + return file_name.strip_prefix('.'); + } + + path.extension() + .and_then(|e| e.to_str()) + .or_else(|| path.file_stem()?.to_str()) + } + + /// Returns a file's extension or, if the file is hidden, its name without the leading dot + fn extension_or_hidden_file_name(&self) -> Option<&str> { + if let Some(extension) = self.as_ref().extension() { + return extension.to_str(); + } + + self.as_ref().file_name()?.to_str()?.split('.').last() + } +} + +/// A delimiter to use in `path_query:row_number:column_number` strings parsing. +pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; + +/// A representation of a path-like string with optional row and column numbers. +/// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct PathLikeWithPosition

{ + pub path_like: P, + pub row: Option, + // Absent if row is absent. + pub column: Option, +} + +impl

PathLikeWithPosition

{ + /// Parses a string that possibly has `:row:column` suffix. + /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`. + /// If any of the row/column component parsing fails, the whole string is then parsed as a path like. + pub fn parse_str( + s: &str, + parse_path_like_str: impl Fn(&str) -> Result, + ) -> Result { + let fallback = |fallback_str| { + Ok(Self { + path_like: parse_path_like_str(fallback_str)?, + row: None, + column: None, + }) + }; + + let trimmed = s.trim(); + + #[cfg(target_os = "windows")] + { + let is_absolute = trimmed.starts_with(r"\\?\"); + if is_absolute { + return Self::parse_absolute_path(trimmed, parse_path_like_str); + } + } + + match trimmed.split_once(FILE_ROW_COLUMN_DELIMITER) { + Some((path_like_str, maybe_row_and_col_str)) => { + let path_like_str = path_like_str.trim(); + let maybe_row_and_col_str = maybe_row_and_col_str.trim(); + if path_like_str.is_empty() { + fallback(s) + } else if maybe_row_and_col_str.is_empty() { + fallback(path_like_str) + } else { + let (row_parse_result, maybe_col_str) = + match maybe_row_and_col_str.split_once(FILE_ROW_COLUMN_DELIMITER) { + Some((maybe_row_str, maybe_col_str)) => { + (maybe_row_str.parse::(), maybe_col_str.trim()) + } + None => (maybe_row_and_col_str.parse::(), ""), + }; + + match row_parse_result { + Ok(row) => { + if maybe_col_str.is_empty() { + Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: Some(row), + column: None, + }) + } else { + let (maybe_col_str, _) = + maybe_col_str.split_once(':').unwrap_or((maybe_col_str, "")); + match maybe_col_str.parse::() { + Ok(col) => Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: Some(row), + column: Some(col), + }), + Err(_) => Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: Some(row), + column: None, + }), + } + } + } + Err(_) => Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: None, + column: None, + }), + } + } + } + None => fallback(s), + } + } + + /// This helper function is used for parsing absolute paths on Windows. It exists because absolute paths on Windows are quite different from other platforms. See [this page](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths) for more information. + #[cfg(target_os = "windows")] + fn parse_absolute_path( + s: &str, + parse_path_like_str: impl Fn(&str) -> Result, + ) -> Result { + let fallback = |fallback_str| { + Ok(Self { + path_like: parse_path_like_str(fallback_str)?, + row: None, + column: None, + }) + }; + + let mut iterator = s.split(FILE_ROW_COLUMN_DELIMITER); + + let drive_prefix = iterator.next().unwrap_or_default(); + let file_path = iterator.next().unwrap_or_default(); + + // TODO: How to handle drives without a letter? UNC paths? + let complete_path = drive_prefix.replace("\\\\?\\", "") + ":" + &file_path; + + if let Some(row_str) = iterator.next() { + if let Some(column_str) = iterator.next() { + match row_str.parse::() { + Ok(row) => match column_str.parse::() { + Ok(col) => { + return Ok(Self { + path_like: parse_path_like_str(&complete_path)?, + row: Some(row), + column: Some(col), + }); + } + + Err(_) => { + return Ok(Self { + path_like: parse_path_like_str(&complete_path)?, + row: Some(row), + column: None, + }); + } + }, + + Err(_) => { + return fallback(&complete_path); + } + } + } + } + return fallback(&complete_path); + } + + pub fn map_path_like( + self, + mapping: impl FnOnce(P) -> Result, + ) -> Result, E> { + Ok(PathLikeWithPosition { + path_like: mapping(self.path_like)?, + row: self.row, + column: self.column, + }) + } + + pub fn to_string(&self, path_like_to_string: impl Fn(&P) -> String) -> String { + let path_like_string = path_like_to_string(&self.path_like); + if let Some(row) = self.row { + if let Some(column) = self.column { + format!("{path_like_string}:{row}:{column}") + } else { + format!("{path_like_string}:{row}") + } + } else { + path_like_string + } + } +} + +#[derive(Clone, Debug)] +pub struct PathMatcher { + maybe_path: PathBuf, + glob: GlobMatcher, +} + +impl std::fmt::Display for PathMatcher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.maybe_path.to_string_lossy().fmt(f) + } +} + +impl PartialEq for PathMatcher { + fn eq(&self, other: &Self) -> bool { + self.maybe_path.eq(&other.maybe_path) + } +} + +impl Eq for PathMatcher {} + +impl PathMatcher { + pub fn new(maybe_glob: &str) -> Result { + Ok(PathMatcher { + glob: Glob::new(maybe_glob)?.compile_matcher(), + maybe_path: PathBuf::from(maybe_glob), + }) + } + + pub fn is_match>(&self, other: P) -> bool { + let other_path = other.as_ref(); + other_path.starts_with(&self.maybe_path) + || other_path.ends_with(&self.maybe_path) + || self.glob.is_match(other_path) + || self.check_with_end_separator(other_path) + } + + fn check_with_end_separator(&self, path: &Path) -> bool { + let path_str = path.to_string_lossy(); + let separator = std::path::MAIN_SEPARATOR_STR; + if path_str.ends_with(separator) { + self.glob.is_match(path) + } else { + self.glob.is_match(path_str.to_string() + separator) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type TestPath = PathLikeWithPosition; + + fn parse_str(s: &str) -> TestPath { + TestPath::parse_str(s, |s| Ok::<_, std::convert::Infallible>(s.to_string())) + .expect("infallible") + } + + #[test] + fn path_with_position_parsing_positive() { + let input_and_expected = [ + ( + "test_file.rs", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: None, + column: None, + }, + ), + ( + "test_file.rs:1", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: None, + }, + ), + ( + "test_file.rs:1:2", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: Some(2), + }, + ), + ]; + + for (input, expected) in input_and_expected { + let actual = parse_str(input); + assert_eq!( + actual, expected, + "For positive case input str '{input}', got a parse mismatch" + ); + } + } + + #[test] + fn path_with_position_parsing_negative() { + for (input, row, column) in [ + ("test_file.rs:a", None, None), + ("test_file.rs:a:b", None, None), + ("test_file.rs::", None, None), + ("test_file.rs::1", None, None), + ("test_file.rs:1::", Some(1), None), + ("test_file.rs::1:2", None, None), + ("test_file.rs:1::2", Some(1), None), + ("test_file.rs:1:2:3", Some(1), Some(2)), + ] { + let actual = parse_str(input); + assert_eq!( + actual, + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row, + column, + }, + "For negative case input str '{input}', got a parse mismatch" + ); + } + } + + // Trim off trailing `:`s for otherwise valid input. + #[test] + fn path_with_position_parsing_special() { + #[cfg(not(target_os = "windows"))] + let input_and_expected = [ + ( + "test_file.rs:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: None, + column: None, + }, + ), + ( + "test_file.rs:1:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: None, + }, + ), + ( + "crates/file_finder/src/file_finder.rs:1902:13:", + PathLikeWithPosition { + path_like: "crates/file_finder/src/file_finder.rs".to_string(), + row: Some(1902), + column: Some(13), + }, + ), + ]; + + #[cfg(target_os = "windows")] + let input_and_expected = [ + ( + "test_file.rs:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: None, + column: None, + }, + ), + ( + "test_file.rs:1:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: None, + }, + ), + ( + "\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:", + PathLikeWithPosition { + path_like: "C:\\Users\\someone\\test_file.rs".to_string(), + row: Some(1902), + column: Some(13), + }, + ), + ( + "\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:", + PathLikeWithPosition { + path_like: "C:\\Users\\someone\\test_file.rs".to_string(), + row: Some(1902), + column: Some(13), + }, + ), + ( + "\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:", + PathLikeWithPosition { + path_like: "C:\\Users\\someone\\test_file.rs".to_string(), + row: Some(1902), + column: None, + }, + ), + ]; + + for (input, expected) in input_and_expected { + let actual = parse_str(input); + assert_eq!( + actual, expected, + "For special case input str '{input}', got a parse mismatch" + ); + } + } + + #[test] + fn test_path_compact() { + let path: PathBuf = [ + HOME.to_string_lossy().to_string(), + "some_file.txt".to_string(), + ] + .iter() + .collect(); + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + assert_eq!(path.compact().to_str(), Some("~/some_file.txt")); + } else { + assert_eq!(path.compact().to_str(), path.to_str()); + } + } + + #[test] + fn test_icon_stem_or_suffix() { + // No dots in name + let path = Path::new("/a/b/c/file_name.rs"); + assert_eq!(path.icon_stem_or_suffix(), Some("rs")); + + // Single dot in name + let path = Path::new("/a/b/c/file.name.rs"); + assert_eq!(path.icon_stem_or_suffix(), Some("rs")); + + // No suffix + let path = Path::new("/a/b/c/file"); + assert_eq!(path.icon_stem_or_suffix(), Some("file")); + + // Multiple dots in name + let path = Path::new("/a/b/c/long.file.name.rs"); + assert_eq!(path.icon_stem_or_suffix(), Some("rs")); + + // Hidden file, no extension + let path = Path::new("/a/b/c/.gitignore"); + assert_eq!(path.icon_stem_or_suffix(), Some("gitignore")); + + // Hidden file, with extension + let path = Path::new("/a/b/c/.eslintrc.js"); + assert_eq!(path.icon_stem_or_suffix(), Some("eslintrc.js")); + } + + #[test] + fn test_extension_or_hidden_file_name() { + // No dots in name + let path = Path::new("/a/b/c/file_name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Single dot in name + let path = Path::new("/a/b/c/file.name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Multiple dots in name + let path = Path::new("/a/b/c/long.file.name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Hidden file, no extension + let path = Path::new("/a/b/c/.gitignore"); + assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore")); + + // Hidden file, with extension + let path = Path::new("/a/b/c/.eslintrc.js"); + assert_eq!(path.extension_or_hidden_file_name(), Some("js")); + } + + #[test] + fn edge_of_glob() { + let path = Path::new("/work/node_modules"); + let path_matcher = PathMatcher::new("**/node_modules/**").unwrap(); + assert!( + path_matcher.is_match(path), + "Path matcher {path_matcher} should match {path:?}" + ); + } + + #[test] + fn project_search() { + let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); + let path_matcher = PathMatcher::new("**/node_modules/**").unwrap(); + assert!( + path_matcher.is_match(path), + "Path matcher {path_matcher} should match {path:?}" + ); + } +} diff --git a/crates/util/src/semver.rs b/crates/util/src/semver.rs new file mode 100644 index 0000000..64ded57 --- /dev/null +++ b/crates/util/src/semver.rs @@ -0,0 +1,97 @@ +//! Constructs for working with [semantic versions](https://semver.org/). + +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +use anyhow::{anyhow, Result}; +use serde::{de::Error, Deserialize, Serialize}; + +/// A [semantic version](https://semver.org/) number. +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] +pub struct SemanticVersion { + major: usize, + minor: usize, + patch: usize, +} + +impl SemanticVersion { + /// Returns a new [`SemanticVersion`] from the given components. + pub const fn new(major: usize, minor: usize, patch: usize) -> Self { + Self { + major, + minor, + patch, + } + } + + /// Returns the major version number. + #[inline(always)] + pub fn major(&self) -> usize { + self.major + } + + /// Returns the minor version number. + #[inline(always)] + pub fn minor(&self) -> usize { + self.minor + } + + /// Returns the patch version number. + #[inline(always)] + pub fn patch(&self) -> usize { + self.patch + } +} + +impl FromStr for SemanticVersion { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut components = s.trim().split('.'); + let major = components + .next() + .ok_or_else(|| anyhow!("missing major version number"))? + .parse()?; + let minor = components + .next() + .ok_or_else(|| anyhow!("missing minor version number"))? + .parse()?; + let patch = components + .next() + .ok_or_else(|| anyhow!("missing patch version number"))? + .parse()?; + Ok(Self { + major, + minor, + patch, + }) + } +} + +impl Display for SemanticVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl Serialize for SemanticVersion { + fn serialize(&self, serializer: S) -> std::prelude::v1::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for SemanticVersion { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + Self::from_str(&string) + .map_err(|_| Error::custom(format!("Invalid version string \"{string}\""))) + } +} diff --git a/crates/util/src/serde.rs b/crates/util/src/serde.rs new file mode 100644 index 0000000..be948c6 --- /dev/null +++ b/crates/util/src/serde.rs @@ -0,0 +1,3 @@ +pub const fn default_true() -> bool { + true +} diff --git a/crates/util/src/sum_tree.rs b/crates/util/src/sum_tree.rs new file mode 100644 index 0000000..6b32a80 --- /dev/null +++ b/crates/util/src/sum_tree.rs @@ -0,0 +1,1339 @@ +mod cursor; +mod tree_map; + +use arrayvec::ArrayVec; +pub use cursor::{Cursor, FilterCursor, Iter}; +use rayon::prelude::*; +use std::marker::PhantomData; +use std::mem; +use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc}; +pub use tree_map::{MapSeekTarget, TreeMap, TreeSet}; + +#[cfg(test)] +pub const TREE_BASE: usize = 2; +#[cfg(not(test))] +pub const TREE_BASE: usize = 6; + +/// An item that can be stored in a [`SumTree`] +/// +/// Must be summarized by a type that implements [`Summary`] +pub trait Item: Clone { + type Summary: Summary; + + fn summary(&self) -> Self::Summary; +} + +/// An [`Item`] whose summary has a specific key that can be used to identify it +pub trait KeyedItem: Item { + type Key: for<'a> Dimension<'a, Self::Summary> + Ord; + + fn key(&self) -> Self::Key; +} + +/// A type that describes the Sum of all [`Item`]s in a subtree of the [`SumTree`] +/// +/// Each Summary type can have multiple [`Dimensions`] that it measures, +/// which can be used to navigate the tree +pub trait Summary: Default + Clone + fmt::Debug { + type Context; + + fn add_summary(&mut self, summary: &Self, cx: &Self::Context); +} + +/// Each [`Summary`] type can have more than one [`Dimension`] type that it measures. +/// +/// You can use dimensions to seek to a specific location in the [`SumTree`] +/// +/// # Example: +/// Zed's rope has a `TextSummary` type that summarizes lines, characters, and bytes. +/// Each of these are different dimensions we may want to seek to +pub trait Dimension<'a, S: Summary>: Clone + fmt::Debug + Default { + fn add_summary(&mut self, _summary: &'a S, _: &S::Context); + + fn from_summary(summary: &'a S, cx: &S::Context) -> Self { + let mut dimension = Self::default(); + dimension.add_summary(summary, cx); + dimension + } +} + +impl<'a, T: Summary> Dimension<'a, T> for T { + fn add_summary(&mut self, summary: &'a T, cx: &T::Context) { + Summary::add_summary(self, summary, cx); + } +} + +pub trait SeekTarget<'a, S: Summary, D: Dimension<'a, S>>: fmt::Debug { + fn cmp(&self, cursor_location: &D, cx: &S::Context) -> Ordering; +} + +impl<'a, S: Summary, D: Dimension<'a, S> + Ord> SeekTarget<'a, S, D> for D { + fn cmp(&self, cursor_location: &Self, _: &S::Context) -> Ordering { + Ord::cmp(self, cursor_location) + } +} + +impl<'a, T: Summary> Dimension<'a, T> for () { + fn add_summary(&mut self, _: &'a T, _: &T::Context) {} +} + +impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>> Dimension<'a, T> for (D1, D2) { + fn add_summary(&mut self, summary: &'a T, cx: &T::Context) { + self.0.add_summary(summary, cx); + self.1.add_summary(summary, cx); + } +} + +impl<'a, S: Summary, D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, D2: Dimension<'a, S>> + SeekTarget<'a, S, (D1, D2)> for D1 +{ + fn cmp(&self, cursor_location: &(D1, D2), cx: &S::Context) -> Ordering { + self.cmp(&cursor_location.0, cx) + } +} + +struct End(PhantomData); + +impl End { + fn new() -> Self { + Self(PhantomData) + } +} + +impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End { + fn cmp(&self, _: &D, _: &S::Context) -> Ordering { + Ordering::Greater + } +} + +impl fmt::Debug for End { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("End").finish() + } +} + +/// Bias is used to settle ambiguities when determining positions in an ordered sequence. +/// +/// The primary use case is for text, where Bias influences +/// which character an offset or anchor is associated with. +/// +/// # Examples +/// Given the buffer `AˇBCD`: +/// - The offset of the cursor is 1 +/// - [Bias::Left] would attach the cursor to the character `A` +/// - [Bias::Right] would attach the cursor to the character `B` +/// +/// Given the buffer `A«BCˇ»D`: +/// - The offset of the cursor is 3, and the selection is from 1 to 3 +/// - The left anchor of the selection has [Bias::Right], attaching it to the character `B` +/// - The right anchor of the selection has [Bias::Left], attaching it to the character `C` +/// +/// Given the buffer `{ˇ<...>`, where `<...>` is a folded region: +/// - The display offset of the cursor is 1, but the offset in the buffer is determined by the bias +/// - [Bias::Left] would attach the cursor to the character `{`, with a buffer offset of 1 +/// - [Bias::Right] would attach the cursor to the first character of the folded region, +/// and the buffer offset would be the offset of the first character of the folded region +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)] +pub enum Bias { + /// Attach to the character on the left + #[default] + Left, + /// Attach to the character on the right + Right, +} + +impl Bias { + pub fn invert(self) -> Self { + match self { + Self::Left => Self::Right, + Self::Right => Self::Left, + } + } +} + +/// A B+ tree in which each leaf node contains `Item`s of type `T` and a `Summary`s for each `Item`. +/// Each internal node contains a `Summary` of the items in its subtree. +/// +/// The maximum number of items per node is `TREE_BASE * 2`. +/// +/// Any [`Dimension`] supported by the [`Summary`] type can be used to seek to a specific location in the tree. +#[derive(Debug, Clone)] +pub struct SumTree(Arc>); + +impl SumTree { + pub fn new() -> Self { + SumTree(Arc::new(Node::Leaf { + summary: T::Summary::default(), + items: ArrayVec::new(), + item_summaries: ArrayVec::new(), + })) + } + + pub fn from_item(item: T, cx: &::Context) -> Self { + let mut tree = Self::new(); + tree.push(item, cx); + tree + } + + pub fn from_iter>( + iter: I, + cx: &::Context, + ) -> Self { + let mut nodes = Vec::new(); + + let mut iter = iter.into_iter().fuse().peekable(); + while iter.peek().is_some() { + let items: ArrayVec = iter.by_ref().take(2 * TREE_BASE).collect(); + let item_summaries: ArrayVec = + items.iter().map(|item| item.summary()).collect(); + + let mut summary = item_summaries[0].clone(); + for item_summary in &item_summaries[1..] { + ::add_summary(&mut summary, item_summary, cx); + } + + nodes.push(Node::Leaf { + summary, + items, + item_summaries, + }); + } + + let mut parent_nodes = Vec::new(); + let mut height = 0; + while nodes.len() > 1 { + height += 1; + let mut current_parent_node = None; + for child_node in nodes.drain(..) { + let parent_node = current_parent_node.get_or_insert_with(|| Node::Internal { + summary: T::Summary::default(), + height, + child_summaries: ArrayVec::new(), + child_trees: ArrayVec::new(), + }); + let Node::Internal { + summary, + child_summaries, + child_trees, + .. + } = parent_node + else { + unreachable!() + }; + let child_summary = child_node.summary(); + ::add_summary(summary, child_summary, cx); + child_summaries.push(child_summary.clone()); + child_trees.push(Self(Arc::new(child_node))); + + if child_trees.len() == 2 * TREE_BASE { + parent_nodes.extend(current_parent_node.take()); + } + } + parent_nodes.extend(current_parent_node.take()); + mem::swap(&mut nodes, &mut parent_nodes); + } + + if nodes.is_empty() { + Self::new() + } else { + debug_assert_eq!(nodes.len(), 1); + Self(Arc::new(nodes.pop().unwrap())) + } + } + + pub fn from_par_iter(iter: I, cx: &::Context) -> Self + where + I: IntoParallelIterator, + Iter: IndexedParallelIterator, + T: Send + Sync, + T::Summary: Send + Sync, + ::Context: Sync, + { + let mut nodes = iter + .into_par_iter() + .chunks(2 * TREE_BASE) + .map(|items| { + let items: ArrayVec = items.into_iter().collect(); + let item_summaries: ArrayVec = + items.iter().map(|item| item.summary()).collect(); + let mut summary = item_summaries[0].clone(); + for item_summary in &item_summaries[1..] { + ::add_summary(&mut summary, item_summary, cx); + } + SumTree(Arc::new(Node::Leaf { + summary, + items, + item_summaries, + })) + }) + .collect::>(); + + let mut height = 0; + while nodes.len() > 1 { + height += 1; + nodes = nodes + .into_par_iter() + .chunks(2 * TREE_BASE) + .map(|child_nodes| { + let child_trees: ArrayVec, { 2 * TREE_BASE }> = + child_nodes.into_iter().collect(); + let child_summaries: ArrayVec = child_trees + .iter() + .map(|child_tree| child_tree.summary().clone()) + .collect(); + let mut summary = child_summaries[0].clone(); + for child_summary in &child_summaries[1..] { + ::add_summary(&mut summary, child_summary, cx); + } + SumTree(Arc::new(Node::Internal { + height, + summary, + child_summaries, + child_trees, + })) + }) + .collect::>(); + } + + if nodes.is_empty() { + Self::new() + } else { + debug_assert_eq!(nodes.len(), 1); + nodes.pop().unwrap() + } + } + + #[allow(unused)] + pub fn items(&self, cx: &::Context) -> Vec { + let mut items = Vec::new(); + let mut cursor = self.cursor::<()>(); + cursor.next(cx); + while let Some(item) = cursor.item() { + items.push(item.clone()); + cursor.next(cx); + } + items + } + + pub fn iter(&self) -> Iter { + Iter::new(self) + } + + pub fn cursor<'a, S>(&'a self) -> Cursor + where + S: Dimension<'a, T::Summary>, + { + Cursor::new(self) + } + + /// Note: If the summary type requires a non `()` context, then the filter cursor + /// that is returned cannot be used with Rust's iterators. + pub fn filter<'a, F, U>(&'a self, filter_node: F) -> FilterCursor + where + F: FnMut(&T::Summary) -> bool, + U: Dimension<'a, T::Summary>, + { + FilterCursor::new(self, filter_node) + } + + #[allow(dead_code)] + pub fn first(&self) -> Option<&T> { + self.leftmost_leaf().0.items().first() + } + + pub fn last(&self) -> Option<&T> { + self.rightmost_leaf().0.items().last() + } + + pub fn update_last(&mut self, f: impl FnOnce(&mut T), cx: &::Context) { + self.update_last_recursive(f, cx); + } + + fn update_last_recursive( + &mut self, + f: impl FnOnce(&mut T), + cx: &::Context, + ) -> Option { + match Arc::make_mut(&mut self.0) { + Node::Internal { + summary, + child_summaries, + child_trees, + .. + } => { + let last_summary = child_summaries.last_mut().unwrap(); + let last_child = child_trees.last_mut().unwrap(); + *last_summary = last_child.update_last_recursive(f, cx).unwrap(); + *summary = sum(child_summaries.iter(), cx); + Some(summary.clone()) + } + Node::Leaf { + summary, + items, + item_summaries, + } => { + if let Some((item, item_summary)) = items.last_mut().zip(item_summaries.last_mut()) + { + (f)(item); + *item_summary = item.summary(); + *summary = sum(item_summaries.iter(), cx); + Some(summary.clone()) + } else { + None + } + } + } + } + + pub fn extent<'a, D: Dimension<'a, T::Summary>>( + &'a self, + cx: &::Context, + ) -> D { + let mut extent = D::default(); + match self.0.as_ref() { + Node::Internal { summary, .. } | Node::Leaf { summary, .. } => { + extent.add_summary(summary, cx); + } + } + extent + } + + pub fn summary(&self) -> &T::Summary { + match self.0.as_ref() { + Node::Internal { summary, .. } => summary, + Node::Leaf { summary, .. } => summary, + } + } + + pub fn is_empty(&self) -> bool { + match self.0.as_ref() { + Node::Internal { .. } => false, + Node::Leaf { items, .. } => items.is_empty(), + } + } + + pub fn extend(&mut self, iter: I, cx: &::Context) + where + I: IntoIterator, + { + self.append(Self::from_iter(iter, cx), cx); + } + + pub fn par_extend(&mut self, iter: I, cx: &::Context) + where + I: IntoParallelIterator, + Iter: IndexedParallelIterator, + T: Send + Sync, + T::Summary: Send + Sync, + ::Context: Sync, + { + self.append(Self::from_par_iter(iter, cx), cx); + } + + pub fn push(&mut self, item: T, cx: &::Context) { + let summary = item.summary(); + self.append( + SumTree(Arc::new(Node::Leaf { + summary: summary.clone(), + items: ArrayVec::from_iter(Some(item)), + item_summaries: ArrayVec::from_iter(Some(summary)), + })), + cx, + ); + } + + pub fn append(&mut self, other: Self, cx: &::Context) { + if self.is_empty() { + *self = other; + } else if !other.0.is_leaf() || !other.0.items().is_empty() { + if self.0.height() < other.0.height() { + for tree in other.0.child_trees() { + self.append(tree.clone(), cx); + } + } else if let Some(split_tree) = self.push_tree_recursive(other, cx) { + *self = Self::from_child_trees(self.clone(), split_tree, cx); + } + } + } + + fn push_tree_recursive( + &mut self, + other: SumTree, + cx: &::Context, + ) -> Option> { + match Arc::make_mut(&mut self.0) { + Node::Internal { + height, + summary, + child_summaries, + child_trees, + .. + } => { + let other_node = other.0.clone(); + ::add_summary(summary, other_node.summary(), cx); + + let height_delta = *height - other_node.height(); + let mut summaries_to_append = ArrayVec::::new(); + let mut trees_to_append = ArrayVec::, { 2 * TREE_BASE }>::new(); + if height_delta == 0 { + summaries_to_append.extend(other_node.child_summaries().iter().cloned()); + trees_to_append.extend(other_node.child_trees().iter().cloned()); + } else if height_delta == 1 && !other_node.is_underflowing() { + summaries_to_append.push(other_node.summary().clone()); + trees_to_append.push(other) + } else { + let tree_to_append = child_trees + .last_mut() + .unwrap() + .push_tree_recursive(other, cx); + *child_summaries.last_mut().unwrap() = + child_trees.last().unwrap().0.summary().clone(); + + if let Some(split_tree) = tree_to_append { + summaries_to_append.push(split_tree.0.summary().clone()); + trees_to_append.push(split_tree); + } + } + + let child_count = child_trees.len() + trees_to_append.len(); + if child_count > 2 * TREE_BASE { + let left_summaries: ArrayVec<_, { 2 * TREE_BASE }>; + let right_summaries: ArrayVec<_, { 2 * TREE_BASE }>; + let left_trees; + let right_trees; + + let midpoint = (child_count + child_count % 2) / 2; + { + let mut all_summaries = child_summaries + .iter() + .chain(summaries_to_append.iter()) + .cloned(); + left_summaries = all_summaries.by_ref().take(midpoint).collect(); + right_summaries = all_summaries.collect(); + let mut all_trees = + child_trees.iter().chain(trees_to_append.iter()).cloned(); + left_trees = all_trees.by_ref().take(midpoint).collect(); + right_trees = all_trees.collect(); + } + *summary = sum(left_summaries.iter(), cx); + *child_summaries = left_summaries; + *child_trees = left_trees; + + Some(SumTree(Arc::new(Node::Internal { + height: *height, + summary: sum(right_summaries.iter(), cx), + child_summaries: right_summaries, + child_trees: right_trees, + }))) + } else { + child_summaries.extend(summaries_to_append); + child_trees.extend(trees_to_append); + None + } + } + Node::Leaf { + summary, + items, + item_summaries, + } => { + let other_node = other.0; + + let child_count = items.len() + other_node.items().len(); + if child_count > 2 * TREE_BASE { + let left_items; + let right_items; + let left_summaries; + let right_summaries: ArrayVec; + + let midpoint = (child_count + child_count % 2) / 2; + { + let mut all_items = items.iter().chain(other_node.items().iter()).cloned(); + left_items = all_items.by_ref().take(midpoint).collect(); + right_items = all_items.collect(); + + let mut all_summaries = item_summaries + .iter() + .chain(other_node.child_summaries()) + .cloned(); + left_summaries = all_summaries.by_ref().take(midpoint).collect(); + right_summaries = all_summaries.collect(); + } + *items = left_items; + *item_summaries = left_summaries; + *summary = sum(item_summaries.iter(), cx); + Some(SumTree(Arc::new(Node::Leaf { + items: right_items, + summary: sum(right_summaries.iter(), cx), + item_summaries: right_summaries, + }))) + } else { + ::add_summary(summary, other_node.summary(), cx); + items.extend(other_node.items().iter().cloned()); + item_summaries.extend(other_node.child_summaries().iter().cloned()); + None + } + } + } + } + + fn from_child_trees( + left: SumTree, + right: SumTree, + cx: &::Context, + ) -> Self { + let height = left.0.height() + 1; + let mut child_summaries = ArrayVec::new(); + child_summaries.push(left.0.summary().clone()); + child_summaries.push(right.0.summary().clone()); + let mut child_trees = ArrayVec::new(); + child_trees.push(left); + child_trees.push(right); + SumTree(Arc::new(Node::Internal { + height, + summary: sum(child_summaries.iter(), cx), + child_summaries, + child_trees, + })) + } + + fn leftmost_leaf(&self) -> &Self { + match *self.0 { + Node::Leaf { .. } => self, + Node::Internal { + ref child_trees, .. + } => child_trees.first().unwrap().leftmost_leaf(), + } + } + + fn rightmost_leaf(&self) -> &Self { + match *self.0 { + Node::Leaf { .. } => self, + Node::Internal { + ref child_trees, .. + } => child_trees.last().unwrap().rightmost_leaf(), + } + } + + #[cfg(debug_assertions)] + pub fn _debug_entries(&self) -> Vec<&T> { + self.iter().collect::>() + } +} + +impl PartialEq for SumTree { + fn eq(&self, other: &Self) -> bool { + self.iter().eq(other.iter()) + } +} + +impl Eq for SumTree {} + +impl SumTree { + pub fn insert_or_replace( + &mut self, + item: T, + cx: &::Context, + ) -> Option { + let mut replaced = None; + *self = { + let mut cursor = self.cursor::(); + let mut new_tree = cursor.slice(&item.key(), Bias::Left, cx); + if let Some(cursor_item) = cursor.item() { + if cursor_item.key() == item.key() { + replaced = Some(cursor_item.clone()); + cursor.next(cx); + } + } + new_tree.push(item, cx); + new_tree.append(cursor.suffix(cx), cx); + new_tree + }; + replaced + } + + pub fn remove(&mut self, key: &T::Key, cx: &::Context) -> Option { + let mut removed = None; + *self = { + let mut cursor = self.cursor::(); + let mut new_tree = cursor.slice(key, Bias::Left, cx); + if let Some(item) = cursor.item() { + if item.key() == *key { + removed = Some(item.clone()); + cursor.next(cx); + } + } + new_tree.append(cursor.suffix(cx), cx); + new_tree + }; + removed + } + + pub fn edit( + &mut self, + mut edits: Vec>, + cx: &::Context, + ) -> Vec { + if edits.is_empty() { + return Vec::new(); + } + + let mut removed = Vec::new(); + edits.sort_unstable_by_key(|item| item.key()); + + *self = { + let mut cursor = self.cursor::(); + let mut new_tree = SumTree::new(); + let mut buffered_items = Vec::new(); + + cursor.seek(&T::Key::default(), Bias::Left, cx); + for edit in edits { + let new_key = edit.key(); + let mut old_item = cursor.item(); + + if old_item + .as_ref() + .map_or(false, |old_item| old_item.key() < new_key) + { + new_tree.extend(buffered_items.drain(..), cx); + let slice = cursor.slice(&new_key, Bias::Left, cx); + new_tree.append(slice, cx); + old_item = cursor.item(); + } + + if let Some(old_item) = old_item { + if old_item.key() == new_key { + removed.push(old_item.clone()); + cursor.next(cx); + } + } + + match edit { + Edit::Insert(item) => { + buffered_items.push(item); + } + Edit::Remove(_) => {} + } + } + + new_tree.extend(buffered_items, cx); + new_tree.append(cursor.suffix(cx), cx); + new_tree + }; + + removed + } + + pub fn get(&self, key: &T::Key, cx: &::Context) -> Option<&T> { + let mut cursor = self.cursor::(); + if cursor.seek(key, Bias::Left, cx) { + cursor.item() + } else { + None + } + } +} + +impl Default for SumTree { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Debug)] +pub enum Node { + Internal { + height: u8, + summary: T::Summary, + child_summaries: ArrayVec, + child_trees: ArrayVec, { 2 * TREE_BASE }>, + }, + Leaf { + summary: T::Summary, + items: ArrayVec, + item_summaries: ArrayVec, + }, +} + +impl Node { + fn is_leaf(&self) -> bool { + matches!(self, Node::Leaf { .. }) + } + + fn height(&self) -> u8 { + match self { + Node::Internal { height, .. } => *height, + Node::Leaf { .. } => 0, + } + } + + fn summary(&self) -> &T::Summary { + match self { + Node::Internal { summary, .. } => summary, + Node::Leaf { summary, .. } => summary, + } + } + + fn child_summaries(&self) -> &[T::Summary] { + match self { + Node::Internal { + child_summaries, .. + } => child_summaries.as_slice(), + Node::Leaf { item_summaries, .. } => item_summaries.as_slice(), + } + } + + fn child_trees(&self) -> &ArrayVec, { 2 * TREE_BASE }> { + match self { + Node::Internal { child_trees, .. } => child_trees, + Node::Leaf { .. } => panic!("Leaf nodes have no child trees"), + } + } + + fn items(&self) -> &ArrayVec { + match self { + Node::Leaf { items, .. } => items, + Node::Internal { .. } => panic!("Internal nodes have no items"), + } + } + + fn is_underflowing(&self) -> bool { + match self { + Node::Internal { child_trees, .. } => child_trees.len() < TREE_BASE, + Node::Leaf { items, .. } => items.len() < TREE_BASE, + } + } +} + +#[derive(Debug)] +pub enum Edit { + Insert(T), + Remove(T::Key), +} + +impl Edit { + fn key(&self) -> T::Key { + match self { + Edit::Insert(item) => item.key(), + Edit::Remove(key) => key.clone(), + } + } +} + +fn sum<'a, T, I>(iter: I, cx: &T::Context) -> T +where + T: 'a + Summary, + I: Iterator, +{ + let mut sum = T::default(); + for value in iter { + sum.add_summary(value, cx); + } + sum +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::{distributions, prelude::*}; + use std::cmp; + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + + #[test] + fn test_extend_and_push_tree() { + let mut tree1 = SumTree::new(); + tree1.extend(0..20, &()); + + let mut tree2 = SumTree::new(); + tree2.extend(50..100, &()); + + tree1.append(tree2, &()); + assert_eq!( + tree1.items(&()), + (0..20).chain(50..100).collect::>() + ); + } + + #[test] + fn test_random() { + let mut starting_seed = 0; + if let Ok(value) = std::env::var("SEED") { + starting_seed = value.parse().expect("invalid SEED variable"); + } + let mut num_iterations = 100; + if let Ok(value) = std::env::var("ITERATIONS") { + num_iterations = value.parse().expect("invalid ITERATIONS variable"); + } + let num_operations = std::env::var("OPERATIONS") + .map_or(5, |o| o.parse().expect("invalid OPERATIONS variable")); + + for seed in starting_seed..(starting_seed + num_iterations) { + eprintln!("seed = {}", seed); + let mut rng = StdRng::seed_from_u64(seed); + + let rng = &mut rng; + let mut tree = SumTree::::new(); + let count = rng.gen_range(0..10); + if rng.gen() { + tree.extend(rng.sample_iter(distributions::Standard).take(count), &()); + } else { + let items = rng + .sample_iter(distributions::Standard) + .take(count) + .collect::>(); + tree.par_extend(items, &()); + } + + for _ in 0..num_operations { + let splice_end = rng.gen_range(0..tree.extent::(&()).0 + 1); + let splice_start = rng.gen_range(0..splice_end + 1); + let count = rng.gen_range(0..10); + let tree_end = tree.extent::(&()); + let new_items = rng + .sample_iter(distributions::Standard) + .take(count) + .collect::>(); + + let mut reference_items = tree.items(&()); + reference_items.splice(splice_start..splice_end, new_items.clone()); + + tree = { + let mut cursor = tree.cursor::(); + let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right, &()); + if rng.gen() { + new_tree.extend(new_items, &()); + } else { + new_tree.par_extend(new_items, &()); + } + cursor.seek(&Count(splice_end), Bias::Right, &()); + new_tree.append(cursor.slice(&tree_end, Bias::Right, &()), &()); + new_tree + }; + + assert_eq!(tree.items(&()), reference_items); + assert_eq!( + tree.iter().collect::>(), + tree.cursor::<()>().collect::>() + ); + + log::info!("tree items: {:?}", tree.items(&())); + + let mut filter_cursor = tree.filter::<_, Count>(|summary| summary.contains_even); + let expected_filtered_items = tree + .items(&()) + .into_iter() + .enumerate() + .filter(|(_, item)| (item & 1) == 0) + .collect::>(); + + let mut item_ix = if rng.gen() { + filter_cursor.next(&()); + 0 + } else { + filter_cursor.prev(&()); + expected_filtered_items.len().saturating_sub(1) + }; + while item_ix < expected_filtered_items.len() { + log::info!("filter_cursor, item_ix: {}", item_ix); + let actual_item = filter_cursor.item().unwrap(); + let (reference_index, reference_item) = expected_filtered_items[item_ix]; + assert_eq!(actual_item, &reference_item); + assert_eq!(filter_cursor.start().0, reference_index); + log::info!("next"); + filter_cursor.next(&()); + item_ix += 1; + + while item_ix > 0 && rng.gen_bool(0.2) { + log::info!("prev"); + filter_cursor.prev(&()); + item_ix -= 1; + + if item_ix == 0 && rng.gen_bool(0.2) { + filter_cursor.prev(&()); + assert_eq!(filter_cursor.item(), None); + assert_eq!(filter_cursor.start().0, 0); + filter_cursor.next(&()); + } + } + } + assert_eq!(filter_cursor.item(), None); + + let mut before_start = false; + let mut cursor = tree.cursor::(); + let start_pos = rng.gen_range(0..=reference_items.len()); + cursor.seek(&Count(start_pos), Bias::Right, &()); + let mut pos = rng.gen_range(start_pos..=reference_items.len()); + cursor.seek_forward(&Count(pos), Bias::Right, &()); + + for i in 0..10 { + assert_eq!(cursor.start().0, pos); + + if pos > 0 { + assert_eq!(cursor.prev_item().unwrap(), &reference_items[pos - 1]); + } else { + assert_eq!(cursor.prev_item(), None); + } + + if pos < reference_items.len() && !before_start { + assert_eq!(cursor.item().unwrap(), &reference_items[pos]); + } else { + assert_eq!(cursor.item(), None); + } + + if before_start { + assert_eq!(cursor.next_item(), reference_items.get(0)); + } else if pos + 1 < reference_items.len() { + assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]); + } else { + assert_eq!(cursor.next_item(), None); + } + + if i < 5 { + cursor.next(&()); + if pos < reference_items.len() { + pos += 1; + before_start = false; + } + } else { + cursor.prev(&()); + if pos == 0 { + before_start = true; + } + pos = pos.saturating_sub(1); + } + } + } + + for _ in 0..10 { + let end = rng.gen_range(0..tree.extent::(&()).0 + 1); + let start = rng.gen_range(0..end + 1); + let start_bias = if rng.gen() { Bias::Left } else { Bias::Right }; + let end_bias = if rng.gen() { Bias::Left } else { Bias::Right }; + + let mut cursor = tree.cursor::(); + cursor.seek(&Count(start), start_bias, &()); + let slice = cursor.slice(&Count(end), end_bias, &()); + + cursor.seek(&Count(start), start_bias, &()); + let summary = cursor.summary::<_, Sum>(&Count(end), end_bias, &()); + + assert_eq!(summary.0, slice.summary().sum); + } + } + } + + #[test] + fn test_cursor() { + // Empty tree + let tree = SumTree::::new(); + let mut cursor = tree.cursor::(); + assert_eq!( + cursor.slice(&Count(0), Bias::Right, &()).items(&()), + Vec::::new() + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + cursor.prev(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + cursor.next(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + + // Single-element tree + let mut tree = SumTree::::new(); + tree.extend(vec![1], &()); + let mut cursor = tree.cursor::(); + assert_eq!( + cursor.slice(&Count(0), Bias::Right, &()).items(&()), + Vec::::new() + ); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + + cursor.next(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 1); + + cursor.prev(&()); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 0); + + let mut cursor = tree.cursor::(); + assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 1); + + cursor.seek(&Count(0), Bias::Right, &()); + assert_eq!( + cursor + .slice(&tree.extent::(&()), Bias::Right, &()) + .items(&()), + [1] + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 1); + + // Multiple-element tree + let mut tree = SumTree::new(); + tree.extend(vec![1, 2, 3, 4, 5, 6], &()); + let mut cursor = tree.cursor::(); + + assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]); + assert_eq!(cursor.item(), Some(&3)); + assert_eq!(cursor.prev_item(), Some(&2)); + assert_eq!(cursor.next_item(), Some(&4)); + assert_eq!(cursor.start().sum, 3); + + cursor.next(&()); + assert_eq!(cursor.item(), Some(&4)); + assert_eq!(cursor.prev_item(), Some(&3)); + assert_eq!(cursor.next_item(), Some(&5)); + assert_eq!(cursor.start().sum, 6); + + cursor.next(&()); + assert_eq!(cursor.item(), Some(&5)); + assert_eq!(cursor.prev_item(), Some(&4)); + assert_eq!(cursor.next_item(), Some(&6)); + assert_eq!(cursor.start().sum, 10); + + cursor.next(&()); + assert_eq!(cursor.item(), Some(&6)); + assert_eq!(cursor.prev_item(), Some(&5)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 15); + + cursor.next(&()); + cursor.next(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 21); + + cursor.prev(&()); + assert_eq!(cursor.item(), Some(&6)); + assert_eq!(cursor.prev_item(), Some(&5)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 15); + + cursor.prev(&()); + assert_eq!(cursor.item(), Some(&5)); + assert_eq!(cursor.prev_item(), Some(&4)); + assert_eq!(cursor.next_item(), Some(&6)); + assert_eq!(cursor.start().sum, 10); + + cursor.prev(&()); + assert_eq!(cursor.item(), Some(&4)); + assert_eq!(cursor.prev_item(), Some(&3)); + assert_eq!(cursor.next_item(), Some(&5)); + assert_eq!(cursor.start().sum, 6); + + cursor.prev(&()); + assert_eq!(cursor.item(), Some(&3)); + assert_eq!(cursor.prev_item(), Some(&2)); + assert_eq!(cursor.next_item(), Some(&4)); + assert_eq!(cursor.start().sum, 3); + + cursor.prev(&()); + assert_eq!(cursor.item(), Some(&2)); + assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), Some(&3)); + assert_eq!(cursor.start().sum, 1); + + cursor.prev(&()); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&2)); + assert_eq!(cursor.start().sum, 0); + + cursor.prev(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&1)); + assert_eq!(cursor.start().sum, 0); + + cursor.next(&()); + assert_eq!(cursor.item(), Some(&1)); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&2)); + assert_eq!(cursor.start().sum, 0); + + let mut cursor = tree.cursor::(); + assert_eq!( + cursor + .slice(&tree.extent::(&()), Bias::Right, &()) + .items(&()), + tree.items(&()) + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 21); + + cursor.seek(&Count(3), Bias::Right, &()); + assert_eq!( + cursor + .slice(&tree.extent::(&()), Bias::Right, &()) + .items(&()), + [4, 5, 6] + ); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); + assert_eq!(cursor.start().sum, 21); + + // Seeking can bias left or right + cursor.seek(&Count(1), Bias::Left, &()); + assert_eq!(cursor.item(), Some(&1)); + cursor.seek(&Count(1), Bias::Right, &()); + assert_eq!(cursor.item(), Some(&2)); + + // Slicing without resetting starts from where the cursor is parked at. + cursor.seek(&Count(1), Bias::Right, &()); + assert_eq!( + cursor.slice(&Count(3), Bias::Right, &()).items(&()), + vec![2, 3] + ); + assert_eq!( + cursor.slice(&Count(6), Bias::Left, &()).items(&()), + vec![4, 5] + ); + assert_eq!( + cursor.slice(&Count(6), Bias::Right, &()).items(&()), + vec![6] + ); + } + + #[test] + fn test_edit() { + let mut tree = SumTree::::new(); + + let removed = tree.edit(vec![Edit::Insert(1), Edit::Insert(2), Edit::Insert(0)], &()); + assert_eq!(tree.items(&()), vec![0, 1, 2]); + assert_eq!(removed, Vec::::new()); + assert_eq!(tree.get(&0, &()), Some(&0)); + assert_eq!(tree.get(&1, &()), Some(&1)); + assert_eq!(tree.get(&2, &()), Some(&2)); + assert_eq!(tree.get(&4, &()), None); + + let removed = tree.edit(vec![Edit::Insert(2), Edit::Insert(4), Edit::Remove(0)], &()); + assert_eq!(tree.items(&()), vec![1, 2, 4]); + assert_eq!(removed, vec![0, 2]); + assert_eq!(tree.get(&0, &()), None); + assert_eq!(tree.get(&1, &()), Some(&1)); + assert_eq!(tree.get(&2, &()), Some(&2)); + assert_eq!(tree.get(&4, &()), Some(&4)); + } + + #[test] + fn test_from_iter() { + assert_eq!( + SumTree::from_iter(0..100, &()).items(&()), + (0..100).collect::>() + ); + + // Ensure `from_iter` works correctly when the given iterator restarts + // after calling `next` if `None` was already returned. + let mut ix = 0; + let iterator = std::iter::from_fn(|| { + ix = (ix + 1) % 2; + if ix == 1 { + Some(1) + } else { + None + } + }); + assert_eq!(SumTree::from_iter(iterator, &()).items(&()), vec![1]); + } + + #[derive(Clone, Default, Debug)] + pub struct IntegersSummary { + count: usize, + sum: usize, + contains_even: bool, + max: u8, + } + + #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] + struct Count(usize); + + #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] + struct Sum(usize); + + impl Item for u8 { + type Summary = IntegersSummary; + + fn summary(&self) -> Self::Summary { + IntegersSummary { + count: 1, + sum: *self as usize, + contains_even: (*self & 1) == 0, + max: *self, + } + } + } + + impl KeyedItem for u8 { + type Key = u8; + + fn key(&self) -> Self::Key { + *self + } + } + + impl Summary for IntegersSummary { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + self.count += other.count; + self.sum += other.sum; + self.contains_even |= other.contains_even; + self.max = cmp::max(self.max, other.max); + } + } + + impl<'a> Dimension<'a, IntegersSummary> for u8 { + fn add_summary(&mut self, summary: &IntegersSummary, _: &()) { + *self = summary.max; + } + } + + impl<'a> Dimension<'a, IntegersSummary> for Count { + fn add_summary(&mut self, summary: &IntegersSummary, _: &()) { + self.0 += summary.count; + } + } + + impl<'a> SeekTarget<'a, IntegersSummary, IntegersSummary> for Count { + fn cmp(&self, cursor_location: &IntegersSummary, _: &()) -> Ordering { + self.0.cmp(&cursor_location.count) + } + } + + impl<'a> Dimension<'a, IntegersSummary> for Sum { + fn add_summary(&mut self, summary: &IntegersSummary, _: &()) { + self.0 += summary.sum; + } + } +} diff --git a/crates/util/src/sum_tree/cursor.rs b/crates/util/src/sum_tree/cursor.rs new file mode 100644 index 0000000..4604d9e --- /dev/null +++ b/crates/util/src/sum_tree/cursor.rs @@ -0,0 +1,751 @@ +use super::*; +use arrayvec::ArrayVec; +use std::{cmp::Ordering, mem, sync::Arc}; + +#[derive(Clone)] +struct StackEntry<'a, T: Item, D> { + tree: &'a SumTree, + index: usize, + position: D, +} + +#[derive(Clone)] +pub struct Cursor<'a, T: Item, D> { + tree: &'a SumTree, + stack: ArrayVec, 16>, + position: D, + did_seek: bool, + at_end: bool, +} + +pub struct Iter<'a, T: Item> { + tree: &'a SumTree, + stack: ArrayVec, 16>, +} + +impl<'a, T, D> Cursor<'a, T, D> +where + T: Item, + D: Dimension<'a, T::Summary>, +{ + pub fn new(tree: &'a SumTree) -> Self { + Self { + tree, + stack: ArrayVec::new(), + position: D::default(), + did_seek: false, + at_end: tree.is_empty(), + } + } + + fn reset(&mut self) { + self.did_seek = false; + self.at_end = self.tree.is_empty(); + self.stack.truncate(0); + self.position = D::default(); + } + + pub fn start(&self) -> &D { + &self.position + } + + pub fn end(&self, cx: &::Context) -> D { + if let Some(item_summary) = self.item_summary() { + let mut end = self.start().clone(); + end.add_summary(item_summary, cx); + end + } else { + self.start().clone() + } + } + + pub fn item(&self) -> Option<&'a T> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + match *entry.tree.0 { + Node::Leaf { ref items, .. } => { + if entry.index == items.len() { + None + } else { + Some(&items[entry.index]) + } + } + _ => unreachable!(), + } + } else { + None + } + } + + pub fn item_summary(&self) -> Option<&'a T::Summary> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + match *entry.tree.0 { + Node::Leaf { + ref item_summaries, .. + } => { + if entry.index == item_summaries.len() { + None + } else { + Some(&item_summaries[entry.index]) + } + } + _ => unreachable!(), + } + } else { + None + } + } + + pub fn next_item(&self) -> Option<&'a T> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + if entry.index == entry.tree.0.items().len() - 1 { + if let Some(next_leaf) = self.next_leaf() { + Some(next_leaf.0.items().first().unwrap()) + } else { + None + } + } else { + match *entry.tree.0 { + Node::Leaf { ref items, .. } => Some(&items[entry.index + 1]), + _ => unreachable!(), + } + } + } else if self.at_end { + None + } else { + self.tree.first() + } + } + + fn next_leaf(&self) -> Option<&'a SumTree> { + for entry in self.stack.iter().rev().skip(1) { + if entry.index < entry.tree.0.child_trees().len() - 1 { + match *entry.tree.0 { + Node::Internal { + ref child_trees, .. + } => return Some(child_trees[entry.index + 1].leftmost_leaf()), + Node::Leaf { .. } => unreachable!(), + }; + } + } + None + } + + pub fn prev_item(&self) -> Option<&'a T> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + if entry.index == 0 { + if let Some(prev_leaf) = self.prev_leaf() { + Some(prev_leaf.0.items().last().unwrap()) + } else { + None + } + } else { + match *entry.tree.0 { + Node::Leaf { ref items, .. } => Some(&items[entry.index - 1]), + _ => unreachable!(), + } + } + } else if self.at_end { + self.tree.last() + } else { + None + } + } + + fn prev_leaf(&self) -> Option<&'a SumTree> { + for entry in self.stack.iter().rev().skip(1) { + if entry.index != 0 { + match *entry.tree.0 { + Node::Internal { + ref child_trees, .. + } => return Some(child_trees[entry.index - 1].rightmost_leaf()), + Node::Leaf { .. } => unreachable!(), + }; + } + } + None + } + + pub fn prev(&mut self, cx: &::Context) { + self.prev_internal(|_| true, cx) + } + + fn prev_internal(&mut self, mut filter_node: F, cx: &::Context) + where + F: FnMut(&T::Summary) -> bool, + { + if !self.did_seek { + self.did_seek = true; + self.at_end = true; + } + + if self.at_end { + self.position = D::default(); + self.at_end = self.tree.is_empty(); + if !self.tree.is_empty() { + self.stack.push(StackEntry { + tree: self.tree, + index: self.tree.0.child_summaries().len(), + position: D::from_summary(self.tree.summary(), cx), + }); + } + } + + let mut descending = false; + while !self.stack.is_empty() { + if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) { + self.position = position.clone(); + } else { + self.position = D::default(); + } + + let entry = self.stack.last_mut().unwrap(); + if !descending { + if entry.index == 0 { + self.stack.pop(); + continue; + } else { + entry.index -= 1; + } + } + + for summary in &entry.tree.0.child_summaries()[..entry.index] { + self.position.add_summary(summary, cx); + } + entry.position = self.position.clone(); + + descending = filter_node(&entry.tree.0.child_summaries()[entry.index]); + match entry.tree.0.as_ref() { + Node::Internal { child_trees, .. } => { + if descending { + let tree = &child_trees[entry.index]; + self.stack.push(StackEntry { + position: D::default(), + tree, + index: tree.0.child_summaries().len() - 1, + }) + } + } + Node::Leaf { .. } => { + if descending { + break; + } + } + } + } + } + + pub fn next(&mut self, cx: &::Context) { + self.next_internal(|_| true, cx) + } + + fn next_internal(&mut self, mut filter_node: F, cx: &::Context) + where + F: FnMut(&T::Summary) -> bool, + { + let mut descend = false; + + if self.stack.is_empty() { + if !self.at_end { + self.stack.push(StackEntry { + tree: self.tree, + index: 0, + position: D::default(), + }); + descend = true; + } + self.did_seek = true; + } + + while !self.stack.is_empty() { + let new_subtree = { + let entry = self.stack.last_mut().unwrap(); + match entry.tree.0.as_ref() { + Node::Internal { + child_trees, + child_summaries, + .. + } => { + if !descend { + entry.index += 1; + entry.position = self.position.clone(); + } + + while entry.index < child_summaries.len() { + let next_summary = &child_summaries[entry.index]; + if filter_node(next_summary) { + break; + } else { + entry.index += 1; + entry.position.add_summary(next_summary, cx); + self.position.add_summary(next_summary, cx); + } + } + + child_trees.get(entry.index) + } + Node::Leaf { item_summaries, .. } => { + if !descend { + let item_summary = &item_summaries[entry.index]; + entry.index += 1; + entry.position.add_summary(item_summary, cx); + self.position.add_summary(item_summary, cx); + } + + loop { + if let Some(next_item_summary) = item_summaries.get(entry.index) { + if filter_node(next_item_summary) { + return; + } else { + entry.index += 1; + entry.position.add_summary(next_item_summary, cx); + self.position.add_summary(next_item_summary, cx); + } + } else { + break None; + } + } + } + } + }; + + if let Some(subtree) = new_subtree { + descend = true; + self.stack.push(StackEntry { + tree: subtree, + index: 0, + position: self.position.clone(), + }); + } else { + descend = false; + self.stack.pop(); + } + } + + self.at_end = self.stack.is_empty(); + debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); + } + + fn assert_did_seek(&self) { + assert!( + self.did_seek, + "Must call `seek`, `next` or `prev` before calling this method" + ); + } +} + +impl<'a, T, D> Cursor<'a, T, D> +where + T: Item, + D: Dimension<'a, T::Summary>, +{ + pub fn seek( + &mut self, + pos: &Target, + bias: Bias, + cx: &::Context, + ) -> bool + where + Target: SeekTarget<'a, T::Summary, D>, + { + self.reset(); + self.seek_internal(pos, bias, &mut (), cx) + } + + pub fn seek_forward( + &mut self, + pos: &Target, + bias: Bias, + cx: &::Context, + ) -> bool + where + Target: SeekTarget<'a, T::Summary, D>, + { + self.seek_internal(pos, bias, &mut (), cx) + } + + pub fn slice( + &mut self, + end: &Target, + bias: Bias, + cx: &::Context, + ) -> SumTree + where + Target: SeekTarget<'a, T::Summary, D>, + { + let mut slice = SliceSeekAggregate { + tree: SumTree::new(), + leaf_items: ArrayVec::new(), + leaf_item_summaries: ArrayVec::new(), + leaf_summary: T::Summary::default(), + }; + self.seek_internal(end, bias, &mut slice, cx); + slice.tree + } + + pub fn suffix(&mut self, cx: &::Context) -> SumTree { + self.slice(&End::new(), Bias::Right, cx) + } + + pub fn summary( + &mut self, + end: &Target, + bias: Bias, + cx: &::Context, + ) -> Output + where + Target: SeekTarget<'a, T::Summary, D>, + Output: Dimension<'a, T::Summary>, + { + let mut summary = SummarySeekAggregate(Output::default()); + self.seek_internal(end, bias, &mut summary, cx); + summary.0 + } + + /// Returns whether we found the item you where seeking for + fn seek_internal( + &mut self, + target: &dyn SeekTarget<'a, T::Summary, D>, + bias: Bias, + aggregate: &mut dyn SeekAggregate<'a, T>, + cx: &::Context, + ) -> bool { + debug_assert!( + target.cmp(&self.position, cx) >= Ordering::Equal, + "cannot seek backward from {:?} to {:?}", + self.position, + target + ); + + if !self.did_seek { + self.did_seek = true; + self.stack.push(StackEntry { + tree: self.tree, + index: 0, + position: Default::default(), + }); + } + + let mut ascending = false; + 'outer: while let Some(entry) = self.stack.last_mut() { + match *entry.tree.0 { + Node::Internal { + ref child_summaries, + ref child_trees, + .. + } => { + if ascending { + entry.index += 1; + entry.position = self.position.clone(); + } + + for (child_tree, child_summary) in child_trees[entry.index..] + .iter() + .zip(&child_summaries[entry.index..]) + { + let mut child_end = self.position.clone(); + child_end.add_summary(child_summary, cx); + + let comparison = target.cmp(&child_end, cx); + if comparison == Ordering::Greater + || (comparison == Ordering::Equal && bias == Bias::Right) + { + self.position = child_end; + aggregate.push_tree(child_tree, child_summary, cx); + entry.index += 1; + entry.position = self.position.clone(); + } else { + self.stack.push(StackEntry { + tree: child_tree, + index: 0, + position: self.position.clone(), + }); + ascending = false; + continue 'outer; + } + } + } + Node::Leaf { + ref items, + ref item_summaries, + .. + } => { + aggregate.begin_leaf(); + + for (item, item_summary) in items[entry.index..] + .iter() + .zip(&item_summaries[entry.index..]) + { + let mut child_end = self.position.clone(); + child_end.add_summary(item_summary, cx); + + let comparison = target.cmp(&child_end, cx); + if comparison == Ordering::Greater + || (comparison == Ordering::Equal && bias == Bias::Right) + { + self.position = child_end; + aggregate.push_item(item, item_summary, cx); + entry.index += 1; + } else { + aggregate.end_leaf(cx); + break 'outer; + } + } + + aggregate.end_leaf(cx); + } + } + + self.stack.pop(); + ascending = true; + } + + self.at_end = self.stack.is_empty(); + debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); + + let mut end = self.position.clone(); + if bias == Bias::Left { + if let Some(summary) = self.item_summary() { + end.add_summary(summary, cx); + } + } + + target.cmp(&end, cx) == Ordering::Equal + } +} + +impl<'a, T: Item> Iter<'a, T> { + pub(crate) fn new(tree: &'a SumTree) -> Self { + Self { + tree, + stack: Default::default(), + } + } +} + +impl<'a, T: Item> Iterator for Iter<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + let mut descend = false; + + if self.stack.is_empty() { + self.stack.push(StackEntry { + tree: self.tree, + index: 0, + position: (), + }); + descend = true; + } + + while !self.stack.is_empty() { + let new_subtree = { + let entry = self.stack.last_mut().unwrap(); + match entry.tree.0.as_ref() { + Node::Internal { child_trees, .. } => { + if !descend { + entry.index += 1; + } + child_trees.get(entry.index) + } + Node::Leaf { items, .. } => { + if !descend { + entry.index += 1; + } + + if let Some(next_item) = items.get(entry.index) { + return Some(next_item); + } else { + None + } + } + } + }; + + if let Some(subtree) = new_subtree { + descend = true; + self.stack.push(StackEntry { + tree: subtree, + index: 0, + position: (), + }); + } else { + descend = false; + self.stack.pop(); + } + } + + None + } +} + +impl<'a, T, S, D> Iterator for Cursor<'a, T, D> +where + T: Item

, + S: Summary, + D: Dimension<'a, T::Summary>, +{ + type Item = &'a T; + + fn next(&mut self) -> Option { + if !self.did_seek { + self.next(&()); + } + + if let Some(item) = self.item() { + self.next(&()); + Some(item) + } else { + None + } + } +} + +pub struct FilterCursor<'a, F, T: Item, D> { + cursor: Cursor<'a, T, D>, + filter_node: F, +} + +impl<'a, F, T, D> FilterCursor<'a, F, T, D> +where + F: FnMut(&T::Summary) -> bool, + T: Item, + D: Dimension<'a, T::Summary>, +{ + pub fn new(tree: &'a SumTree, filter_node: F) -> Self { + let cursor = tree.cursor::(); + Self { + cursor, + filter_node, + } + } + + pub fn start(&self) -> &D { + self.cursor.start() + } + + pub fn end(&self, cx: &::Context) -> D { + self.cursor.end(cx) + } + + pub fn item(&self) -> Option<&'a T> { + self.cursor.item() + } + + pub fn item_summary(&self) -> Option<&'a T::Summary> { + self.cursor.item_summary() + } + + pub fn next(&mut self, cx: &::Context) { + self.cursor.next_internal(&mut self.filter_node, cx); + } + + pub fn prev(&mut self, cx: &::Context) { + self.cursor.prev_internal(&mut self.filter_node, cx); + } +} + +impl<'a, F, T, S, U> Iterator for FilterCursor<'a, F, T, U> +where + F: FnMut(&T::Summary) -> bool, + T: Item, + S: Summary, //Context for the summary must be unit type, as .next() doesn't take arguments + U: Dimension<'a, T::Summary>, +{ + type Item = &'a T; + + fn next(&mut self) -> Option { + if !self.cursor.did_seek { + self.next(&()); + } + + if let Some(item) = self.item() { + self.cursor.next_internal(&mut self.filter_node, &()); + Some(item) + } else { + None + } + } +} + +trait SeekAggregate<'a, T: Item> { + fn begin_leaf(&mut self); + fn end_leaf(&mut self, cx: &::Context); + fn push_item( + &mut self, + item: &'a T, + summary: &'a T::Summary, + cx: &::Context, + ); + fn push_tree( + &mut self, + tree: &'a SumTree, + summary: &'a T::Summary, + cx: &::Context, + ); +} + +struct SliceSeekAggregate { + tree: SumTree, + leaf_items: ArrayVec, + leaf_item_summaries: ArrayVec, + leaf_summary: T::Summary, +} + +struct SummarySeekAggregate(D); + +impl<'a, T: Item> SeekAggregate<'a, T> for () { + fn begin_leaf(&mut self) {} + fn end_leaf(&mut self, _: &::Context) {} + fn push_item(&mut self, _: &T, _: &T::Summary, _: &::Context) {} + fn push_tree(&mut self, _: &SumTree, _: &T::Summary, _: &::Context) {} +} + +impl<'a, T: Item> SeekAggregate<'a, T> for SliceSeekAggregate { + fn begin_leaf(&mut self) {} + fn end_leaf(&mut self, cx: &::Context) { + self.tree.append( + SumTree(Arc::new(Node::Leaf { + summary: mem::take(&mut self.leaf_summary), + items: mem::take(&mut self.leaf_items), + item_summaries: mem::take(&mut self.leaf_item_summaries), + })), + cx, + ); + } + fn push_item(&mut self, item: &T, summary: &T::Summary, cx: &::Context) { + self.leaf_items.push(item.clone()); + self.leaf_item_summaries.push(summary.clone()); + Summary::add_summary(&mut self.leaf_summary, summary, cx); + } + fn push_tree( + &mut self, + tree: &SumTree, + _: &T::Summary, + cx: &::Context, + ) { + self.tree.append(tree.clone(), cx); + } +} + +impl<'a, T: Item, D> SeekAggregate<'a, T> for SummarySeekAggregate +where + D: Dimension<'a, T::Summary>, +{ + fn begin_leaf(&mut self) {} + fn end_leaf(&mut self, _: &::Context) {} + fn push_item(&mut self, _: &T, summary: &'a T::Summary, cx: &::Context) { + self.0.add_summary(summary, cx); + } + fn push_tree( + &mut self, + _: &SumTree, + summary: &'a T::Summary, + cx: &::Context, + ) { + self.0.add_summary(summary, cx); + } +} diff --git a/crates/util/src/sum_tree/tree_map.rs b/crates/util/src/sum_tree/tree_map.rs new file mode 100644 index 0000000..2c33778 --- /dev/null +++ b/crates/util/src/sum_tree/tree_map.rs @@ -0,0 +1,463 @@ +use std::{cmp::Ordering, fmt::Debug}; + +use super::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; + +#[derive(Clone, PartialEq, Eq)] +pub struct TreeMap(SumTree>) +where + K: Clone + Debug + Ord, + V: Clone + Debug; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MapEntry { + key: K, + value: V, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct MapKey(Option); + +impl Default for MapKey { + fn default() -> Self { + Self(None) + } +} + +#[derive(Clone, Debug)] +pub struct MapKeyRef<'a, K>(Option<&'a K>); + +impl<'a, K> Default for MapKeyRef<'a, K> { + fn default() -> Self { + Self(None) + } +} + +#[derive(Clone)] +pub struct TreeSet(TreeMap) +where + K: Clone + Debug + Ord; + +impl TreeMap { + pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { + let tree = SumTree::from_iter( + entries + .into_iter() + .map(|(key, value)| MapEntry { key, value }), + &(), + ); + Self(tree) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn get(&self, key: &K) -> Option<&V> { + let mut cursor = self.0.cursor::>(); + cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &()); + if let Some(item) = cursor.item() { + if Some(key) == item.key().0.as_ref() { + Some(&item.value) + } else { + None + } + } else { + None + } + } + + pub fn insert(&mut self, key: K, value: V) { + self.0.insert_or_replace(MapEntry { key, value }, &()); + } + + pub fn remove(&mut self, key: &K) -> Option { + let mut removed = None; + let mut cursor = self.0.cursor::>(); + let key = MapKeyRef(Some(key)); + let mut new_tree = cursor.slice(&key, Bias::Left, &()); + if key.cmp(&cursor.end(&()), &()) == Ordering::Equal { + removed = Some(cursor.item().unwrap().value.clone()); + cursor.next(&()); + } + new_tree.append(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + removed + } + + pub fn remove_range(&mut self, start: &impl MapSeekTarget, end: &impl MapSeekTarget) { + let start = MapSeekTargetAdaptor(start); + let end = MapSeekTargetAdaptor(end); + let mut cursor = self.0.cursor::>(); + let mut new_tree = cursor.slice(&start, Bias::Left, &()); + cursor.seek(&end, Bias::Left, &()); + new_tree.append(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + } + + /// Returns the key-value pair with the greatest key less than or equal to the given key. + pub fn closest(&self, key: &K) -> Option<(&K, &V)> { + let mut cursor = self.0.cursor::>(); + let key = MapKeyRef(Some(key)); + cursor.seek(&key, Bias::Right, &()); + cursor.prev(&()); + cursor.item().map(|item| (&item.key, &item.value)) + } + + pub fn iter_from<'a>(&'a self, from: &'a K) -> impl Iterator + '_ { + let mut cursor = self.0.cursor::>(); + let from_key = MapKeyRef(Some(from)); + cursor.seek(&from_key, Bias::Left, &()); + + cursor.map(|map_entry| (&map_entry.key, &map_entry.value)) + } + + pub fn update(&mut self, key: &K, f: F) -> Option + where + F: FnOnce(&mut V) -> T, + { + let mut cursor = self.0.cursor::>(); + let key = MapKeyRef(Some(key)); + let mut new_tree = cursor.slice(&key, Bias::Left, &()); + let mut result = None; + if key.cmp(&cursor.end(&()), &()) == Ordering::Equal { + let mut updated = cursor.item().unwrap().clone(); + result = Some(f(&mut updated.value)); + new_tree.push(updated, &()); + cursor.next(&()); + } + new_tree.append(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + result + } + + pub fn retain bool>(&mut self, mut predicate: F) { + let mut new_map = SumTree::>::default(); + + let mut cursor = self.0.cursor::>(); + cursor.next(&()); + while let Some(item) = cursor.item() { + if predicate(&item.key, &item.value) { + new_map.push(item.clone(), &()); + } + cursor.next(&()); + } + drop(cursor); + + self.0 = new_map; + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().map(|entry| (&entry.key, &entry.value)) + } + + pub fn values(&self) -> impl Iterator + '_ { + self.0.iter().map(|entry| &entry.value) + } + + pub fn insert_tree(&mut self, other: TreeMap) { + let edits = other + .iter() + .map(|(key, value)| { + Edit::Insert(MapEntry { + key: key.to_owned(), + value: value.to_owned(), + }) + }) + .collect(); + + self.0.edit(edits, &()); + } +} + +impl Debug for TreeMap +where + K: Clone + Debug + Ord, + V: Clone + Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + +#[derive(Debug)] +struct MapSeekTargetAdaptor<'a, T>(&'a T); + +impl<'a, K: Debug + Clone + Ord, T: MapSeekTarget> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> + for MapSeekTargetAdaptor<'_, T> +{ + fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { + if let Some(key) = &cursor_location.0 { + MapSeekTarget::cmp_cursor(self.0, key) + } else { + Ordering::Greater + } + } +} + +pub trait MapSeekTarget: Debug { + fn cmp_cursor(&self, cursor_location: &K) -> Ordering; +} + +impl MapSeekTarget for K { + fn cmp_cursor(&self, cursor_location: &K) -> Ordering { + self.cmp(cursor_location) + } +} + +impl Default for TreeMap +where + K: Clone + Debug + Ord, + V: Clone + Debug, +{ + fn default() -> Self { + Self(Default::default()) + } +} + +impl Item for MapEntry +where + K: Clone + Debug + Ord, + V: Clone, +{ + type Summary = MapKey; + + fn summary(&self) -> Self::Summary { + self.key() + } +} + +impl KeyedItem for MapEntry +where + K: Clone + Debug + Ord, + V: Clone, +{ + type Key = MapKey; + + fn key(&self) -> Self::Key { + MapKey(Some(self.key.clone())) + } +} + +impl Summary for MapKey +where + K: Clone + Debug, +{ + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + *self = summary.clone() + } +} + +impl<'a, K> Dimension<'a, MapKey> for MapKeyRef<'a, K> +where + K: Clone + Debug + Ord, +{ + fn add_summary(&mut self, summary: &'a MapKey, _: &()) { + self.0 = summary.0.as_ref(); + } +} + +impl<'a, K> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapKeyRef<'_, K> +where + K: Clone + Debug + Ord, +{ + fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { + Ord::cmp(&self.0, &cursor_location.0) + } +} + +impl Default for TreeSet +where + K: Clone + Debug + Ord, +{ + fn default() -> Self { + Self(Default::default()) + } +} + +impl TreeSet +where + K: Clone + Debug + Ord, +{ + pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { + Self(TreeMap::from_ordered_entries( + entries.into_iter().map(|key| (key, ())), + )) + } + + pub fn insert(&mut self, key: K) { + self.0.insert(key, ()); + } + + pub fn contains(&self, key: &K) -> bool { + self.0.get(key).is_some() + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().map(|(k, _)| k) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic() { + let mut map = TreeMap::default(); + assert_eq!(map.iter().collect::>(), vec![]); + + map.insert(3, "c"); + assert_eq!(map.get(&3), Some(&"c")); + assert_eq!(map.iter().collect::>(), vec![(&3, &"c")]); + + map.insert(1, "a"); + assert_eq!(map.get(&1), Some(&"a")); + assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); + + map.insert(2, "b"); + assert_eq!(map.get(&2), Some(&"b")); + assert_eq!(map.get(&1), Some(&"a")); + assert_eq!(map.get(&3), Some(&"c")); + assert_eq!( + map.iter().collect::>(), + vec![(&1, &"a"), (&2, &"b"), (&3, &"c")] + ); + + assert_eq!(map.closest(&0), None); + assert_eq!(map.closest(&1), Some((&1, &"a"))); + assert_eq!(map.closest(&10), Some((&3, &"c"))); + + map.remove(&2); + assert_eq!(map.get(&2), None); + assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); + + assert_eq!(map.closest(&2), Some((&1, &"a"))); + + map.remove(&3); + assert_eq!(map.get(&3), None); + assert_eq!(map.iter().collect::>(), vec![(&1, &"a")]); + + map.remove(&1); + assert_eq!(map.get(&1), None); + assert_eq!(map.iter().collect::>(), vec![]); + + map.insert(4, "d"); + map.insert(5, "e"); + map.insert(6, "f"); + map.retain(|key, _| *key % 2 == 0); + assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); + } + + #[test] + fn test_iter_from() { + let mut map = TreeMap::default(); + + map.insert("a", 1); + map.insert("b", 2); + map.insert("baa", 3); + map.insert("baaab", 4); + map.insert("c", 5); + + let result = map + .iter_from(&"ba") + .take_while(|(key, _)| key.starts_with(&"ba")) + .collect::>(); + + assert_eq!(result.len(), 2); + assert!(result.iter().any(|(k, _)| k == &&"baa")); + assert!(result.iter().any(|(k, _)| k == &&"baaab")); + + let result = map + .iter_from(&"c") + .take_while(|(key, _)| key.starts_with(&"c")) + .collect::>(); + + assert_eq!(result.len(), 1); + assert!(result.iter().any(|(k, _)| k == &&"c")); + } + + #[test] + fn test_insert_tree() { + let mut map = TreeMap::default(); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 3); + + let mut other = TreeMap::default(); + other.insert("a", 2); + other.insert("b", 2); + other.insert("d", 4); + + map.insert_tree(other); + + assert_eq!(map.iter().count(), 4); + assert_eq!(map.get(&"a"), Some(&2)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"c"), Some(&3)); + assert_eq!(map.get(&"d"), Some(&4)); + } + + #[test] + fn test_remove_between_and_path_successor() { + use std::path::{Path, PathBuf}; + + #[derive(Debug)] + pub struct PathDescendants<'a>(&'a Path); + + impl MapSeekTarget for PathDescendants<'_> { + fn cmp_cursor(&self, key: &PathBuf) -> Ordering { + if key.starts_with(&self.0) { + Ordering::Greater + } else { + self.0.cmp(key) + } + } + } + + let mut map = TreeMap::default(); + + map.insert(PathBuf::from("a"), 1); + map.insert(PathBuf::from("a/a"), 1); + map.insert(PathBuf::from("b"), 2); + map.insert(PathBuf::from("b/a/a"), 3); + map.insert(PathBuf::from("b/a/a/a/b"), 4); + map.insert(PathBuf::from("c"), 5); + map.insert(PathBuf::from("c/a"), 6); + + map.remove_range( + &PathBuf::from("b/a"), + &PathDescendants(&PathBuf::from("b/a")), + ); + + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("b/a/a")), None); + assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None); + assert_eq!(map.get(&PathBuf::from("c")), Some(&5)); + assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6)); + + map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c"))); + + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("c")), None); + assert_eq!(map.get(&PathBuf::from("c/a")), None); + + map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a"))); + + assert_eq!(map.get(&PathBuf::from("a")), None); + assert_eq!(map.get(&PathBuf::from("a/a")), None); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + + map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b"))); + + assert_eq!(map.get(&PathBuf::from("b")), None); + } +} diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs new file mode 100644 index 0000000..9915a6c --- /dev/null +++ b/crates/util/src/test.rs @@ -0,0 +1,65 @@ +mod assertions; +mod marked_text; + +use git2; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; +use tempfile::TempDir; + +pub use assertions::*; +pub use marked_text::*; + +pub fn temp_tree(tree: serde_json::Value) -> TempDir { + let dir = TempDir::new().unwrap(); + write_tree(dir.path(), tree); + dir +} + +fn write_tree(path: &Path, tree: serde_json::Value) { + use serde_json::Value; + use std::fs; + + if let Value::Object(map) = tree { + for (name, contents) in map { + let mut path = PathBuf::from(path); + path.push(name); + match contents { + Value::Object(_) => { + fs::create_dir(&path).unwrap(); + + if path.file_name() == Some(OsStr::new(".git")) { + git2::Repository::init(path.parent().unwrap()).unwrap(); + } + + write_tree(&path, contents); + } + Value::Null => { + fs::create_dir(&path).unwrap(); + } + Value::String(contents) => { + fs::write(&path, contents).unwrap(); + } + _ => { + panic!("JSON object must contain only objects, strings, or null"); + } + } + } + } else { + panic!("You must pass a JSON object to this helper") + } +} + +pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { + let mut text = String::new(); + for row in 0..rows { + let c: char = (start_char as u32 + row as u32) as u8 as char; + let mut line = c.to_string().repeat(cols); + if row < rows - 1 { + line.push('\n'); + } + text += &line; + } + text +} diff --git a/crates/util/src/test/assertions.rs b/crates/util/src/test/assertions.rs new file mode 100644 index 0000000..afb1397 --- /dev/null +++ b/crates/util/src/test/assertions.rs @@ -0,0 +1,62 @@ +pub enum SetEqError { + LeftMissing(T), + RightMissing(T), +} + +impl SetEqError { + pub fn map R>(self, update: F) -> SetEqError { + match self { + SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)), + SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)), + } + } +} + +#[macro_export] +macro_rules! set_eq { + ($left:expr,$right:expr) => {{ + use util::test::*; + + let left = $left; + let right = $right; + + let mut result = Ok(()); + for right_value in right.iter() { + if !left.contains(right_value) { + result = Err(SetEqError::LeftMissing(right_value.clone())); + break; + } + } + + if result.is_ok() { + for left_value in left.iter() { + if !right.contains(left_value) { + result = Err(SetEqError::RightMissing(left_value.clone())); + } + } + } + + result + }}; +} + +#[macro_export] +macro_rules! assert_set_eq { + ($left:expr,$right:expr) => {{ + use util::test::*; + use util::set_eq; + + let left = $left; + let right = $right; + + match set_eq!(&left, &right) { + Err(SetEqError::LeftMissing(missing)) => { + panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing); + }, + Err(SetEqError::RightMissing(missing)) => { + panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing); + }, + _ => {} + } + }}; +} diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs new file mode 100644 index 0000000..7ab45e9 --- /dev/null +++ b/crates/util/src/test/marked_text.rs @@ -0,0 +1,272 @@ +use collections::HashMap; +use std::{cmp::Ordering, ops::Range}; + +/// Construct a string and a list of offsets within that string using a single +/// string containing embedded position markers. +pub fn marked_text_offsets_by( + marked_text: &str, + markers: Vec, +) -> (String, HashMap>) { + let mut extracted_markers: HashMap> = Default::default(); + let mut unmarked_text = String::new(); + + for char in marked_text.chars() { + if markers.contains(&char) { + let char_offsets = extracted_markers.entry(char).or_default(); + char_offsets.push(unmarked_text.len()); + } else { + unmarked_text.push(char); + } + } + + (unmarked_text, extracted_markers) +} + +/// Construct a string and a list of ranges within that string using a single +/// string containing embedded range markers, using arbitrary characters as +/// range markers. By using multiple different range markers, you can construct +/// ranges that overlap each other. +/// +/// The returned ranges will be grouped by their range marking characters. +pub fn marked_text_ranges_by( + marked_text: &str, + markers: Vec, +) -> (String, HashMap>>) { + let all_markers = markers.iter().flat_map(|m| m.markers()).collect(); + + let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers); + let range_lookup = markers + .into_iter() + .map(|marker| { + ( + marker.clone(), + match marker { + TextRangeMarker::Empty(empty_marker_char) => marker_offsets + .remove(&empty_marker_char) + .unwrap_or_default() + .into_iter() + .map(|empty_index| empty_index..empty_index) + .collect::>>(), + TextRangeMarker::Range(start_marker, end_marker) => { + let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); + let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); + assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); + starts + .into_iter() + .zip(ends) + .map(|(start, end)| { + assert!(end >= start, "marked ranges must be disjoint"); + start..end + }) + .collect::>>() + } + TextRangeMarker::ReverseRange(start_marker, end_marker) => { + let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); + let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); + assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); + starts + .into_iter() + .zip(ends) + .map(|(start, end)| { + assert!(end >= start, "marked ranges must be disjoint"); + end..start + }) + .collect::>>() + } + }, + ) + }) + .collect(); + + (unmarked_text, range_lookup) +} + +/// Construct a string and a list of ranges within that string using a single +/// string containing embedded range markers. The characters used to mark the +/// ranges are as follows: +/// +/// 1. To mark a range of text, surround it with the `«` and `»` angle brackets, +/// which can be typed on a US keyboard with the `alt-|` and `alt-shift-|` keys. +/// +/// ```text +/// foo «selected text» bar +/// ``` +/// +/// 2. To mark a single position in the text, use the `ˇ` caron, +/// which can be typed on a US keyboard with the `alt-shift-t` key. +/// +/// ```text +/// the cursors are hereˇ and hereˇ. +/// ``` +/// +/// 3. To mark a range whose direction is meaningful (like a selection), +/// put a caron character beside one of its bounds, on the inside: +/// +/// ```text +/// one «ˇreversed» selection and one «forwardˇ» selection +/// ``` +/// +/// Any • characters in the input string will be replaced with spaces. This makes +/// it easier to test cases with trailing spaces, which tend to get trimmed from the +/// source code. +pub fn marked_text_ranges( + marked_text: &str, + ranges_are_directed: bool, +) -> (String, Vec>) { + let mut unmarked_text = String::with_capacity(marked_text.len()); + let mut ranges = Vec::new(); + let mut prev_marked_ix = 0; + let mut current_range_start = None; + let mut current_range_cursor = None; + + let marked_text = marked_text.replace('•', " "); + for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) { + unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]); + let unmarked_len = unmarked_text.len(); + let len = marker.len(); + prev_marked_ix = marked_ix + len; + + match marker { + "ˇ" => { + if current_range_start.is_some() { + if current_range_cursor.is_some() { + panic!("duplicate point marker 'ˇ' at index {marked_ix}"); + } + + current_range_cursor = Some(unmarked_len); + } else { + ranges.push(unmarked_len..unmarked_len); + } + } + "«" => { + if current_range_start.is_some() { + panic!("unexpected range start marker '«' at index {marked_ix}"); + } + current_range_start = Some(unmarked_len); + } + "»" => { + let current_range_start = if let Some(start) = current_range_start.take() { + start + } else { + panic!("unexpected range end marker '»' at index {marked_ix}"); + }; + + let mut reversed = false; + if let Some(current_range_cursor) = current_range_cursor.take() { + if current_range_cursor == current_range_start { + reversed = true; + } else if current_range_cursor != unmarked_len { + panic!("unexpected 'ˇ' marker in the middle of a range"); + } + } else if ranges_are_directed { + panic!("missing 'ˇ' marker to indicate range direction"); + } + + ranges.push(if reversed { + unmarked_len..current_range_start + } else { + current_range_start..unmarked_len + }); + } + _ => unreachable!(), + } + } + + unmarked_text.push_str(&marked_text[prev_marked_ix..]); + (unmarked_text, ranges) +} + +pub fn marked_text_offsets(marked_text: &str) -> (String, Vec) { + let (text, ranges) = marked_text_ranges(marked_text, false); + ( + text, + ranges + .into_iter() + .map(|range| { + assert_eq!(range.start, range.end); + range.start + }) + .collect(), + ) +} + +pub fn generate_marked_text( + unmarked_text: &str, + ranges: &[Range], + indicate_cursors: bool, +) -> String { + let mut marked_text = unmarked_text.to_string(); + for range in ranges.iter().rev() { + if indicate_cursors { + match range.start.cmp(&range.end) { + Ordering::Less => { + marked_text.insert_str(range.end, "ˇ»"); + marked_text.insert(range.start, '«'); + } + Ordering::Equal => { + marked_text.insert(range.start, 'ˇ'); + } + Ordering::Greater => { + marked_text.insert(range.start, '»'); + marked_text.insert_str(range.end, "«ˇ"); + } + } + } else { + marked_text.insert(range.end, '»'); + marked_text.insert(range.start, '«'); + } + } + marked_text +} + +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum TextRangeMarker { + Empty(char), + Range(char, char), + ReverseRange(char, char), +} + +impl TextRangeMarker { + fn markers(&self) -> Vec { + match self { + Self::Empty(m) => vec![*m], + Self::Range(l, r) => vec![*l, *r], + Self::ReverseRange(l, r) => vec![*l, *r], + } + } +} + +impl From for TextRangeMarker { + fn from(marker: char) -> Self { + Self::Empty(marker) + } +} + +impl From<(char, char)> for TextRangeMarker { + fn from((left_marker, right_marker): (char, char)) -> Self { + Self::Range(left_marker, right_marker) + } +} + +#[cfg(test)] +mod tests { + use super::{generate_marked_text, marked_text_ranges}; + + #[allow(clippy::reversed_empty_ranges)] + #[test] + fn test_marked_text() { + let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true); + + assert_eq!(text, "one two three four five six"); + assert_eq!(ranges.len(), 4); + assert_eq!(ranges[0], 7..4); + assert_eq!(ranges[1], 8..13); + assert_eq!(ranges[2], 18..14); + assert_eq!(ranges[3], 23..23); + + assert_eq!( + generate_marked_text(&text, &ranges, true), + "one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six" + ); + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..94e5fef --- /dev/null +++ b/flake.lock @@ -0,0 +1,435 @@ +{ + "nodes": { + "crane": { + "flake": false, + "locked": { + "lastModified": 1699217310, + "narHash": "sha256-xpW3VFUG7yE6UE6Wl0dhqencuENSkV7qpnpe9I8VbPw=", + "owner": "ipetkov", + "repo": "crane", + "rev": "d535642bbe6f377077f7c23f0febb78b1463f449", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "ref": "v0.15.0", + "repo": "crane", + "type": "github" + } + }, + "devshell": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1713532798, + "narHash": "sha256-wtBhsdMJA3Wa32Wtm1eeo84GejtI43pMrFrmwLXrsEc=", + "owner": "numtide", + "repo": "devshell", + "rev": "12e914740a25ea1891ec619bb53cf5e6ca922e40", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "dream2nix": { + "inputs": { + "nixpkgs": [ + "nci", + "nixpkgs" + ], + "purescript-overlay": "purescript-overlay", + "pyproject-nix": "pyproject-nix" + }, + "locked": { + "lastModified": 1715517859, + "narHash": "sha256-H/9fwzjwRRELLL8egvJfNB6ebEQo+j3p1qddbbzKego=", + "owner": "nix-community", + "repo": "dream2nix", + "rev": "1a4df0e94f273b10834a6701c58d5f1742d26936", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "dream2nix", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1714641030, + "narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "mk-naked-shell": { + "flake": false, + "locked": { + "lastModified": 1681286841, + "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", + "owner": "yusdacra", + "repo": "mk-naked-shell", + "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", + "type": "github" + }, + "original": { + "owner": "yusdacra", + "repo": "mk-naked-shell", + "type": "github" + } + }, + "nci": { + "inputs": { + "crane": "crane", + "dream2nix": "dream2nix", + "mk-naked-shell": "mk-naked-shell", + "nixpkgs": "nixpkgs", + "parts": "parts", + "rust-overlay": "rust-overlay", + "treefmt": "treefmt" + }, + "locked": { + "lastModified": 1715580775, + "narHash": "sha256-QpSLFjV4k3+ZJ62UXavuyq3U4Ag/6bC2eqr6claEu+w=", + "owner": "yusdacra", + "repo": "nix-cargo-integration", + "rev": "a51105224b29a488ae5ae17f5c8752901de9b56a", + "type": "github" + }, + "original": { + "owner": "yusdacra", + "repo": "nix-cargo-integration", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1715447595, + "narHash": "sha256-VsVAUQOj/cS1LCOmMjAGeRksXIAdPnFIjCQ0XLkCsT0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "062ca2a9370a27a35c524dc82d540e6e9824b652", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1714640452, + "narHash": "sha256-QBx10+k6JWz6u7VsohfSw8g8hjdBZEf8CFzXH1/1Z94=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1715530780, + "narHash": "sha256-bBz4/T/zBzv9Xi5XUlFDeosmSNppLaCQTizMKSksAvk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3281bec7174f679eabf584591e75979a258d8c40", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1706487304, + "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1708475490, + "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "0e74ca98a74bc7270d28838369593635a5db3260", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "parts": { + "inputs": { + "nixpkgs-lib": [ + "nci", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1714641030, + "narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "purescript-overlay": { + "inputs": { + "nixpkgs": [ + "nci", + "dream2nix", + "nixpkgs" + ], + "slimlock": "slimlock" + }, + "locked": { + "lastModified": 1696022621, + "narHash": "sha256-eMjFmsj2G1E0Q5XiibUNgFjTiSz0GxIeSSzzVdoN730=", + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "rev": "047c7933abd6da8aa239904422e22d190ce55ead", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "type": "github" + } + }, + "pyproject-nix": { + "flake": false, + "locked": { + "lastModified": 1702448246, + "narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=", + "owner": "davhau", + "repo": "pyproject.nix", + "rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb", + "type": "github" + }, + "original": { + "owner": "davhau", + "ref": "dream2nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts", + "nci": "nci", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay_2", + "treefmt-nix": "treefmt-nix" + } + }, + "rust-overlay": { + "flake": false, + "locked": { + "lastModified": 1715566659, + "narHash": "sha256-OpI0TnN+uE0vvxjPStlTzf5RTohIXVSMwrP9NEgMtaY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "6c465248316cd31502c82f81f1a3acf2d621b01c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1715566659, + "narHash": "sha256-OpI0TnN+uE0vvxjPStlTzf5RTohIXVSMwrP9NEgMtaY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "6c465248316cd31502c82f81f1a3acf2d621b01c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "slimlock": { + "inputs": { + "nixpkgs": [ + "nci", + "dream2nix", + "purescript-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1688610262, + "narHash": "sha256-Wg0ViDotFWGWqKIQzyYCgayeH8s4U1OZcTiWTQYdAp4=", + "owner": "thomashoneyman", + "repo": "slimlock", + "rev": "b5c6cdcaf636ebbebd0a1f32520929394493f1a6", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "slimlock", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt": { + "inputs": { + "nixpkgs": [ + "nci", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1714058656, + "narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_4" + }, + "locked": { + "lastModified": 1714058656, + "narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a568b78 --- /dev/null +++ b/flake.nix @@ -0,0 +1,77 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-parts.url = "github:hercules-ci/flake-parts"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + nci.url = "github:yusdacra/nix-cargo-integration"; + devshell = { + url = "github:numtide/devshell"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + nixConfig = { + extra-trusted-public-keys = + [ "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=" ]; + extra-substituters = [ "https://devenv.cachix.org" ]; + }; + + outputs = { self, flake-parts, ... }@inputs: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + inputs.devshell.flakeModule + inputs.treefmt-nix.flakeModule + inputs.nci.flakeModule + ]; + systems = inputs.nixpkgs.lib.systems.flakeExposed; + perSystem = { config, pkgs, ... }: rec { + treefmt = { + programs = { + nixfmt-rfc-style.enable = true; + rustfmt.enable = true; + }; + projectRootFile = ./flake.nix; + }; + + nci.projects."nite".path = ./.; + nci.crates."nite" = { }; + + packages = config.nci.outputs."nite".packages.release; + + devshells.default = let + zcommands = [ "ar" "cc" "c++" ]; + zcc = pkgs.writeShellScriptBin "zcc" '' + zig cc $@ + ''; + + libs = with pkgs; [ fontconfig freetype ]; + in { + imports = [ (inputs.devshell + "/extra/language/c.nix") ]; + env = [{ + name = "AR"; + value = pkgs.writeShellScript "zar" '' + zig ar $@ + ''; + }]; + + devshell.name = "nite"; + language.c.compiler = zcc; + language.c.includes = libs; + + commands = [ + { + help = "format the entire tree"; + name = "format"; + command = "${config.treefmt.build.wrapper}/bin/treefmt ."; + } + { + help = "launch the editor"; + name = "nite"; + command = "RUST_LOG=info,nite=trace cargo run"; + } + ]; + }; + }; + }; +}