mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
GPUI on the web (#50228)
Implements a basic web platform for the wasm32-unknown-unknown target for gpui Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: John Tur <john-tur@outlook.com>
This commit is contained in:
parent
e8325318a0
commit
14f37ed502
117 changed files with 5304 additions and 1132 deletions
41
.github/workflows/run_tests.yml
vendored
41
.github/workflows/run_tests.yml
vendored
|
|
@ -466,6 +466,45 @@ jobs:
|
|||
run: |
|
||||
rm -rf ./../.cargo
|
||||
timeout-minutes: 60
|
||||
check_wasm:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.run_tests == 'true'
|
||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
- name: steps::setup_cargo_config
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
- name: steps::cache_rust_dependencies_namespace
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
path: ~/.rustup
|
||||
- name: run_tests::check_wasm::install_nightly_wasm_toolchain
|
||||
run: rustup toolchain install nightly --component rust-src --target wasm32-unknown-unknown
|
||||
- name: steps::setup_sccache
|
||||
run: ./script/setup-sccache
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
SCCACHE_BUCKET: sccache-zed
|
||||
- name: run_tests::check_wasm::cargo_check_wasm
|
||||
run: cargo +nightly -Zbuild-std=std,panic_abort check --target wasm32-unknown-unknown -p gpui_platform
|
||||
env:
|
||||
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: -C target-feature=+atomics,+bulk-memory,+mutable-globals
|
||||
- name: steps::show_sccache_stats
|
||||
run: sccache --show-stats || true
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
timeout-minutes: 60
|
||||
check_dependencies:
|
||||
needs:
|
||||
- orchestrate
|
||||
|
|
@ -641,6 +680,7 @@ jobs:
|
|||
- run_tests_mac
|
||||
- doctests
|
||||
- check_workspace_binaries
|
||||
- check_wasm
|
||||
- check_dependencies
|
||||
- check_docs
|
||||
- check_licenses
|
||||
|
|
@ -668,6 +708,7 @@ jobs:
|
|||
check_result "run_tests_mac" "${{ needs.run_tests_mac.result }}"
|
||||
check_result "doctests" "${{ needs.doctests.result }}"
|
||||
check_result "check_workspace_binaries" "${{ needs.check_workspace_binaries.result }}"
|
||||
check_result "check_wasm" "${{ needs.check_wasm.result }}"
|
||||
check_result "check_dependencies" "${{ needs.check_dependencies.result }}"
|
||||
check_result "check_docs" "${{ needs.check_docs.result }}"
|
||||
check_result "check_licenses" "${{ needs.check_licenses.result }}"
|
||||
|
|
|
|||
111
Cargo.lock
generated
111
Cargo.lock
generated
|
|
@ -3494,6 +3494,16 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
|
|
@ -6243,6 +6253,12 @@ version = "0.4.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.8"
|
||||
|
|
@ -6592,6 +6608,19 @@ dependencies = [
|
|||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-concurrency"
|
||||
version = "7.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6"
|
||||
dependencies = [
|
||||
"fixedbitset 0.5.7",
|
||||
"futures-core",
|
||||
"futures-lite 2.6.1",
|
||||
"pin-project",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
|
|
@ -7416,6 +7445,7 @@ name = "gpui"
|
|||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"async-task",
|
||||
"backtrace",
|
||||
"bindgen 0.71.1",
|
||||
|
|
@ -7439,8 +7469,11 @@ dependencies = [
|
|||
"etagere",
|
||||
"foreign-types 0.5.0",
|
||||
"futures 0.3.31",
|
||||
"futures-concurrency",
|
||||
"getrandom 0.3.4",
|
||||
"gpui_macros",
|
||||
"gpui_platform",
|
||||
"gpui_util",
|
||||
"http_client",
|
||||
"image",
|
||||
"inventory",
|
||||
|
|
@ -7459,6 +7492,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
"pin-project",
|
||||
"pollster 0.4.0",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"profiling",
|
||||
|
|
@ -7474,7 +7508,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"spin 0.10.0",
|
||||
"stacksafe",
|
||||
"strum 0.27.2",
|
||||
|
|
@ -7482,11 +7515,13 @@ dependencies = [
|
|||
"taffy",
|
||||
"thiserror 2.0.17",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"usvg",
|
||||
"util",
|
||||
"util_macros",
|
||||
"uuid",
|
||||
"waker-fn",
|
||||
"wasm-bindgen",
|
||||
"web-time",
|
||||
"windows 0.61.3",
|
||||
"zed-font-kit",
|
||||
"zed-scap",
|
||||
|
|
@ -7504,7 +7539,6 @@ dependencies = [
|
|||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"collections",
|
||||
"cosmic-text",
|
||||
"filedescriptor",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
|
|
@ -7517,6 +7551,7 @@ dependencies = [
|
|||
"open",
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
"pollster 0.4.0",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"smallvec",
|
||||
|
|
@ -7535,7 +7570,6 @@ dependencies = [
|
|||
"x11-clipboard",
|
||||
"x11rb",
|
||||
"xkbcommon",
|
||||
"zed-font-kit",
|
||||
"zed-scap",
|
||||
"zed-xim",
|
||||
]
|
||||
|
|
@ -7596,9 +7630,11 @@ dependencies = [
|
|||
name = "gpui_platform"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"gpui",
|
||||
"gpui_linux",
|
||||
"gpui_macos",
|
||||
"gpui_web",
|
||||
"gpui_windows",
|
||||
]
|
||||
|
||||
|
|
@ -7612,6 +7648,36 @@ dependencies = [
|
|||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui_util"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui_web"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"console_error_panic_hook",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"gpui_wgpu",
|
||||
"js-sys",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"raw-window-handle",
|
||||
"smallvec",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm_thread",
|
||||
"web-sys",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui_wgpu"
|
||||
version = "0.1.0"
|
||||
|
|
@ -7619,15 +7685,24 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bytemuck",
|
||||
"collections",
|
||||
"cosmic-text",
|
||||
"etagere",
|
||||
"gpui",
|
||||
"gpui_util",
|
||||
"itertools 0.14.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pollster 0.4.0",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"smol",
|
||||
"util",
|
||||
"smallvec",
|
||||
"swash",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"wgpu",
|
||||
"zed-font-kit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -12252,7 +12327,7 @@ version = "0.6.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"fixedbitset 0.4.2",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
|
|
@ -12574,6 +12649,12 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "pori"
|
||||
version = "0.0.0"
|
||||
|
|
@ -12631,7 +12712,7 @@ dependencies = [
|
|||
"log",
|
||||
"parking_lot",
|
||||
"pin-project",
|
||||
"pollster",
|
||||
"pollster 0.2.5",
|
||||
"static_assertions",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
|
@ -14801,6 +14882,7 @@ dependencies = [
|
|||
"futures 0.3.31",
|
||||
"parking_lot",
|
||||
"rand 0.9.2",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -18603,6 +18685,7 @@ dependencies = [
|
|||
"futures-lite 1.13.0",
|
||||
"git2",
|
||||
"globset",
|
||||
"gpui_util",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
|
|
@ -19118,6 +19201,18 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm_thread"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e"
|
||||
dependencies = [
|
||||
"futures 0.3.31",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.201.0"
|
||||
|
|
|
|||
31
Cargo.toml
31
Cargo.toml
|
|
@ -1,8 +1,8 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/acp_tools",
|
||||
"crates/acp_thread",
|
||||
"crates/acp_tools",
|
||||
"crates/action_log",
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
|
|
@ -13,9 +13,9 @@ members = [
|
|||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant_text_thread",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_slash_commands",
|
||||
"crates/assistant_text_thread",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_helper",
|
||||
|
|
@ -32,6 +32,7 @@ members = [
|
|||
"crates/cloud_api_client",
|
||||
"crates/cloud_api_types",
|
||||
"crates/cloud_llm_client",
|
||||
"crates/codestral",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
|
|
@ -56,9 +57,10 @@ members = [
|
|||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/edit_prediction",
|
||||
"crates/edit_prediction_cli",
|
||||
"crates/edit_prediction_context",
|
||||
"crates/edit_prediction_types",
|
||||
"crates/edit_prediction_ui",
|
||||
"crates/edit_prediction_context",
|
||||
"crates/editor",
|
||||
"crates/encoding_selector",
|
||||
"crates/etw_tracing",
|
||||
|
|
@ -88,9 +90,11 @@ members = [
|
|||
"crates/gpui_macos",
|
||||
"crates/gpui_macros",
|
||||
"crates/gpui_platform",
|
||||
"crates/gpui_tokio",
|
||||
"crates/gpui_util",
|
||||
"crates/gpui_web",
|
||||
"crates/gpui_wgpu",
|
||||
"crates/gpui_windows",
|
||||
"crates/gpui_tokio",
|
||||
"crates/html_to_markdown",
|
||||
"crates/http_client",
|
||||
"crates/http_client_tls",
|
||||
|
|
@ -119,8 +123,8 @@ members = [
|
|||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/migrator",
|
||||
"crates/mistral",
|
||||
"crates/miniprofiler_ui",
|
||||
"crates/mistral",
|
||||
"crates/multi_buffer",
|
||||
"crates/nc",
|
||||
"crates/net",
|
||||
|
|
@ -136,6 +140,7 @@ members = [
|
|||
"crates/panel",
|
||||
"crates/paths",
|
||||
"crates/picker",
|
||||
"crates/platform_title_bar",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_benchmarks",
|
||||
|
|
@ -147,7 +152,6 @@ members = [
|
|||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/scheduler",
|
||||
"crates/remote",
|
||||
"crates/remote_connection",
|
||||
"crates/remote_server",
|
||||
|
|
@ -157,10 +161,10 @@ members = [
|
|||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rules_library",
|
||||
"crates/scheduler",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
"crates/session",
|
||||
"crates/sidebar",
|
||||
"crates/settings",
|
||||
"crates/settings_content",
|
||||
"crates/settings_json",
|
||||
|
|
@ -168,6 +172,7 @@ members = [
|
|||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/shell_command_parser",
|
||||
"crates/sidebar",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
|
|
@ -179,7 +184,6 @@ members = [
|
|||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/codestral",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
"crates/tab_switcher",
|
||||
|
|
@ -195,7 +199,6 @@ members = [
|
|||
"crates/theme_importer",
|
||||
"crates/theme_selector",
|
||||
"crates/time_format",
|
||||
"crates/platform_title_bar",
|
||||
"crates/title_bar",
|
||||
"crates/toolchain_selector",
|
||||
"crates/ui",
|
||||
|
|
@ -207,10 +210,10 @@ members = [
|
|||
"crates/vercel",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/which_key",
|
||||
"crates/watch",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/which_key",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/worktree_benchmarks",
|
||||
|
|
@ -218,7 +221,6 @@ members = [
|
|||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/edit_prediction_cli",
|
||||
"crates/zeta_prompt",
|
||||
"crates/zlog",
|
||||
"crates/zlog_settings",
|
||||
|
|
@ -332,9 +334,11 @@ gpui_linux = { path = "crates/gpui_linux", default-features = false }
|
|||
gpui_macos = { path = "crates/gpui_macos", default-features = false }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
gpui_platform = { path = "crates/gpui_platform", default-features = false }
|
||||
gpui_web = { path = "crates/gpui_web" }
|
||||
gpui_wgpu = { path = "crates/gpui_wgpu" }
|
||||
gpui_windows = { path = "crates/gpui_windows", default-features = false }
|
||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
gpui_util = { path = "crates/gpui_util" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
http_client_tls = { path = "crates/http_client_tls" }
|
||||
|
|
@ -487,6 +491,7 @@ ashpd = { version = "0.13", default-features = false, features = [
|
|||
"settings",
|
||||
"trash"
|
||||
] }
|
||||
async-channel = "2.5.0"
|
||||
async-compat = "0.2.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
|
|
@ -547,6 +552,7 @@ exec = "0.3.1"
|
|||
fancy-regex = "0.16.0"
|
||||
fork = "0.4.0"
|
||||
futures = "0.3"
|
||||
futures-concurrency = "7.7.1"
|
||||
futures-lite = "1.13"
|
||||
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "c9eac0ed361583e1072860d96776fa52775b82ac" }
|
||||
git2 = { version = "0.20.1", default-features = false, features = ["vendored-libgit2"] }
|
||||
|
|
@ -637,6 +643,7 @@ profiling = "1"
|
|||
prost = "0.9"
|
||||
prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pollster = "0.4.0"
|
||||
pulldown-cmark = { version = "0.13.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.9"
|
||||
|
|
@ -761,6 +768,8 @@ wasmtime = { version = "33", default-features = false, features = [
|
|||
wasmtime-wasi = "33"
|
||||
wax = "0.7"
|
||||
which = "6.0.0"
|
||||
wasm-bindgen = "0.2.104"
|
||||
web-time = "1.1.0"
|
||||
wgpu = "28.0"
|
||||
windows-core = "0.61"
|
||||
yawc = "0.2.5"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ default = ["font-kit", "wayland", "x11", "windows-manifest"]
|
|||
test-support = [
|
||||
"leak-detection",
|
||||
"collections/test-support",
|
||||
"util/test-support",
|
||||
"http_client/test-support",
|
||||
"wayland",
|
||||
"x11",
|
||||
|
|
@ -37,7 +36,7 @@ x11 = [
|
|||
screen-capture = [
|
||||
"scap",
|
||||
]
|
||||
windows-manifest = []
|
||||
windows-manifest = ["dep:embed-resource"]
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
|
|
@ -54,8 +53,8 @@ ctor.workspace = true
|
|||
derive_more.workspace = true
|
||||
etagere = "0.2"
|
||||
futures.workspace = true
|
||||
futures-concurrency.workspace = true
|
||||
gpui_macros.workspace = true
|
||||
http_client.workspace = true
|
||||
image.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
|
|
@ -83,19 +82,29 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
slotmap.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
async-channel.workspace = true
|
||||
stacksafe.workspace = true
|
||||
strum.workspace = true
|
||||
sum_tree.workspace = true
|
||||
taffy = "=0.9.0"
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
gpui_util.workspace = true
|
||||
waker-fn = "1.2.0"
|
||||
lyon = "1.0"
|
||||
pin-project = "1.1.10"
|
||||
circular-buffer.workspace = true
|
||||
spin = "0.10.0"
|
||||
pollster.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
web-time.workspace = true
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
getrandom = { version = "0.3.4", features = ["wasm_js"] }
|
||||
uuid = { workspace = true, features = ["js"] }
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
http_client.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block = "0.1"
|
||||
|
|
@ -135,19 +144,22 @@ backtrace.workspace = true
|
|||
collections = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui_platform.workspace = true
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
lyon = { version = "1.0", features = ["extra"] }
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest_client = { workspace = true, features = ["test-support"] }
|
||||
scheduler = { workspace = true, features = ["test-support"] }
|
||||
unicode-segmentation.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
gpui_util = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
embed-resource = "3.0"
|
||||
[build-dependencies]
|
||||
embed-resource = { version = "3.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
bindgen = "0.71"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
|
||||
#![cfg_attr(not(target_os = "macos"), allow(unused))]
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
|
||||
#[cfg(all(target_os = "windows", feature = "windows-manifest"))]
|
||||
embed_resource();
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
|
||||
if target_os == "windows" {
|
||||
#[cfg(feature = "windows-manifest")]
|
||||
embed_resource();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "windows", feature = "windows-manifest"))]
|
||||
#[cfg(feature = "windows-manifest")]
|
||||
fn embed_resource() {
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -101,7 +103,7 @@ impl Render for AnimationExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().with_assets(Assets {}).run(|cx: &mut App| {
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
|
|
@ -118,3 +120,15 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::{ops::Range, rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
|
|
@ -447,7 +449,7 @@ impl Render for DataTable {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
|
@ -472,3 +474,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, rgb, size,
|
||||
|
|
@ -121,7 +123,7 @@ impl Render for DragDrop {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -136,3 +138,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window,
|
||||
WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
|
||||
|
|
@ -192,7 +194,7 @@ impl Render for Example {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
|
|
@ -213,3 +215,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{App, Context, Render, Window, WindowOptions, div, img, prelude::*};
|
||||
use gpui_platform::application;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -23,8 +25,7 @@ impl Render for GifViewer {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let gif_path =
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif");
|
||||
|
|
@ -40,3 +41,16 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas, div,
|
||||
linear_color_stop, linear_gradient, point, prelude::*, px, size,
|
||||
|
|
@ -243,7 +245,7 @@ impl Render for GradientViewer {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
|
@ -256,3 +258,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
|
@ -64,7 +66,7 @@ impl Render for HolyGrailExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -78,3 +80,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
rgb, size,
|
||||
|
|
@ -87,7 +89,7 @@ impl Render for HelloWorld {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -105,3 +107,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -146,9 +148,7 @@ impl Render for ImageShowcase {
|
|||
|
||||
actions!(image, [Quit]);
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
fn run_example() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
application()
|
||||
|
|
@ -193,3 +193,16 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
App, AppContext, Asset as _, AssetLogger, Bounds, ClickEvent, Context, ElementId, Entity,
|
||||
|
|
@ -245,9 +247,7 @@ impl ImageCache for SimpleLruCache {
|
|||
|
||||
actions!(image, [Quit]);
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
fn run_example() {
|
||||
application().run(move |cx: &mut App| {
|
||||
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
|
@ -287,3 +287,16 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
|
|
@ -192,8 +194,7 @@ impl Render for ImageLoadingExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
fn run_example() {
|
||||
application().with_assets(Assets {}).run(|cx: &mut App| {
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
|
|
@ -210,3 +211,16 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
|
|
@ -682,7 +684,7 @@ impl Render for InputExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.bind_keys([
|
||||
|
|
@ -752,3 +754,15 @@ fn main() {
|
|||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
fn main() {
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
fn run_example() {
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
example::main();
|
||||
|
||||
|
|
@ -6,6 +8,18 @@ fn main() {
|
|||
panic!("This example requires the `wayland` feature and a linux system.");
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
mod example {
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, MousePressureEvent, PressureStage, Window, WindowBounds, WindowOptions,
|
||||
div, prelude::*, px, rgb, size,
|
||||
|
|
@ -44,7 +46,7 @@ impl MousePressureExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
|
||||
|
|
@ -65,3 +67,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds, WindowOptions, actions,
|
||||
div, prelude::*, px, rgb, size,
|
||||
|
|
@ -35,7 +37,7 @@ impl Render for ExampleWindow {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
|
||||
|
|
@ -81,3 +83,15 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -156,7 +158,7 @@ impl Render for HelloWorld {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application()
|
||||
.with_assets(Assets {
|
||||
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
|
||||
|
|
@ -174,3 +176,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{App, Context, Entity, EventEmitter, prelude::*};
|
||||
use gpui_platform::application;
|
||||
|
||||
|
|
@ -11,7 +13,7 @@ struct Change {
|
|||
|
||||
impl EventEmitter<Change> for Counter {}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
|
||||
let subscriber = cx.new(|cx: &mut Context<Counter>| {
|
||||
|
|
@ -34,3 +36,15 @@ fn main() {
|
|||
assert_eq!(subscriber.read(cx).count, 4);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels,
|
||||
Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, linear_color_stop,
|
||||
|
|
@ -445,7 +447,7 @@ impl Render for PaintingViewer {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
|
@ -462,3 +464,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, TitlebarOptions,
|
||||
Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop, linear_gradient, point,
|
||||
|
|
@ -69,7 +71,7 @@ impl Render for PaintingViewer {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
|
|
@ -91,3 +93,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, Context, Window, WindowBounds, WindowOptions, div, linear_color_stop,
|
||||
linear_gradient, pattern_slash, prelude::*, px, rgb, size,
|
||||
|
|
@ -99,7 +101,7 @@ impl Render for PatternExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -114,3 +116,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Context, Corner, Div, Hsla, Stateful, Window, WindowOptions, anchored, deferred, div,
|
||||
prelude::*, px,
|
||||
|
|
@ -161,7 +163,7 @@ impl Render for HelloWorld {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.open_window(WindowOptions::default(), |_, cx| {
|
||||
cx.new(|_| HelloWorld {
|
||||
|
|
@ -173,3 +175,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, size};
|
||||
use gpui_platform::application;
|
||||
|
||||
|
|
@ -42,7 +44,7 @@ impl Render for Scrollable {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -56,3 +58,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window, WindowOptions,
|
||||
actions, div, prelude::*, rgb,
|
||||
|
|
@ -20,7 +22,7 @@ impl Render for SetMenus {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.set_global(AppState::new());
|
||||
|
||||
|
|
@ -36,6 +38,18 @@ fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum ViewMode {
|
||||
List,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions, div,
|
||||
hsla, point, prelude::*, px, relative, rgb, size,
|
||||
|
|
@ -569,7 +571,7 @@ impl Render for Shadow {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -584,3 +586,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
|
@ -68,7 +70,7 @@ impl Render for SvgExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application()
|
||||
.with_assets(Assets {
|
||||
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
|
||||
|
|
@ -86,3 +88,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window,
|
||||
WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
|
||||
|
|
@ -178,7 +180,7 @@ impl Render for Example {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
|
|
@ -198,3 +200,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
//! Example demonstrating GPUI's testing infrastructure.
|
||||
//!
|
||||
//! When run normally, this displays an interactive counter window.
|
||||
|
|
@ -176,7 +177,7 @@ impl Render for Counter {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.bind_keys([
|
||||
gpui::KeyBinding::new("up", Increment, Some("Counter")),
|
||||
|
|
@ -199,6 +200,18 @@ fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
|
|
@ -298,7 +300,7 @@ impl Render for TextExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "GPUI Typography".into(),
|
||||
|
|
@ -332,3 +334,15 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds, WindowOptions,
|
||||
div, prelude::*, px, size,
|
||||
|
|
@ -81,7 +83,7 @@ impl Render for HelloWorld {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -95,3 +97,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, TextOverflow, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
size,
|
||||
|
|
@ -108,7 +110,7 @@ impl Render for HelloWorld {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -122,3 +124,15 @@ fn main() {
|
|||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
//! Renders a div with deep children hierarchy. This example is useful to exemplify that Zed can
|
||||
//! handle deep hierarchies (even though it cannot just yet!).
|
||||
use std::sync::LazyLock;
|
||||
|
|
@ -29,7 +30,7 @@ impl Render for Tree {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -42,3 +43,15 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
|
||||
uniform_list,
|
||||
|
|
@ -36,7 +38,7 @@ impl Render for UniformListExample {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -49,3 +51,15 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds, WindowKind,
|
||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
|
|
@ -306,7 +308,7 @@ impl Render for WindowDemo {
|
|||
|
||||
actions!(window, [Quit]);
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
|
||||
|
|
@ -333,3 +335,15 @@ fn main() {
|
|||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
|
||||
|
|
@ -68,7 +70,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
// Create several new windows, positioned in the top right corner of each screen
|
||||
let size = Size {
|
||||
|
|
@ -218,3 +220,15 @@ fn main() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton, Pixels,
|
||||
Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
|
||||
|
|
@ -203,7 +205,7 @@ fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> O
|
|||
Some(edge)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
|
|
@ -226,3 +228,15 @@ fn main() {
|
|||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use scheduler::Instant;
|
||||
use std::{
|
||||
any::{TypeId, type_name},
|
||||
cell::{BorrowMutError, Cell, Ref, RefCell, RefMut},
|
||||
|
|
@ -7,7 +8,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{Arc, atomic::Ordering::SeqCst},
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
|
|
@ -25,11 +26,12 @@ pub use async_context::*;
|
|||
use collections::{FxHashMap, FxHashSet, HashMap, VecDeque};
|
||||
pub use context::*;
|
||||
pub use entity_map::*;
|
||||
use gpui_util::{ResultExt, debug_panic};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use http_client::{HttpClient, Url};
|
||||
use smallvec::SmallVec;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_context::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
pub use visual_test_context::*;
|
||||
|
||||
|
|
@ -137,6 +139,7 @@ impl Application {
|
|||
Self(App::new_app(
|
||||
platform,
|
||||
Arc::new(()),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
Arc::new(NullHttpClient),
|
||||
))
|
||||
}
|
||||
|
|
@ -152,6 +155,7 @@ impl Application {
|
|||
}
|
||||
|
||||
/// Sets the HTTP client for the application.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn with_http_client(self, http_client: Arc<dyn HttpClient>) -> Self {
|
||||
let mut context_lock = self.0.borrow_mut();
|
||||
context_lock.http_client = http_client;
|
||||
|
|
@ -581,6 +585,7 @@ pub struct App {
|
|||
pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box<dyn Any>>,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
pub(crate) svg_renderer: SvgRenderer,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
pub(crate) globals_by_type: FxHashMap<TypeId, Box<dyn Any>>,
|
||||
pub(crate) entities: EntityMap,
|
||||
|
|
@ -637,7 +642,7 @@ impl App {
|
|||
pub(crate) fn new_app(
|
||||
platform: Rc<dyn Platform>,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
#[cfg(not(target_family = "wasm"))] http_client: Arc<dyn HttpClient>,
|
||||
) -> Rc<AppCell> {
|
||||
let background_executor = platform.background_executor();
|
||||
let foreground_executor = platform.foreground_executor();
|
||||
|
|
@ -667,6 +672,7 @@ impl App {
|
|||
svg_renderer: SvgRenderer::new(asset_source.clone()),
|
||||
loading_assets: Default::default(),
|
||||
asset_source,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
http_client,
|
||||
globals_by_type: FxHashMap::default(),
|
||||
entities,
|
||||
|
|
@ -1275,11 +1281,13 @@ impl App {
|
|||
}
|
||||
|
||||
/// Returns the HTTP client for the application.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn http_client(&self) -> Arc<dyn HttpClient> {
|
||||
self.http_client.clone()
|
||||
}
|
||||
|
||||
/// Sets the HTTP client for the application.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn set_http_client(&mut self, new_client: Arc<dyn HttpClient>) {
|
||||
self.http_client = new_client;
|
||||
}
|
||||
|
|
@ -2504,8 +2512,10 @@ pub struct KeystrokeEvent {
|
|||
pub context_stack: Vec<KeyContext>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
struct NullHttpClient;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl HttpClient for NullHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
use anyhow::Context as _;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures::channel::oneshot;
|
||||
use smol::future::FutureExt;
|
||||
use futures::future::FutureExt;
|
||||
use std::{future::Future, rc::Weak};
|
||||
|
||||
use super::{Context, WeakEntity};
|
||||
|
|
@ -241,10 +241,10 @@ impl AsyncApp {
|
|||
&self,
|
||||
entity: &WeakEntity<T>,
|
||||
f: Callback,
|
||||
) -> util::Deferred<impl FnOnce() + use<T, Callback>> {
|
||||
) -> gpui_util::Deferred<impl FnOnce() + use<T, Callback>> {
|
||||
let entity = entity.clone();
|
||||
let mut cx = self.clone();
|
||||
util::defer(move || {
|
||||
gpui_util::defer(move || {
|
||||
entity.update(&mut cx, f).ok();
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use crate::{
|
|||
};
|
||||
use anyhow::Result;
|
||||
use futures::FutureExt;
|
||||
use gpui_util::Deferred;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::{Borrow, BorrowMut},
|
||||
|
|
@ -12,7 +13,6 @@ use std::{
|
|||
ops,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::Deferred;
|
||||
|
||||
use super::{App, AsyncWindowContext, Entity, KeystrokeEvent};
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ impl<'a, T: 'static> Context<'a, T> {
|
|||
) -> Deferred<impl FnOnce()> {
|
||||
let this = self.weak_entity();
|
||||
let mut cx = self.to_async();
|
||||
util::defer(move || {
|
||||
gpui_util::defer(move || {
|
||||
this.update(&mut cx, f).ok();
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -904,7 +904,7 @@ impl LeakDetector {
|
|||
/// at the allocation site.
|
||||
#[track_caller]
|
||||
pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId {
|
||||
let id = util::post_inc(&mut self.next_handle_id);
|
||||
let id = gpui_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(
|
||||
|
|
|
|||
|
|
@ -120,10 +120,16 @@ impl TestAppContext {
|
|||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
|
||||
let app = App::new_app(platform.clone(), asset_source, http_client);
|
||||
let app = App::new_app(
|
||||
platform.clone(),
|
||||
asset_source,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
http_client,
|
||||
);
|
||||
app.borrow_mut().mode = GpuiMode::test();
|
||||
|
||||
Self {
|
||||
|
|
@ -521,22 +527,25 @@ impl TestAppContext {
|
|||
let mut notifications = self.notifications(entity);
|
||||
|
||||
use futures::FutureExt as _;
|
||||
use smol::future::FutureExt as _;
|
||||
use futures_concurrency::future::Race as _;
|
||||
|
||||
async {
|
||||
loop {
|
||||
if entity.update(self, &mut predicate) {
|
||||
return Ok(());
|
||||
}
|
||||
(
|
||||
async {
|
||||
loop {
|
||||
if entity.update(self, &mut predicate) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if notifications.next().await.is_none() {
|
||||
bail!("entity dropped")
|
||||
if notifications.next().await.is_none() {
|
||||
bail!("entity dropped")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.race(timer.map(|_| Err(anyhow!("condition timed out"))))
|
||||
.await
|
||||
.unwrap();
|
||||
},
|
||||
timer.map(|_| Err(anyhow!("condition timed out"))),
|
||||
)
|
||||
.race()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Set a name for this App.
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@ impl VisualTestAppContext {
|
|||
predicate: impl Fn(&T) -> bool,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let start = std::time::Instant::now();
|
||||
let start = web_time::Instant::now();
|
||||
loop {
|
||||
{
|
||||
let app = self.app.borrow();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use std::{
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use scheduler::Instant;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use crate::{
|
|||
size,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use gpui_util::ResultExt;
|
||||
use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use stacksafe::{StackSafe, stacksafe};
|
||||
|
|
@ -40,7 +41,6 @@ use std::{
|
|||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use super::ImageCacheProvider;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ use crate::{
|
|||
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
|
||||
SharedString, SharedUri, StyleRefinement, Styled, Task, Window, px,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use futures::Future;
|
||||
use gpui_util::ResultExt;
|
||||
use image::{
|
||||
AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba,
|
||||
codecs::{gif::GifDecoder, webp::WebPDecoder},
|
||||
};
|
||||
use scheduler::Instant;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fs,
|
||||
|
|
@ -19,10 +21,9 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
use super::{Stateful, StatefulInteractiveElement};
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ pub enum ImageSource {
|
|||
}
|
||||
|
||||
fn is_uri(uri: &str) -> bool {
|
||||
http_client::Uri::from_str(uri).is_ok()
|
||||
url::Url::from_str(uri).is_ok()
|
||||
}
|
||||
|
||||
impl From<SharedUri> for ImageSource {
|
||||
|
|
@ -593,6 +594,7 @@ impl Asset for ImageAssetLoader {
|
|||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let client = cx.http_client();
|
||||
// TODO: Can we make SVGs always rescale?
|
||||
// let scale_factor = cx.scale_factor();
|
||||
|
|
@ -601,7 +603,11 @@ impl Asset for ImageAssetLoader {
|
|||
async move {
|
||||
let bytes = match source.clone() {
|
||||
Resource::Path(uri) => fs::read(uri.as_ref())?,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
Resource::Uri(uri) => {
|
||||
use anyhow::Context as _;
|
||||
use futures::AsyncReadExt as _;
|
||||
|
||||
let mut response = client
|
||||
.get(uri.as_ref(), ().into(), true)
|
||||
.await
|
||||
|
|
@ -620,6 +626,12 @@ impl Asset for ImageAssetLoader {
|
|||
}
|
||||
body
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
Resource::Uri(_) => {
|
||||
return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
|
||||
"Uri resources are not supported on wasm"
|
||||
))));
|
||||
}
|
||||
Resource::Embedded(path) => {
|
||||
let data = asset_source.load(&path).ok().flatten();
|
||||
if let Some(data) = data {
|
||||
|
|
@ -710,6 +722,7 @@ pub enum ImageCacheError {
|
|||
#[error("IO error: {0}")]
|
||||
Io(Arc<std::io::Error>),
|
||||
/// An error that occurred while processing an image.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[error("unexpected http status for {uri}: {status}, body: {body}")]
|
||||
BadStatus {
|
||||
/// The URI of the image.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
|
||||
radians, size,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use gpui_util::ResultExt;
|
||||
|
||||
/// An SVG element.
|
||||
pub struct Svg {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use gpui_util::ResultExt;
|
||||
use itertools::Itertools;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
|
|
@ -16,7 +17,6 @@ use std::{
|
|||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
impl Element for &'static str {
|
||||
type RequestLayoutState = TextLayout;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
use crate::{App, PlatformDispatcher, PlatformScheduler};
|
||||
use futures::channel::mpsc;
|
||||
use futures::prelude::*;
|
||||
use gpui_util::TryFutureExt;
|
||||
use scheduler::Instant;
|
||||
use scheduler::Scheduler;
|
||||
use smol::prelude::*;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
fmt::Debug, future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
|
||||
pub use scheduler::{FallibleTask, ForegroundExecutor as SchedulerForegroundExecutor, Priority};
|
||||
|
||||
|
|
@ -569,9 +564,15 @@ mod test {
|
|||
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor);
|
||||
let asset_source = Arc::new(());
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
let app = App::new_app(
|
||||
platform,
|
||||
asset_source,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
http_client,
|
||||
);
|
||||
(dispatcher, background_executor, app)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ mod platform;
|
|||
pub mod prelude;
|
||||
/// Profiling utilities for task timing and thread performance tracking.
|
||||
pub mod profiler;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))]
|
||||
#[expect(missing_docs)]
|
||||
pub mod queue;
|
||||
mod scene;
|
||||
|
|
@ -87,6 +87,8 @@ pub use executor::*;
|
|||
pub use geometry::*;
|
||||
pub use global::*;
|
||||
pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test};
|
||||
pub use gpui_util::arc_cow::ArcCow;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub use http_client;
|
||||
pub use input::*;
|
||||
pub use inspector::*;
|
||||
|
|
@ -96,7 +98,7 @@ pub use keymap::*;
|
|||
pub use path_builder::*;
|
||||
pub use platform::*;
|
||||
pub use profiler::*;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))]
|
||||
pub use queue::{PriorityQueueReceiver, PriorityQueueSender};
|
||||
pub use refineable::*;
|
||||
pub use scene::*;
|
||||
|
|
@ -113,7 +115,7 @@ pub use taffy::{AvailableSpace, LayoutId};
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test::*;
|
||||
pub use text_system::*;
|
||||
pub use util::{FutureExt, Timeout, arc_cow::ArcCow};
|
||||
pub use util::{FutureExt, Timeout};
|
||||
pub use view::*;
|
||||
pub use window::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ use image::RgbaImage;
|
|||
use image::codecs::gif::GifDecoder;
|
||||
use image::{AnimationDecoder as _, Frame};
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
use scheduler::Instant;
|
||||
pub use scheduler::RunnableMeta;
|
||||
use schemars::JsonSchema;
|
||||
use seahash::SeaHasher;
|
||||
|
|
@ -53,7 +54,7 @@ use std::borrow::Cow;
|
|||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Cursor;
|
||||
use std::ops;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
|
|
@ -560,7 +561,7 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
pub type RunnableVariant = Runnable<RunnableMeta>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub type TimerResolutionGuard = util::Deferred<Box<dyn FnOnce() + Send>>;
|
||||
pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
|
||||
|
||||
/// 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.
|
||||
|
|
@ -579,7 +580,7 @@ pub trait PlatformDispatcher: Send + Sync {
|
|||
}
|
||||
|
||||
fn increase_timer_resolution(&self) -> TimerResolutionGuard {
|
||||
util::defer(Box::new(|| {}))
|
||||
gpui_util::defer(Box::new(|| {}))
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
|
@ -827,7 +828,7 @@ impl From<RenderImageParams> for AtlasKey {
|
|||
}
|
||||
|
||||
#[expect(missing_docs)]
|
||||
pub trait PlatformAtlas: Send + Sync {
|
||||
pub trait PlatformAtlas {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
key: &AtlasKey,
|
||||
|
|
@ -1235,7 +1236,7 @@ pub struct WindowOptions {
|
|||
),
|
||||
allow(dead_code)
|
||||
)]
|
||||
#[expect(missing_docs)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct WindowParams {
|
||||
pub bounds: Bounds<Pixels>,
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ fn start_default_target_screen_capture(
|
|||
) {
|
||||
// Due to use of blocking APIs, a dedicated thread is used.
|
||||
std::thread::spawn(|| {
|
||||
let start_result = util::maybe!({
|
||||
let start_result = gpui_util::maybe!({
|
||||
let mut capturer = new_scap_capturer(None)?;
|
||||
capturer.start_capture();
|
||||
let first_frame = capturer
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use crate::{PlatformDispatcher, Priority, RunnableVariant};
|
||||
use scheduler::Instant;
|
||||
use scheduler::{Clock, Scheduler, SessionId, TestScheduler, TestSchedulerConfig, Yield};
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// TestDispatcher provides deterministic async execution for tests.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub(crate) struct TestWindowState {
|
|||
pub(crate) title: Option<String>,
|
||||
pub(crate) edited: bool,
|
||||
platform: Weak<TestPlatform>,
|
||||
// TODO: Replace with `Rc`
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
|
||||
hit_test_window_control_callback: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ use crate::{PlatformDispatcher, RunnableMeta};
|
|||
use async_task::Runnable;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::channel::oneshot;
|
||||
use scheduler::Instant;
|
||||
use scheduler::{Clock, Priority, Scheduler, SessionId, TestScheduler, Timer};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
|
|
@ -10,10 +13,8 @@ use std::{
|
|||
Arc,
|
||||
atomic::{AtomicU16, Ordering},
|
||||
},
|
||||
task::{Context, Poll},
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
use waker_fn::waker_fn;
|
||||
|
||||
/// A production implementation of [`Scheduler`] that wraps a [`PlatformDispatcher`].
|
||||
///
|
||||
|
|
@ -43,37 +44,48 @@ impl Scheduler for PlatformScheduler {
|
|||
fn block(
|
||||
&self,
|
||||
_session_id: Option<SessionId>,
|
||||
mut future: Pin<&mut dyn Future<Output = ()>>,
|
||||
timeout: Option<Duration>,
|
||||
#[cfg_attr(target_family = "wasm", allow(unused_mut))] mut future: Pin<
|
||||
&mut dyn Future<Output = ()>,
|
||||
>,
|
||||
#[cfg_attr(target_family = "wasm", allow(unused_variables))] timeout: Option<Duration>,
|
||||
) -> bool {
|
||||
let deadline = timeout.map(|t| Instant::now() + t);
|
||||
let parker = parking::Parker::new();
|
||||
let unparker = parker.unparker();
|
||||
let waker = waker_fn(move || {
|
||||
unparker.unpark();
|
||||
});
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
|
||||
return true;
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
let _ = (&future, &timeout);
|
||||
panic!("Cannot block on wasm")
|
||||
}
|
||||
|
||||
let park_deadline = |deadline: Instant| {
|
||||
// Timer expirations are only delivered every ~15.6 milliseconds by default on Windows.
|
||||
// We increase the resolution during this wait so that short timeouts stay reasonably short.
|
||||
let _timer_guard = self.dispatcher.increase_timer_resolution();
|
||||
parker.park_deadline(deadline)
|
||||
};
|
||||
|
||||
loop {
|
||||
match deadline {
|
||||
Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => {
|
||||
return false;
|
||||
}
|
||||
Some(_) => (),
|
||||
None => parker.park(),
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
use waker_fn::waker_fn;
|
||||
let deadline = timeout.map(|t| Instant::now() + t);
|
||||
let parker = parking::Parker::new();
|
||||
let unparker = parker.unparker();
|
||||
let waker = waker_fn(move || {
|
||||
unparker.unpark();
|
||||
});
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
|
||||
break true;
|
||||
return true;
|
||||
}
|
||||
|
||||
let park_deadline = |deadline: Instant| {
|
||||
// Timer expirations are only delivered every ~15.6 milliseconds by default on Windows.
|
||||
// We increase the resolution during this wait so that short timeouts stay reasonably short.
|
||||
let _timer_guard = self.dispatcher.increase_timer_resolution();
|
||||
parker.park_deadline(deadline)
|
||||
};
|
||||
|
||||
loop {
|
||||
match deadline {
|
||||
Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => {
|
||||
return false;
|
||||
}
|
||||
Some(_) => (),
|
||||
None => parker.park(),
|
||||
}
|
||||
if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
|
||||
break true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use scheduler::Instant;
|
||||
use std::{
|
||||
cell::LazyCell,
|
||||
collections::HashMap,
|
||||
|
|
@ -5,7 +6,6 @@ use std::{
|
|||
hash::{DefaultHasher, Hash},
|
||||
sync::Arc,
|
||||
thread::ThreadId,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
|
|||
|
|
@ -41,6 +41,32 @@ impl<T> PriorityQueueState<T> {
|
|||
}
|
||||
|
||||
let mut queues = self.queues.lock();
|
||||
Self::push(&mut queues, priority, item);
|
||||
self.condvar.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spin_send(&self, priority: Priority, item: T) -> Result<(), SendError<T>> {
|
||||
if self
|
||||
.receiver_count
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
== 0
|
||||
{
|
||||
return Err(SendError(item));
|
||||
}
|
||||
|
||||
let mut queues = loop {
|
||||
if let Some(guard) = self.queues.try_lock() {
|
||||
break guard;
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
};
|
||||
Self::push(&mut queues, priority, item);
|
||||
self.condvar.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push(queues: &mut PriorityQueues<T>, priority: Priority, item: T) {
|
||||
match priority {
|
||||
Priority::RealtimeAudio => unreachable!(
|
||||
"Realtime audio priority runs on a dedicated thread and is never queued"
|
||||
|
|
@ -49,8 +75,6 @@ impl<T> PriorityQueueState<T> {
|
|||
Priority::Medium => queues.medium_priority.push_back(item),
|
||||
Priority::Low => queues.low_priority.push_back(item),
|
||||
};
|
||||
self.condvar.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv<'a>(&'a self) -> Result<parking_lot::MutexGuard<'a, PriorityQueues<T>>, RecvError> {
|
||||
|
|
@ -84,6 +108,28 @@ impl<T> PriorityQueueState<T> {
|
|||
Ok(Some(queues))
|
||||
}
|
||||
}
|
||||
|
||||
fn spin_try_recv<'a>(
|
||||
&'a self,
|
||||
) -> Result<Option<parking_lot::MutexGuard<'a, PriorityQueues<T>>>, RecvError> {
|
||||
let queues = loop {
|
||||
if let Some(guard) = self.queues.try_lock() {
|
||||
break guard;
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
};
|
||||
|
||||
let sender_count = self.sender_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if queues.is_empty() && sender_count == 0 {
|
||||
return Err(crate::queue::RecvError);
|
||||
}
|
||||
|
||||
if queues.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(queues))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
|
@ -100,6 +146,11 @@ impl<T> PriorityQueueSender<T> {
|
|||
self.state.send(priority, item)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn spin_send(&self, priority: Priority, item: T) -> Result<(), SendError<T>> {
|
||||
self.state.spin_send(priority, item)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for PriorityQueueSender<T> {
|
||||
|
|
@ -183,6 +234,44 @@ impl<T> PriorityQueueReceiver<T> {
|
|||
self.pop_inner(false)
|
||||
}
|
||||
|
||||
pub fn spin_try_pop(&mut self) -> Result<Option<T>, RecvError> {
|
||||
use Priority as P;
|
||||
|
||||
let Some(mut queues) = self.state.spin_try_recv()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let high = P::High.weight() * !queues.high_priority.is_empty() as u32;
|
||||
let medium = P::Medium.weight() * !queues.medium_priority.is_empty() as u32;
|
||||
let low = P::Low.weight() * !queues.low_priority.is_empty() as u32;
|
||||
let mut mass = high + medium + low;
|
||||
|
||||
if !queues.high_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::High.weight(), mass);
|
||||
if flip {
|
||||
return Ok(queues.high_priority.pop_front());
|
||||
}
|
||||
mass -= P::High.weight();
|
||||
}
|
||||
|
||||
if !queues.medium_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::Medium.weight(), mass);
|
||||
if flip {
|
||||
return Ok(queues.medium_priority.pop_front());
|
||||
}
|
||||
mass -= P::Medium.weight();
|
||||
}
|
||||
|
||||
if !queues.low_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::Low.weight(), mass);
|
||||
if flip {
|
||||
return Ok(queues.low_priority.pop_front());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Pops an element from the priority queue blocking if necessary.
|
||||
///
|
||||
/// This method is best suited if you only intend to pop one element, for better performance
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
|||
),
|
||||
allow(dead_code)
|
||||
)]
|
||||
#[expect(missing_docs)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum PrimitiveBatch {
|
||||
Shadows(Range<usize>),
|
||||
Quads(Range<usize>),
|
||||
|
|
@ -711,7 +711,7 @@ impl From<PolychromeSprite> for Primitive {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[expect(missing_docs)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct PaintSurface {
|
||||
pub order: DrawOrder,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
use gpui_util::arc_cow::ArcCow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
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<str>` and `&'static str`,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use collections::{BTreeMap, BTreeSet};
|
||||
use gpui_util::post_inc;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Debug,
|
||||
mem,
|
||||
rc::Rc,
|
||||
};
|
||||
use util::post_inc;
|
||||
|
||||
pub(crate) struct SubscriberSet<EmitterKey, Callback>(
|
||||
Rc<RefCell<SubscriberSetState<EmitterKey, Callback>>>,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
//! ```
|
||||
use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
|
||||
use futures::StreamExt as _;
|
||||
use smol::channel;
|
||||
use std::{
|
||||
env,
|
||||
panic::{self, RefUnwindSafe},
|
||||
|
|
@ -136,7 +135,7 @@ fn calculate_seeds(
|
|||
|
||||
/// A test struct for converting an observation callback into a stream.
|
||||
pub struct Observation<T> {
|
||||
rx: Pin<Box<channel::Receiver<T>>>,
|
||||
rx: Pin<Box<async_channel::Receiver<T>>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
|
|
@ -153,10 +152,10 @@ impl<T: 'static> futures::Stream for Observation<T> {
|
|||
|
||||
/// observe returns a stream of the change events from the given `Entity`
|
||||
pub fn observe<T: 'static>(entity: &Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
let _subscription = cx.update(|cx| {
|
||||
cx.observe(entity, move |_, _| {
|
||||
let _ = smol::block_on(tx.send(()));
|
||||
let _ = pollster::block_on(tx.send(()));
|
||||
})
|
||||
});
|
||||
let rx = Box::pin(rx);
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -26,11 +26,14 @@ use core_video::pixel_buffer::CVPixelBuffer;
|
|||
use derive_more::{Deref, DerefMut};
|
||||
use futures::FutureExt;
|
||||
use futures::channel::oneshot;
|
||||
use gpui_util::post_inc;
|
||||
use gpui_util::{ResultExt, measure};
|
||||
use itertools::FoldWhile::{Continue, Done};
|
||||
use itertools::Itertools;
|
||||
use parking_lot::RwLock;
|
||||
use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle};
|
||||
use refineable::Refineable;
|
||||
use scheduler::Instant;
|
||||
use slotmap::SlotMap;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
|
|
@ -48,10 +51,8 @@ use std::{
|
|||
Arc, Weak,
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
use util::post_inc;
|
||||
use util::{ResultExt, measure};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod prompts;
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ wayland = [
|
|||
"bitflags",
|
||||
"gpui_wgpu",
|
||||
"ashpd/wayland",
|
||||
"cosmic-text",
|
||||
"font-kit",
|
||||
|
||||
"calloop-wayland-source",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
|
|
@ -35,8 +34,7 @@ wayland = [
|
|||
x11 = [
|
||||
"gpui_wgpu",
|
||||
"ashpd",
|
||||
"cosmic-text",
|
||||
"font-kit",
|
||||
|
||||
"as-raw-xcb-connection",
|
||||
"x11rb",
|
||||
"xkbcommon",
|
||||
|
|
@ -58,13 +56,14 @@ bytemuck = "1"
|
|||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_wgpu = { workspace = true, optional = true }
|
||||
gpui_wgpu = { workspace = true, optional = true, features = ["font-kit"] }
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pathfinder_geometry = "0.5"
|
||||
pollster.workspace = true
|
||||
profiling.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
|
|
@ -83,12 +82,7 @@ raw-window-handle = "0.6"
|
|||
|
||||
# Used in both windowing options
|
||||
ashpd = { workspace = true, optional = true }
|
||||
cosmic-text = { version = "0.17.0", optional = true }
|
||||
swash = { version = "0.2.6" }
|
||||
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [
|
||||
"source-fontconfig-dlopen",
|
||||
], optional = true }
|
||||
bitflags = { workspace = true, optional = true }
|
||||
filedescriptor = { version = "0.8.2", optional = true }
|
||||
open = { version = "5.2.0", optional = true }
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ impl LinuxCommon {
|
|||
let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
let text_system = Arc::new(crate::linux::CosmicTextSystem::new());
|
||||
let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
|
||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||
let text_system = Arc::new(gpui::NoopTextSystem::new());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,538 +1 @@
|
|||
use anyhow::{Context as _, Ok, Result};
|
||||
use collections::HashMap;
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
|
||||
FontSystem, ShapeBuffer, ShapeLine,
|
||||
};
|
||||
use gpui::{
|
||||
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, GlyphId, LineLayout,
|
||||
Pixels, PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y,
|
||||
ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point, size,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use parking_lot::RwLock;
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use swash::{
|
||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||
zeno::{Format, Vector},
|
||||
};
|
||||
|
||||
pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct FontKey {
|
||||
family: SharedString,
|
||||
features: FontFeatures,
|
||||
}
|
||||
|
||||
impl FontKey {
|
||||
fn new(family: SharedString, features: FontFeatures) -> Self {
|
||||
Self { family, features }
|
||||
}
|
||||
}
|
||||
|
||||
struct CosmicTextSystemState {
|
||||
font_system: FontSystem,
|
||||
scratch: ShapeBuffer,
|
||||
swash_scale_context: ScaleContext,
|
||||
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
|
||||
loaded_fonts: Vec<LoadedFont>,
|
||||
/// 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<FontKey, SmallVec<[FontId; 4]>>,
|
||||
}
|
||||
|
||||
struct LoadedFont {
|
||||
font: Arc<CosmicTextFont>,
|
||||
features: CosmicFontFeatures,
|
||||
is_known_emoji_font: bool,
|
||||
}
|
||||
|
||||
impl CosmicTextSystem {
|
||||
pub(crate) fn new() -> Self {
|
||||
// todo(linux) make font loading non-blocking
|
||||
let font_system = FontSystem::new();
|
||||
|
||||
Self(RwLock::new(CosmicTextSystemState {
|
||||
font_system,
|
||||
scratch: ShapeBuffer::default(),
|
||||
swash_scale_context: ScaleContext::new(),
|
||||
loaded_fonts: Vec::new(),
|
||||
font_ids_by_family_cache: HashMap::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CosmicTextSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformTextSystem for CosmicTextSystem {
|
||||
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
self.0.write().add_fonts(fonts)
|
||||
}
|
||||
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
let mut result = self
|
||||
.0
|
||||
.read()
|
||||
.font_system
|
||||
.db()
|
||||
.faces()
|
||||
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
|
||||
.collect_vec();
|
||||
result.sort();
|
||||
result.dedup();
|
||||
result
|
||||
}
|
||||
|
||||
fn font_id(&self, font: &Font) -> Result<FontId> {
|
||||
// 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 key = FontKey::new(font.family.clone(), font.features.clone());
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||
state.font_ids_by_family_cache.insert(key.clone(), font_ids);
|
||||
state.font_ids_by_family_cache[&key].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_font(*font_id).font.id();
|
||||
let face_info = state.font_system.db().face(database_id).expect("");
|
||||
face_info_into_properties(face_info)
|
||||
})
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
|
||||
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_font(font_id)
|
||||
.font
|
||||
.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<Bounds<f32>> {
|
||||
let lock = self.0.read();
|
||||
let glyph_metrics = lock.loaded_font(font_id).font.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<Size<f32>> {
|
||||
self.0.read().advance(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.0.read().glyph_for_char(font_id, ch)
|
||||
}
|
||||
|
||||
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
self.0.write().raster_bounds(params)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn recommended_rendering_mode(
|
||||
&self,
|
||||
_font_id: FontId,
|
||||
_font_size: Pixels,
|
||||
) -> TextRenderingMode {
|
||||
// Ideally, we'd use fontconfig to read the user preference.
|
||||
TextRenderingMode::Subpixel
|
||||
}
|
||||
}
|
||||
|
||||
impl CosmicTextSystemState {
|
||||
fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
|
||||
&self.loaded_fonts[font_id.0]
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> 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(())
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
// TODO: Determine the proper system UI font.
|
||||
let name = gpui::font_name_with_fallbacks(name, "IBM Plex Sans");
|
||||
|
||||
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::<SmallVec<[_; 4]>>();
|
||||
|
||||
let mut loaded_font_ids = SmallVec::new();
|
||||
for (font_id, postscript_name) in families {
|
||||
let font = self
|
||||
.font_system
|
||||
.get_font(font_id, cosmic_text::Weight::NORMAL)
|
||||
.context("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.len());
|
||||
loaded_font_ids.push(font_id);
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: cosmic_font_features(features)?,
|
||||
is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(loaded_font_ids)
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
|
||||
Ok(Size {
|
||||
width: glyph_metrics.advance_width(glyph_id.0 as u16),
|
||||
height: glyph_metrics.advance_height(glyph_id.0 as u16),
|
||||
})
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
|
||||
if glyph_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(GlyphId(glyph_id.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let image = self.render_glyph_image(params)?;
|
||||
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<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
|
||||
anyhow::bail!("glyph bounds are empty");
|
||||
}
|
||||
|
||||
let mut image = self.render_glyph_image(params)?;
|
||||
let bitmap_size = glyph_bounds.size;
|
||||
match image.content {
|
||||
swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in image.data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
Ok((bitmap_size, image.data))
|
||||
}
|
||||
swash::scale::image::Content::Mask => Ok((bitmap_size, image.data)),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_glyph_image(
|
||||
&mut self,
|
||||
params: &RenderGlyphParams,
|
||||
) -> Result<swash::scale::image::Image> {
|
||||
let loaded_font = &self.loaded_fonts[params.font_id.0];
|
||||
let font_ref = loaded_font.font.as_swash();
|
||||
let pixel_size = f32::from(params.font_size);
|
||||
|
||||
let subpixel_offset = Vector::new(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
);
|
||||
|
||||
let mut scaler = self
|
||||
.swash_scale_context
|
||||
.builder(font_ref)
|
||||
.size(pixel_size * params.scale_factor)
|
||||
.hint(true)
|
||||
.build();
|
||||
|
||||
let sources: &[Source] = if params.is_emoji {
|
||||
&[
|
||||
Source::ColorOutline(0),
|
||||
Source::ColorBitmap(StrikeWith::BestFit),
|
||||
Source::Outline,
|
||||
]
|
||||
} else {
|
||||
&[Source::Outline]
|
||||
};
|
||||
|
||||
let mut renderer = Render::new(sources);
|
||||
if params.subpixel_rendering {
|
||||
// There seems to be a bug in Swash where the B and R values are swapped.
|
||||
renderer
|
||||
.format(Format::subpixel_bgra())
|
||||
.offset(subpixel_offset);
|
||||
} else {
|
||||
renderer.format(Format::Alpha).offset(subpixel_offset);
|
||||
}
|
||||
|
||||
let glyph_id: u16 = params.glyph_id.0.try_into()?;
|
||||
renderer
|
||||
.render(&mut scaler, glyph_id)
|
||||
.with_context(|| format!("unable to render glyph via swash for {params:?}"))
|
||||
}
|
||||
|
||||
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
|
||||
/// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
|
||||
/// yet have an entry for this fallback font, and so one is added.
|
||||
///
|
||||
/// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
|
||||
/// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
|
||||
/// current use of this field is for the *input* of `layout_line`, and so it's fine to use
|
||||
/// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
|
||||
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
|
||||
if let Some(ix) = self
|
||||
.loaded_fonts
|
||||
.iter()
|
||||
.position(|loaded_font| loaded_font.font.id() == id)
|
||||
{
|
||||
FontId(ix)
|
||||
} else {
|
||||
let font = self
|
||||
.font_system
|
||||
.get_font(id, cosmic_text::Weight::NORMAL)
|
||||
.unwrap();
|
||||
let face = self.font_system.db().face(id).unwrap();
|
||||
|
||||
let font_id = FontId(self.loaded_fonts.len());
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: CosmicFontFeatures::new(),
|
||||
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
|
||||
});
|
||||
|
||||
font_id
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
let loaded_font = self.loaded_font(run.font_id);
|
||||
let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
|
||||
|
||||
attrs_list.add_span(
|
||||
offs..(offs + run.len),
|
||||
&Attrs::new()
|
||||
.metadata(run.font_id.0)
|
||||
.family(Family::Name(&font.families.first().unwrap().0))
|
||||
.stretch(font.stretch)
|
||||
.style(font.style)
|
||||
.weight(font.weight)
|
||||
.font_features(loaded_font.features.clone()),
|
||||
);
|
||||
offs += run.len;
|
||||
}
|
||||
|
||||
let line = ShapeLine::new(
|
||||
&mut self.font_system,
|
||||
text,
|
||||
&attrs_list,
|
||||
cosmic_text::Shaping::Advanced,
|
||||
4,
|
||||
);
|
||||
let mut layout_lines = Vec::with_capacity(1);
|
||||
line.layout_to_buffer(
|
||||
&mut self.scratch,
|
||||
f32::from(font_size),
|
||||
None, // We do our own wrapping
|
||||
cosmic_text::Wrap::None,
|
||||
None,
|
||||
&mut layout_lines,
|
||||
None,
|
||||
cosmic_text::Hinting::Disabled,
|
||||
);
|
||||
let layout = layout_lines.first().unwrap();
|
||||
|
||||
let mut runs: Vec<ShapedRun> = Vec::new();
|
||||
for glyph in &layout.glyphs {
|
||||
let mut font_id = FontId(glyph.metadata);
|
||||
let mut loaded_font = self.loaded_font(font_id);
|
||||
if loaded_font.font.id() != glyph.font_id {
|
||||
font_id = self.font_id_for_cosmic_id(glyph.font_id);
|
||||
loaded_font = self.loaded_font(font_id);
|
||||
}
|
||||
let is_emoji = loaded_font.is_known_emoji_font;
|
||||
|
||||
// HACK: Prevent crash caused by variation selectors.
|
||||
if glyph.glyph_id == 3 && is_emoji {
|
||||
continue;
|
||||
}
|
||||
|
||||
let shaped_glyph = ShapedGlyph {
|
||||
id: GlyphId(glyph.glyph_id as u32),
|
||||
position: point(glyph.x.into(), glyph.y.into()),
|
||||
index: glyph.start,
|
||||
is_emoji,
|
||||
};
|
||||
|
||||
if let Some(last_run) = runs
|
||||
.last_mut()
|
||||
.filter(|last_run| last_run.font_id == font_id)
|
||||
{
|
||||
last_run.glyphs.push(shaped_glyph);
|
||||
} else {
|
||||
runs.push(ShapedRun {
|
||||
font_id,
|
||||
glyphs: vec![shaped_glyph],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LineLayout {
|
||||
font_size,
|
||||
width: layout.w.into(),
|
||||
ascent: layout.max_ascent.into(),
|
||||
descent: layout.max_descent.into(),
|
||||
runs,
|
||||
len: text.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
|
||||
let mut result = CosmicFontFeatures::new();
|
||||
for feature in features.0.iter() {
|
||||
let name_bytes: [u8; 4] = feature
|
||||
.0
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.context("Incorrect feature flag format")?;
|
||||
|
||||
let tag = cosmic_text::FeatureTag::new(&name_bytes);
|
||||
|
||||
result.set(tag, feature.1);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn font_into_properties(font: &gpui::Font) -> font_kit::properties::Properties {
|
||||
font_kit::properties::Properties {
|
||||
style: match font.style {
|
||||
gpui::FontStyle::Normal => font_kit::properties::Style::Normal,
|
||||
gpui::FontStyle::Italic => font_kit::properties::Style::Italic,
|
||||
gpui::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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn check_is_known_emoji_font(postscript_name: &str) -> bool {
|
||||
// TODO: Include other common emoji fonts
|
||||
postscript_name == "NotoColorEmoji"
|
||||
}
|
||||
pub(crate) use gpui_wgpu::CosmicTextSystem;
|
||||
|
|
|
|||
|
|
@ -31,3 +31,7 @@ gpui_windows.workspace = true
|
|||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
gpui_linux.workspace = true
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
gpui_web.workspace = true
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ pub fn headless() -> gpui::Application {
|
|||
gpui::Application::with_platform(current_platform(true))
|
||||
}
|
||||
|
||||
/// Initializes panic hooks and logging for the web platform.
|
||||
/// Call this before running the application in a wasm_bindgen entrypoint.
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn web_init() {
|
||||
console_error_panic_hook::set_once();
|
||||
gpui_web::init_logging();
|
||||
}
|
||||
|
||||
/// Returns the default [`Platform`] for the current OS.
|
||||
pub fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -33,10 +41,16 @@ pub fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
|||
)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
gpui_linux::current_platform(headless)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
let _ = headless;
|
||||
Rc::new(gpui_web::WebPlatform::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "macos"))]
|
||||
|
|
|
|||
12
crates/gpui_util/Cargo.toml
Normal file
12
crates/gpui_util/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "gpui_util"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
crates/gpui_util/LICENSE-APACHE
Symbolic link
1
crates/gpui_util/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
292
crates/gpui_util/src/lib.rs
Normal file
292
crates/gpui_util/src/lib.rs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
// FluentBuilder
|
||||
// pub use gpui_util::{FutureExt, Timeout, arc_cow::ArcCow};
|
||||
|
||||
use std::{
|
||||
env,
|
||||
ops::AddAssign,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
sync::OnceLock,
|
||||
task::{Context, Poll},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
pub mod arc_cow;
|
||||
|
||||
pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
|
||||
let prev = *value;
|
||||
*value += T::from(1);
|
||||
prev
|
||||
}
|
||||
|
||||
pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
|
||||
static ZED_MEASUREMENTS: OnceLock<bool> = OnceLock::new();
|
||||
let zed_measurements = ZED_MEASUREMENTS.get_or_init(|| {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn some_or_debug_panic<T>(option: Option<T>) -> Option<T> {
|
||||
#[cfg(debug_assertions)]
|
||||
if option.is_none() {
|
||||
panic!("Unexpected None");
|
||||
}
|
||||
option
|
||||
}
|
||||
|
||||
/// 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 ResultExt<E> {
|
||||
type Ok;
|
||||
|
||||
fn log_err(self) -> Option<Self::Ok>;
|
||||
/// 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<Self::Ok>;
|
||||
fn log_with_level(self, level: log::Level) -> Option<Self::Ok>;
|
||||
fn anyhow(self) -> anyhow::Result<Self::Ok>
|
||||
where
|
||||
E: Into<anyhow::Error>;
|
||||
}
|
||||
|
||||
impl<T, E> ResultExt<E> for Result<T, E>
|
||||
where
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
type Ok = T;
|
||||
|
||||
#[track_caller]
|
||||
fn log_err(self) -> Option<T> {
|
||||
self.log_with_level(log::Level::Error)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn debug_assert_ok(self, reason: &str) -> Self {
|
||||
if let Err(error) = &self {
|
||||
debug_panic!("{reason} - {error:?}");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn warn_on_err(self) -> Option<T> {
|
||||
self.log_with_level(log::Level::Warn)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn log_with_level(self, level: log::Level) -> Option<T> {
|
||||
match self {
|
||||
Ok(value) => Some(value),
|
||||
Err(error) => {
|
||||
log_error_with_caller(*Location::caller(), error, level);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn anyhow(self) -> anyhow::Result<T>
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
self.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
fn log_error_with_caller<E>(caller: core::panic::Location<'_>, error: E, level: log::Level)
|
||||
where
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
#[cfg(not(windows))]
|
||||
let file = caller.file();
|
||||
#[cfg(windows)]
|
||||
let file = caller.file().replace('\\', "/");
|
||||
// In this codebase all crates reside in a `crates` directory,
|
||||
// so discard the prefix up to that segment to find the crate name
|
||||
let file = file.split_once("crates/");
|
||||
let target = file.as_ref().and_then(|(_, s)| s.split_once("/src/"));
|
||||
|
||||
let module_path = target.map(|(krate, module)| {
|
||||
if module.starts_with(krate) {
|
||||
module.trim_end_matches(".rs").replace('/', "::")
|
||||
} else {
|
||||
krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::")
|
||||
}
|
||||
});
|
||||
let file = file.map(|(_, file)| format!("crates/{file}"));
|
||||
log::logger().log(
|
||||
&log::Record::builder()
|
||||
.target(module_path.as_deref().unwrap_or(""))
|
||||
.module_path(file.as_deref())
|
||||
.args(format_args!("{:?}", error))
|
||||
.file(Some(caller.file()))
|
||||
.line(Some(caller.line()))
|
||||
.level(level)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn log_err<E: std::fmt::Debug>(error: &E) {
|
||||
log_error_with_caller(*Location::caller(), error, log::Level::Error);
|
||||
}
|
||||
|
||||
pub trait TryFutureExt {
|
||||
fn log_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn warn_on_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
fn unwrap(self) -> UnwrapFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<F, T, E> TryFutureExt for F
|
||||
where
|
||||
F: Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
#[track_caller]
|
||||
fn log_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let location = Location::caller();
|
||||
LogErrorFuture(self, log::Level::Error, *location)
|
||||
}
|
||||
|
||||
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
LogErrorFuture(self, log::Level::Error, location)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn warn_on_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let location = Location::caller();
|
||||
LogErrorFuture(self, log::Level::Warn, *location)
|
||||
}
|
||||
|
||||
fn unwrap(self) -> UnwrapFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
UnwrapFuture(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
|
||||
|
||||
impl<F, T, E> Future for LogErrorFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
type Output = Option<T>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
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_error_with_caller(location, error, level);
|
||||
None
|
||||
}
|
||||
}),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnwrapFuture<F>(F);
|
||||
|
||||
impl<F, T, E> Future for UnwrapFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
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<F: FnOnce()>(Option<F>);
|
||||
|
||||
impl<F: FnOnce()> Deferred<F> {
|
||||
/// Drop without running the deferred function.
|
||||
pub fn abort(mut self) {
|
||||
self.0.take();
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce()> Drop for Deferred<F> {
|
||||
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: FnOnce()>(f: F) -> Deferred<F> {
|
||||
Deferred(Some(f))
|
||||
}
|
||||
61
crates/gpui_web/Cargo.toml
Normal file
61
crates/gpui_web/Cargo.toml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
[package]
|
||||
name = "gpui_web"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
license = "Apache-2.0"
|
||||
autoexamples = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/gpui_web.rs"
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
gpui.workspace = true
|
||||
parking_lot = { workspace = true, features = ["nightly"] }
|
||||
gpui_wgpu.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
smallvec.workspace = true
|
||||
uuid.workspace = true
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-time.workspace = true
|
||||
console_error_panic_hook = "0.1.7"
|
||||
js-sys = "0.3"
|
||||
raw-window-handle = "0.6"
|
||||
wasm_thread = { version = "0.3", features = ["es_modules"] }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"CssStyleDeclaration",
|
||||
"DataTransfer",
|
||||
"Document",
|
||||
"DomRect",
|
||||
"DragEvent",
|
||||
"Element",
|
||||
"EventTarget",
|
||||
"File",
|
||||
"FileList",
|
||||
"HtmlCanvasElement",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
"KeyboardEvent",
|
||||
"MediaQueryList",
|
||||
"MediaQueryListEvent",
|
||||
"MouseEvent",
|
||||
"Navigator",
|
||||
"PointerEvent",
|
||||
"ResizeObserver",
|
||||
"ResizeObserverBoxOptions",
|
||||
"ResizeObserverEntry",
|
||||
"ResizeObserverSize",
|
||||
"ResizeObserverOptions",
|
||||
"Screen",
|
||||
"Storage",
|
||||
"VisualViewport",
|
||||
"WheelEvent",
|
||||
"Window",
|
||||
] }
|
||||
1
crates/gpui_web/LICENSE-APACHE
Symbolic link
1
crates/gpui_web/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
14
crates/gpui_web/examples/hello_web/.cargo/config.toml
Normal file
14
crates/gpui_web/examples/hello_web/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
|
||||
"-C", "link-arg=--shared-memory",
|
||||
"-C", "link-arg=--max-memory=1073741824",
|
||||
"-C", "link-arg=--import-memory",
|
||||
"-C", "link-arg=--export=__wasm_init_tls",
|
||||
"-C", "link-arg=--export=__tls_size",
|
||||
"-C", "link-arg=--export=__tls_align",
|
||||
"-C", "link-arg=--export=__tls_base",
|
||||
]
|
||||
|
||||
[unstable]
|
||||
build-std = ["std,panic_abort"]
|
||||
3
crates/gpui_web/examples/hello_web/.gitignore
vendored
Normal file
3
crates/gpui_web/examples/hello_web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/dist
|
||||
/target
|
||||
Cargo.lock
|
||||
16
crates/gpui_web/examples/hello_web/Cargo.toml
Normal file
16
crates/gpui_web/examples/hello_web/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "hello_web"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "hello_web"
|
||||
path = "main.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../../../gpui" }
|
||||
gpui_platform = { path = "../../../gpui_platform" }
|
||||
web-time = "1"
|
||||
1
crates/gpui_web/examples/hello_web/LICENSE-APACHE
Symbolic link
1
crates/gpui_web/examples/hello_web/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../LICENSE-APACHE
|
||||
31
crates/gpui_web/examples/hello_web/index.html
Normal file
31
crates/gpui_web/examples/hello_web/index.html
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=0" />
|
||||
<title>GPUI Web: hello_web</title>
|
||||
<link data-trunk rel="rust" data-bin="hello_web" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
outline: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
422
crates/gpui_web/examples/hello_web/main.rs
Normal file
422
crates/gpui_web/examples/hello_web/main.rs
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
App, Bounds, Context, ElementId, SharedString, Task, Window, WindowBounds, WindowOptions, div,
|
||||
px, rgb, size,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prime counting (intentionally brute-force so it hammers the CPU)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn is_prime(n: u64) -> bool {
|
||||
if n < 2 {
|
||||
return false;
|
||||
}
|
||||
if n < 4 {
|
||||
return true;
|
||||
}
|
||||
if n % 2 == 0 || n % 3 == 0 {
|
||||
return false;
|
||||
}
|
||||
let mut i = 5;
|
||||
while i * i <= n {
|
||||
if n % i == 0 || n % (i + 2) == 0 {
|
||||
return false;
|
||||
}
|
||||
i += 6;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn count_primes_in_range(start: u64, end: u64) -> u64 {
|
||||
let mut count = 0;
|
||||
for n in start..end {
|
||||
if is_prime(n) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NUM_CHUNKS: u64 = 12;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Preset {
|
||||
TenMillion,
|
||||
FiftyMillion,
|
||||
HundredMillion,
|
||||
}
|
||||
|
||||
impl Preset {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Preset::TenMillion => "10 M",
|
||||
Preset::FiftyMillion => "50 M",
|
||||
Preset::HundredMillion => "100 M",
|
||||
}
|
||||
}
|
||||
|
||||
fn value(self) -> u64 {
|
||||
match self {
|
||||
Preset::TenMillion => 10_000_000,
|
||||
Preset::FiftyMillion => 50_000_000,
|
||||
Preset::HundredMillion => 100_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
const ALL: [Preset; 3] = [
|
||||
Preset::TenMillion,
|
||||
Preset::FiftyMillion,
|
||||
Preset::HundredMillion,
|
||||
];
|
||||
}
|
||||
|
||||
struct ChunkResult {
|
||||
count: u64,
|
||||
}
|
||||
|
||||
struct Run {
|
||||
limit: u64,
|
||||
chunks_done: u64,
|
||||
chunk_results: Vec<ChunkResult>,
|
||||
total: Option<u64>,
|
||||
elapsed: Option<f64>,
|
||||
}
|
||||
|
||||
struct HelloWeb {
|
||||
selected_preset: Preset,
|
||||
current_run: Option<Run>,
|
||||
history: Vec<SharedString>,
|
||||
_tasks: Vec<Task<()>>,
|
||||
}
|
||||
|
||||
impl HelloWeb {
|
||||
fn new(_cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
selected_preset: Preset::TenMillion,
|
||||
current_run: None,
|
||||
history: Vec::new(),
|
||||
_tasks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_search(&mut self, cx: &mut Context<Self>) {
|
||||
let limit = self.selected_preset.value();
|
||||
let chunk_size = limit / NUM_CHUNKS;
|
||||
|
||||
self.current_run = Some(Run {
|
||||
limit,
|
||||
chunks_done: 0,
|
||||
chunk_results: Vec::new(),
|
||||
total: None,
|
||||
elapsed: None,
|
||||
});
|
||||
self._tasks.clear();
|
||||
cx.notify();
|
||||
|
||||
let start_time = web_time::Instant::now();
|
||||
|
||||
for i in 0..NUM_CHUNKS {
|
||||
let range_start = i * chunk_size;
|
||||
let range_end = if i == NUM_CHUNKS - 1 {
|
||||
limit
|
||||
} else {
|
||||
range_start + chunk_size
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let count = cx
|
||||
.background_spawn(async move { count_primes_in_range(range_start, range_end) })
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(run) = &mut this.current_run {
|
||||
run.chunk_results.push(ChunkResult { count });
|
||||
run.chunks_done += 1;
|
||||
|
||||
if run.chunks_done == NUM_CHUNKS {
|
||||
let total: u64 = run.chunk_results.iter().map(|r| r.count).sum();
|
||||
let elapsed_ms = start_time.elapsed().as_secs_f64() * 1000.0;
|
||||
run.total = Some(total);
|
||||
run.elapsed = Some(elapsed_ms);
|
||||
this.history.push(
|
||||
format!(
|
||||
"π({}) = {} ({:.0} ms, {} chunks)",
|
||||
format_number(run.limit),
|
||||
format_number(total),
|
||||
elapsed_ms,
|
||||
NUM_CHUNKS,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
self._tasks.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_number(n: u64) -> String {
|
||||
let s = n.to_string();
|
||||
let mut result = String::new();
|
||||
for (i, ch) in s.chars().rev().enumerate() {
|
||||
if i > 0 && i % 3 == 0 {
|
||||
result.push(',');
|
||||
}
|
||||
result.push(ch);
|
||||
}
|
||||
result.chars().rev().collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BG_BASE: u32 = 0x1e1e2e;
|
||||
const BG_SURFACE: u32 = 0x313244;
|
||||
const BG_OVERLAY: u32 = 0x45475a;
|
||||
const TEXT_PRIMARY: u32 = 0xcdd6f4;
|
||||
const TEXT_SECONDARY: u32 = 0xa6adc8;
|
||||
const TEXT_DIM: u32 = 0x6c7086;
|
||||
const ACCENT_YELLOW: u32 = 0xf9e2af;
|
||||
const ACCENT_GREEN: u32 = 0xa6e3a1;
|
||||
const ACCENT_BLUE: u32 = 0x89b4fa;
|
||||
const ACCENT_MAUVE: u32 = 0xcba6f7;
|
||||
|
||||
impl Render for HelloWeb {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_running = self.current_run.as_ref().is_some_and(|r| r.total.is_none());
|
||||
|
||||
// -- Preset buttons --
|
||||
let preset_row = Preset::ALL.iter().enumerate().fold(
|
||||
div().flex().flex_row().gap_2(),
|
||||
|row, (index, &preset)| {
|
||||
let is_selected = preset == self.selected_preset;
|
||||
let (bg, text_color) = if is_selected {
|
||||
(ACCENT_BLUE, BG_BASE)
|
||||
} else {
|
||||
(BG_OVERLAY, TEXT_SECONDARY)
|
||||
};
|
||||
row.child(
|
||||
div()
|
||||
.id(ElementId::NamedInteger("preset".into(), index as u64))
|
||||
.px_3()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.bg(rgb(bg))
|
||||
.text_color(rgb(text_color))
|
||||
.text_sm()
|
||||
.cursor_pointer()
|
||||
.when(!is_running, |this| {
|
||||
this.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.selected_preset = preset;
|
||||
}))
|
||||
})
|
||||
.child(preset.label()),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// -- Go button --
|
||||
let (go_bg, go_text, go_label) = if is_running {
|
||||
(BG_OVERLAY, TEXT_DIM, "Running…")
|
||||
} else {
|
||||
(ACCENT_GREEN, BG_BASE, "Count Primes")
|
||||
};
|
||||
let go_button = div()
|
||||
.id("go")
|
||||
.px_4()
|
||||
.py(px(6.))
|
||||
.rounded_md()
|
||||
.bg(rgb(go_bg))
|
||||
.text_color(rgb(go_text))
|
||||
.cursor_pointer()
|
||||
.when(!is_running, |this| {
|
||||
this.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.start_search(cx);
|
||||
}))
|
||||
})
|
||||
.child(go_label);
|
||||
|
||||
// -- Progress / result area --
|
||||
let status_area = if let Some(run) = &self.current_run {
|
||||
let progress_fraction = run.chunks_done as f32 / NUM_CHUNKS as f32;
|
||||
let progress_pct = (progress_fraction * 100.0) as u32;
|
||||
|
||||
let status_text: SharedString = if let Some(total) = run.total {
|
||||
format!(
|
||||
"Found {} primes below {} in {:.0} ms",
|
||||
format_number(total),
|
||||
format_number(run.limit),
|
||||
run.elapsed.unwrap_or(0.0),
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
format!(
|
||||
"Searching up to {} … {}/{} chunks ({}%)",
|
||||
format_number(run.limit),
|
||||
run.chunks_done,
|
||||
NUM_CHUNKS,
|
||||
progress_pct,
|
||||
)
|
||||
.into()
|
||||
};
|
||||
|
||||
let bar_color = if run.total.is_some() {
|
||||
ACCENT_GREEN
|
||||
} else {
|
||||
ACCENT_BLUE
|
||||
};
|
||||
|
||||
let chunk_dots =
|
||||
(0..NUM_CHUNKS as usize).fold(div().flex().flex_row().gap_1().mt_2(), |row, i| {
|
||||
let done = i < run.chunks_done as usize;
|
||||
let color = if done { ACCENT_MAUVE } else { BG_OVERLAY };
|
||||
row.child(div().size(px(10.)).rounded_sm().bg(rgb(color)))
|
||||
});
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(div().text_color(rgb(TEXT_PRIMARY)).child(status_text))
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h(px(8.))
|
||||
.rounded_sm()
|
||||
.bg(rgb(BG_OVERLAY))
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.rounded_sm()
|
||||
.bg(rgb(bar_color))
|
||||
.w(gpui::relative(progress_fraction)),
|
||||
),
|
||||
)
|
||||
.child(chunk_dots)
|
||||
} else {
|
||||
div().flex().flex_col().w_full().child(
|
||||
div()
|
||||
.text_color(rgb(TEXT_DIM))
|
||||
.child("Select a range and press Count Primes to begin."),
|
||||
)
|
||||
};
|
||||
|
||||
// -- History log --
|
||||
let history_section = if self.history.is_empty() {
|
||||
div()
|
||||
} else {
|
||||
self.history
|
||||
.iter()
|
||||
.rev()
|
||||
.fold(div().flex().flex_col().gap_1(), |col, entry| {
|
||||
col.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT_SECONDARY))
|
||||
.child(entry.clone()),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
// -- Layout --
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(rgb(BG_BASE))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
// Title
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(rgb(TEXT_PRIMARY))
|
||||
.child("Prime Sieve — GPUI Web"),
|
||||
)
|
||||
.child(div().text_sm().text_color(rgb(TEXT_DIM)).child(format!(
|
||||
"Background threads: {} · Chunks per run: {}",
|
||||
std::thread::available_parallelism().map_or(2, |n| n.get().max(2)),
|
||||
NUM_CHUNKS,
|
||||
)))
|
||||
// Controls
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.w(px(500.))
|
||||
.rounded_lg()
|
||||
.bg(rgb(BG_SURFACE))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(ACCENT_YELLOW))
|
||||
.child("Count primes below:"),
|
||||
)
|
||||
.child(preset_row)
|
||||
.child(go_button),
|
||||
)
|
||||
// Status
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w(px(500.))
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(rgb(BG_SURFACE))
|
||||
.child(status_area),
|
||||
)
|
||||
// History
|
||||
.when(!self.history.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w(px(500.))
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(rgb(BG_SURFACE))
|
||||
.gap_2()
|
||||
.child(div().text_sm().text_color(rgb(TEXT_DIM)).child("History"))
|
||||
.child(history_section),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn main() {
|
||||
gpui_platform::web_init();
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(640.), px(560.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(HelloWeb::new),
|
||||
)
|
||||
.expect("failed to open window");
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
4
crates/gpui_web/examples/hello_web/rust-toolchain.toml
Normal file
4
crates/gpui_web/examples/hello_web/rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
components = ["rust-src", "rustfmt", "clippy"]
|
||||
7
crates/gpui_web/examples/hello_web/trunk.toml
Normal file
7
crates/gpui_web/examples/hello_web/trunk.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[serve]
|
||||
addresses = ["127.0.0.1"]
|
||||
port = 8080
|
||||
open = true
|
||||
|
||||
# Headers required for WebGPU / SharedArrayBuffer support.
|
||||
headers = { "Cross-Origin-Embedder-Policy" = "require-corp", "Cross-Origin-Opener-Policy" = "same-origin" }
|
||||
333
crates/gpui_web/src/dispatcher.rs
Normal file
333
crates/gpui_web/src/dispatcher.rs
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
use gpui::{
|
||||
PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant,
|
||||
ThreadTaskTimings,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_time::Instant;
|
||||
|
||||
const MIN_BACKGROUND_THREADS: usize = 2;
|
||||
|
||||
fn shared_memory_supported() -> bool {
|
||||
let global = js_sys::global();
|
||||
let has_shared_array_buffer =
|
||||
js_sys::Reflect::has(&global, &JsValue::from_str("SharedArrayBuffer")).unwrap_or(false);
|
||||
let has_atomics = js_sys::Reflect::has(&global, &JsValue::from_str("Atomics")).unwrap_or(false);
|
||||
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
|
||||
let buffer = memory.buffer();
|
||||
let is_shared_buffer = buffer.is_instance_of::<js_sys::SharedArrayBuffer>();
|
||||
has_shared_array_buffer && has_atomics && is_shared_buffer
|
||||
}
|
||||
|
||||
enum MainThreadItem {
|
||||
Runnable(RunnableVariant),
|
||||
Delayed {
|
||||
runnable: RunnableVariant,
|
||||
millis: i32,
|
||||
},
|
||||
// TODO-Wasm: Shouldn't these run on their own dedicated thread?
|
||||
RealtimeFunction(Box<dyn FnOnce() + Send>),
|
||||
}
|
||||
|
||||
struct MainThreadMailbox {
|
||||
sender: PriorityQueueSender<MainThreadItem>,
|
||||
receiver: parking_lot::Mutex<PriorityQueueReceiver<MainThreadItem>>,
|
||||
signal: AtomicI32,
|
||||
}
|
||||
|
||||
impl MainThreadMailbox {
|
||||
fn new() -> Self {
|
||||
let (sender, receiver) = PriorityQueueReceiver::new();
|
||||
Self {
|
||||
sender,
|
||||
receiver: parking_lot::Mutex::new(receiver),
|
||||
signal: AtomicI32::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn post(&self, priority: Priority, item: MainThreadItem) {
|
||||
if self.sender.spin_send(priority, item).is_err() {
|
||||
log::error!("MainThreadMailbox::send failed: receiver disconnected");
|
||||
}
|
||||
|
||||
// TODO-Wasm: Verify this lock-free protocol
|
||||
let view = self.signal_view();
|
||||
js_sys::Atomics::store(&view, 0, 1).ok();
|
||||
js_sys::Atomics::notify(&view, 0).ok();
|
||||
}
|
||||
|
||||
fn drain(&self, window: &web_sys::Window) {
|
||||
let mut receiver = self.receiver.lock();
|
||||
loop {
|
||||
// We need these `spin` variants because we can't acquire a lock on the main thread.
|
||||
// TODO-WASM: Should we do something different?
|
||||
match receiver.spin_try_pop() {
|
||||
Ok(Some(item)) => execute_on_main_thread(window, item),
|
||||
Ok(None) => break,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn signal_view(&self) -> js_sys::Int32Array {
|
||||
let byte_offset = self.signal.as_ptr() as u32;
|
||||
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
|
||||
js_sys::Int32Array::new_with_byte_offset_and_length(&memory.buffer(), byte_offset, 1)
|
||||
}
|
||||
|
||||
fn run_waker_loop(self: &Arc<Self>, window: web_sys::Window) {
|
||||
if !shared_memory_supported() {
|
||||
log::warn!("SharedArrayBuffer not available; main thread mailbox waker loop disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
let mailbox = Arc::clone(self);
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let view = mailbox.signal_view();
|
||||
loop {
|
||||
js_sys::Atomics::store(&view, 0, 0).expect("Atomics.store failed");
|
||||
|
||||
let result = match js_sys::Atomics::wait_async(&view, 0, 0) {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
log::error!("Atomics.waitAsync failed: {error:?}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async"))
|
||||
.ok()
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_async {
|
||||
log::error!("Atomics.waitAsync returned synchronously; waker loop exiting");
|
||||
break;
|
||||
}
|
||||
|
||||
let promise: js_sys::Promise =
|
||||
js_sys::Reflect::get(&result, &JsValue::from_str("value"))
|
||||
.expect("waitAsync result missing 'value'")
|
||||
.unchecked_into();
|
||||
|
||||
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||
|
||||
mailbox.drain(&window);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebDispatcher {
|
||||
main_thread_id: std::thread::ThreadId,
|
||||
browser_window: web_sys::Window,
|
||||
background_sender: PriorityQueueSender<RunnableVariant>,
|
||||
main_thread_mailbox: Arc<MainThreadMailbox>,
|
||||
supports_threads: bool,
|
||||
_background_threads: Vec<wasm_thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
// Safety: `web_sys::Window` is only accessed from the main thread
|
||||
// All other fields are `Send + Sync` by construction.
|
||||
unsafe impl Send for WebDispatcher {}
|
||||
unsafe impl Sync for WebDispatcher {}
|
||||
|
||||
impl WebDispatcher {
|
||||
pub fn new(browser_window: web_sys::Window) -> Self {
|
||||
let (background_sender, background_receiver) = PriorityQueueReceiver::new();
|
||||
|
||||
let main_thread_mailbox = Arc::new(MainThreadMailbox::new());
|
||||
let supports_threads = shared_memory_supported();
|
||||
|
||||
if supports_threads {
|
||||
main_thread_mailbox.run_waker_loop(browser_window.clone());
|
||||
} else {
|
||||
log::warn!(
|
||||
"SharedArrayBuffer not available; falling back to single-threaded dispatcher"
|
||||
);
|
||||
}
|
||||
|
||||
let background_threads = if supports_threads {
|
||||
let thread_count = browser_window
|
||||
.navigator()
|
||||
.hardware_concurrency()
|
||||
.max(MIN_BACKGROUND_THREADS as f64) as usize;
|
||||
|
||||
// TODO-Wasm: Is it bad to have web workers blocking for a long time like this?
|
||||
(0..thread_count)
|
||||
.map(|i| {
|
||||
let mut receiver = background_receiver.clone();
|
||||
wasm_thread::Builder::new()
|
||||
.name(format!("background-worker-{i}"))
|
||||
.spawn(move || {
|
||||
loop {
|
||||
let runnable: RunnableVariant = match receiver.pop() {
|
||||
Ok(runnable) => runnable,
|
||||
Err(_) => {
|
||||
log::info!(
|
||||
"background-worker-{i}: channel disconnected, exiting"
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if runnable.metadata().is_closed() {
|
||||
continue;
|
||||
}
|
||||
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn background worker thread")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Self {
|
||||
main_thread_id: std::thread::current().id(),
|
||||
browser_window,
|
||||
background_sender,
|
||||
main_thread_mailbox,
|
||||
supports_threads,
|
||||
_background_threads: background_threads,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_main_thread(&self) -> bool {
|
||||
std::thread::current().id() == self.main_thread_id
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDispatcher for WebDispatcher {
|
||||
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
|
||||
// TODO-Wasm: should we panic here?
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
|
||||
ThreadTaskTimings {
|
||||
thread_name: None,
|
||||
thread_id: std::thread::current().id(),
|
||||
timings: Vec::new(),
|
||||
total_pushed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_main_thread(&self) -> bool {
|
||||
self.on_main_thread()
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
|
||||
if !self.supports_threads {
|
||||
self.dispatch_on_main_thread(runnable, priority);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = if self.on_main_thread() {
|
||||
self.background_sender.spin_send(priority, runnable)
|
||||
} else {
|
||||
self.background_sender.send(priority, runnable)
|
||||
};
|
||||
|
||||
if let Err(error) = result {
|
||||
log::error!("dispatch: failed to send to background queue: {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
|
||||
if self.on_main_thread() {
|
||||
schedule_runnable(&self.browser_window, runnable, priority);
|
||||
} else {
|
||||
self.main_thread_mailbox
|
||||
.post(priority, MainThreadItem::Runnable(runnable));
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
|
||||
let millis = duration.as_millis().min(i32::MAX as u128) as i32;
|
||||
if self.on_main_thread() {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
if !runnable.metadata().is_closed() {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
self.browser_window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
callback.unchecked_ref(),
|
||||
millis,
|
||||
)
|
||||
.ok();
|
||||
} else {
|
||||
self.main_thread_mailbox
|
||||
.post(Priority::High, MainThreadItem::Delayed { runnable, millis });
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_realtime(&self, function: Box<dyn FnOnce() + Send>) {
|
||||
if self.on_main_thread() {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
function();
|
||||
});
|
||||
self.browser_window
|
||||
.queue_microtask(callback.unchecked_ref());
|
||||
} else {
|
||||
self.main_thread_mailbox
|
||||
.post(Priority::High, MainThreadItem::RealtimeFunction(function));
|
||||
}
|
||||
}
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
|
||||
match item {
|
||||
MainThreadItem::Runnable(runnable) => {
|
||||
if !runnable.metadata().is_closed() {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
MainThreadItem::Delayed { runnable, millis } => {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
if !runnable.metadata().is_closed() {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
callback.unchecked_ref(),
|
||||
millis,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
MainThreadItem::RealtimeFunction(function) => {
|
||||
function();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
if !runnable.metadata().is_closed() {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
let callback: &js_sys::Function = callback.unchecked_ref();
|
||||
|
||||
match priority {
|
||||
Priority::RealtimeAudio => {
|
||||
window.queue_microtask(callback);
|
||||
}
|
||||
_ => {
|
||||
// TODO-Wasm: this ought to enqueue so we can dequeue with proper priority
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(callback, 0)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
crates/gpui_web/src/display.rs
Normal file
98
crates/gpui_web/src/display.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use anyhow::Result;
|
||||
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebDisplay {
|
||||
id: DisplayId,
|
||||
uuid: uuid::Uuid,
|
||||
browser_window: web_sys::Window,
|
||||
}
|
||||
|
||||
// Safety: WASM is single-threaded — there is no concurrent access to `web_sys::Window`.
|
||||
unsafe impl Send for WebDisplay {}
|
||||
unsafe impl Sync for WebDisplay {}
|
||||
|
||||
impl WebDisplay {
|
||||
pub fn new(browser_window: web_sys::Window) -> Self {
|
||||
WebDisplay {
|
||||
id: DisplayId::new(1),
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
browser_window,
|
||||
}
|
||||
}
|
||||
|
||||
fn screen_size(&self) -> Size<Pixels> {
|
||||
let Some(screen) = self.browser_window.screen().ok() else {
|
||||
return Size {
|
||||
width: px(1920.),
|
||||
height: px(1080.),
|
||||
};
|
||||
};
|
||||
|
||||
let width = screen.width().unwrap_or(1920) as f32;
|
||||
let height = screen.height().unwrap_or(1080) as f32;
|
||||
|
||||
Size {
|
||||
width: px(width),
|
||||
height: px(height),
|
||||
}
|
||||
}
|
||||
|
||||
fn viewport_size(&self) -> Size<Pixels> {
|
||||
let width = self
|
||||
.browser_window
|
||||
.inner_width()
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(1920.0) as f32;
|
||||
let height = self
|
||||
.browser_window
|
||||
.inner_height()
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(1080.0) as f32;
|
||||
|
||||
Size {
|
||||
width: px(width),
|
||||
height: px(height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDisplay for WebDisplay {
|
||||
fn id(&self) -> DisplayId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn uuid(&self) -> Result<uuid::Uuid> {
|
||||
Ok(self.uuid)
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
let size = self.screen_size();
|
||||
Bounds {
|
||||
origin: Point::default(),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
fn visible_bounds(&self) -> Bounds<Pixels> {
|
||||
let size = self.viewport_size();
|
||||
Bounds {
|
||||
origin: Point::default(),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_bounds(&self) -> Bounds<Pixels> {
|
||||
let visible = self.visible_bounds();
|
||||
let width = visible.size.width * 0.75;
|
||||
let height = visible.size.height * 0.75;
|
||||
let origin_x = (visible.size.width - width) / 2.0;
|
||||
let origin_y = (visible.size.height - height) / 2.0;
|
||||
Bounds {
|
||||
origin: Point::new(origin_x, origin_y),
|
||||
size: Size { width, height },
|
||||
}
|
||||
}
|
||||
}
|
||||
615
crates/gpui_web/src/events.rs
Normal file
615
crates/gpui_web/src/events.rs
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Capslock, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
|
||||
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent,
|
||||
TouchPhase, point, px,
|
||||
};
|
||||
use smallvec::smallvec;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::window::WebWindowInner;
|
||||
|
||||
pub struct WebEventListeners {
|
||||
#[allow(dead_code)]
|
||||
closures: Vec<Closure<dyn FnMut(JsValue)>>,
|
||||
}
|
||||
|
||||
pub(crate) struct ClickState {
|
||||
last_position: Point<Pixels>,
|
||||
last_time: f64,
|
||||
current_count: usize,
|
||||
}
|
||||
|
||||
impl Default for ClickState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_position: Point::default(),
|
||||
last_time: 0.0,
|
||||
current_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClickState {
|
||||
fn register_click(&mut self, position: Point<Pixels>, time: f64) -> usize {
|
||||
let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2)
|
||||
+ (f32::from(position.y) - f32::from(self.last_position.y)).powi(2))
|
||||
.sqrt();
|
||||
|
||||
if (time - self.last_time) < 400.0 && distance < 5.0 {
|
||||
self.current_count += 1;
|
||||
} else {
|
||||
self.current_count = 1;
|
||||
}
|
||||
|
||||
self.last_position = position;
|
||||
self.last_time = time;
|
||||
self.current_count
|
||||
}
|
||||
}
|
||||
|
||||
impl WebWindowInner {
|
||||
pub fn register_event_listeners(self: &Rc<Self>) -> WebEventListeners {
|
||||
let mut closures = vec![
|
||||
self.register_pointer_down(),
|
||||
self.register_pointer_up(),
|
||||
self.register_pointer_move(),
|
||||
self.register_pointer_leave(),
|
||||
self.register_wheel(),
|
||||
self.register_context_menu(),
|
||||
self.register_dragover(),
|
||||
self.register_drop(),
|
||||
self.register_dragleave(),
|
||||
self.register_key_down(),
|
||||
self.register_key_up(),
|
||||
self.register_focus(),
|
||||
self.register_blur(),
|
||||
self.register_pointer_enter(),
|
||||
self.register_pointer_leave_hover(),
|
||||
];
|
||||
closures.extend(self.register_visibility_change());
|
||||
closures.extend(self.register_appearance_change());
|
||||
|
||||
WebEventListeners { closures }
|
||||
}
|
||||
|
||||
fn listen(
|
||||
self: &Rc<Self>,
|
||||
event_name: &str,
|
||||
handler: impl FnMut(JsValue) + 'static,
|
||||
) -> Closure<dyn FnMut(JsValue)> {
|
||||
let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
|
||||
self.canvas
|
||||
.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
|
||||
.ok();
|
||||
closure
|
||||
}
|
||||
|
||||
/// Registers a listener with `{passive: false}` so that `preventDefault()` works.
|
||||
/// Needed for events like `wheel` which are passive by default in modern browsers.
|
||||
fn listen_non_passive(
|
||||
self: &Rc<Self>,
|
||||
event_name: &str,
|
||||
handler: impl FnMut(JsValue) + 'static,
|
||||
) -> Closure<dyn FnMut(JsValue)> {
|
||||
let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
|
||||
let canvas_js: &JsValue = self.canvas.as_ref();
|
||||
let callback_js: &JsValue = closure.as_ref();
|
||||
let options = js_sys::Object::new();
|
||||
js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok();
|
||||
if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) {
|
||||
if let Ok(add_fn) = add_fn_val.dyn_into::<js_sys::Function>() {
|
||||
add_fn
|
||||
.call3(canvas_js, &event_name.into(), callback_js, &options)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
closure
|
||||
}
|
||||
|
||||
fn dispatch_input(&self, input: PlatformInput) {
|
||||
let mut borrowed = self.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = borrowed.input {
|
||||
callback(input);
|
||||
}
|
||||
}
|
||||
|
||||
fn register_pointer_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("pointerdown", move |event: JsValue| {
|
||||
let event: web_sys::PointerEvent = event.unchecked_into();
|
||||
event.prevent_default();
|
||||
this.canvas.focus().ok();
|
||||
|
||||
let button = dom_mouse_button_to_gpui(event.button());
|
||||
let position = pointer_position_in_element(&event);
|
||||
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
|
||||
let time = js_sys::Date::now();
|
||||
|
||||
this.pressed_button.set(Some(button));
|
||||
let click_count = this.click_state.borrow_mut().register_click(position, time);
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.mouse_position = position;
|
||||
current_state.modifiers = modifiers;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_pointer_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("pointerup", move |event: JsValue| {
|
||||
let event: web_sys::PointerEvent = event.unchecked_into();
|
||||
event.prevent_default();
|
||||
|
||||
let button = dom_mouse_button_to_gpui(event.button());
|
||||
let position = pointer_position_in_element(&event);
|
||||
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
|
||||
|
||||
this.pressed_button.set(None);
|
||||
let click_count = this.click_state.borrow().current_count;
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.mouse_position = position;
|
||||
current_state.modifiers = modifiers;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count,
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_pointer_move(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("pointermove", move |event: JsValue| {
|
||||
let event: web_sys::PointerEvent = event.unchecked_into();
|
||||
event.prevent_default();
|
||||
|
||||
let position = pointer_position_in_element(&event);
|
||||
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
|
||||
let current_pressed = this.pressed_button.get();
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.mouse_position = position;
|
||||
current_state.modifiers = modifiers;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent {
|
||||
position,
|
||||
pressed_button: current_pressed,
|
||||
modifiers,
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_pointer_leave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("pointerleave", move |event: JsValue| {
|
||||
let event: web_sys::PointerEvent = event.unchecked_into();
|
||||
|
||||
let position = pointer_position_in_element(&event);
|
||||
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
|
||||
let current_pressed = this.pressed_button.get();
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.mouse_position = position;
|
||||
current_state.modifiers = modifiers;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent {
|
||||
position,
|
||||
pressed_button: current_pressed,
|
||||
modifiers,
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_wheel(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen_non_passive("wheel", move |event: JsValue| {
|
||||
let event: web_sys::WheelEvent = event.unchecked_into();
|
||||
event.prevent_default();
|
||||
|
||||
let mouse_event: &web_sys::MouseEvent = event.as_ref();
|
||||
let position = mouse_position_in_element(mouse_event);
|
||||
let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac);
|
||||
|
||||
let delta_mode = event.delta_mode();
|
||||
let delta = if delta_mode == 1 {
|
||||
ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32))
|
||||
} else {
|
||||
ScrollDelta::Pixels(point(
|
||||
px(-event.delta_x() as f32),
|
||||
px(-event.delta_y() as f32),
|
||||
))
|
||||
};
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.modifiers = modifiers;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent {
|
||||
position,
|
||||
delta,
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_context_menu(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
self.listen("contextmenu", move |event: JsValue| {
|
||||
let event: web_sys::Event = event.unchecked_into();
|
||||
event.prevent_default();
|
||||
})
|
||||
}
|
||||
|
||||
fn register_dragover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("dragover", move |event: JsValue| {
|
||||
let event: web_sys::DragEvent = event.unchecked_into();
|
||||
event.prevent_default();
|
||||
|
||||
let mouse_event: &web_sys::MouseEvent = event.as_ref();
|
||||
let position = mouse_position_in_element(mouse_event);
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.mouse_position = position;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position }));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_drop(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("drop", move |event: JsValue| {
|
||||
let event: web_sys::DragEvent = event.unchecked_into();
|
||||
event.prevent_default();
|
||||
|
||||
let mouse_event: &web_sys::MouseEvent = event.as_ref();
|
||||
let position = mouse_position_in_element(mouse_event);
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.mouse_position = position;
|
||||
}
|
||||
|
||||
let paths = extract_file_paths_from_drag(&event);
|
||||
|
||||
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered {
|
||||
position,
|
||||
paths: ExternalPaths(paths),
|
||||
}));
|
||||
|
||||
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position }));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_dragleave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("dragleave", move |_event: JsValue| {
|
||||
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_key_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("keydown", move |event: JsValue| {
|
||||
let event: web_sys::KeyboardEvent = event.unchecked_into();
|
||||
|
||||
let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
|
||||
let capslock = capslock_from_keyboard_event(&event);
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.modifiers = modifiers;
|
||||
current_state.capslock = capslock;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
|
||||
modifiers,
|
||||
capslock,
|
||||
}));
|
||||
|
||||
let key = dom_key_to_gpui_key(&event);
|
||||
|
||||
if is_modifier_only_key(&key) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.prevent_default();
|
||||
|
||||
let is_held = event.repeat();
|
||||
let key_char = compute_key_char(&event, &key, &modifiers);
|
||||
|
||||
let keystroke = Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char,
|
||||
};
|
||||
|
||||
this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke,
|
||||
is_held,
|
||||
prefer_character_input: false,
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_key_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("keyup", move |event: JsValue| {
|
||||
let event: web_sys::KeyboardEvent = event.unchecked_into();
|
||||
|
||||
let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
|
||||
let capslock = capslock_from_keyboard_event(&event);
|
||||
|
||||
{
|
||||
let mut current_state = this.state.borrow_mut();
|
||||
current_state.modifiers = modifiers;
|
||||
current_state.capslock = capslock;
|
||||
}
|
||||
|
||||
this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
|
||||
modifiers,
|
||||
capslock,
|
||||
}));
|
||||
|
||||
let key = dom_key_to_gpui_key(&event);
|
||||
|
||||
if is_modifier_only_key(&key) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.prevent_default();
|
||||
|
||||
let key_char = compute_key_char(&event, &key, &modifiers);
|
||||
|
||||
let keystroke = Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char,
|
||||
};
|
||||
|
||||
this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke }));
|
||||
})
|
||||
}
|
||||
|
||||
fn register_focus(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("focus", move |_event: JsValue| {
|
||||
{
|
||||
let mut state = this.state.borrow_mut();
|
||||
state.is_active = true;
|
||||
}
|
||||
let mut callbacks = this.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = callbacks.active_status_change {
|
||||
callback(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn register_blur(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("blur", move |_event: JsValue| {
|
||||
{
|
||||
let mut state = this.state.borrow_mut();
|
||||
state.is_active = false;
|
||||
}
|
||||
let mut callbacks = this.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = callbacks.active_status_change {
|
||||
callback(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn register_pointer_enter(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("pointerenter", move |_event: JsValue| {
|
||||
{
|
||||
let mut state = this.state.borrow_mut();
|
||||
state.is_hovered = true;
|
||||
}
|
||||
let mut callbacks = this.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = callbacks.hover_status_change {
|
||||
callback(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn register_pointer_leave_hover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
|
||||
let this = Rc::clone(self);
|
||||
self.listen("pointerleave", move |_event: JsValue| {
|
||||
{
|
||||
let mut state = this.state.borrow_mut();
|
||||
state.is_hovered = false;
|
||||
}
|
||||
let mut callbacks = this.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = callbacks.hover_status_change {
|
||||
callback(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String {
|
||||
let key = event.key();
|
||||
match key.as_str() {
|
||||
"Enter" => "enter".to_string(),
|
||||
"Backspace" => "backspace".to_string(),
|
||||
"Tab" => "tab".to_string(),
|
||||
"Escape" => "escape".to_string(),
|
||||
"Delete" => "delete".to_string(),
|
||||
" " => "space".to_string(),
|
||||
"ArrowLeft" => "left".to_string(),
|
||||
"ArrowRight" => "right".to_string(),
|
||||
"ArrowUp" => "up".to_string(),
|
||||
"ArrowDown" => "down".to_string(),
|
||||
"Home" => "home".to_string(),
|
||||
"End" => "end".to_string(),
|
||||
"PageUp" => "pageup".to_string(),
|
||||
"PageDown" => "pagedown".to_string(),
|
||||
"Insert" => "insert".to_string(),
|
||||
"Control" => "control".to_string(),
|
||||
"Alt" => "alt".to_string(),
|
||||
"Shift" => "shift".to_string(),
|
||||
"Meta" => "platform".to_string(),
|
||||
"CapsLock" => "capslock".to_string(),
|
||||
other => {
|
||||
if let Some(rest) = other.strip_prefix('F') {
|
||||
if let Ok(number) = rest.parse::<u8>() {
|
||||
if (1..=35).contains(&number) {
|
||||
return format!("f{number}");
|
||||
}
|
||||
}
|
||||
}
|
||||
other.to_lowercase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dom_mouse_button_to_gpui(button: i16) -> MouseButton {
|
||||
match button {
|
||||
0 => MouseButton::Left,
|
||||
1 => MouseButton::Middle,
|
||||
2 => MouseButton::Right,
|
||||
3 => MouseButton::Navigate(NavigationDirection::Back),
|
||||
4 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||
_ => MouseButton::Left,
|
||||
}
|
||||
}
|
||||
|
||||
fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers {
|
||||
Modifiers {
|
||||
control: event.ctrl_key(),
|
||||
alt: event.alt_key(),
|
||||
shift: event.shift_key(),
|
||||
platform: event.meta_key(),
|
||||
function: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers {
|
||||
let mouse_event: &web_sys::MouseEvent = event.as_ref();
|
||||
Modifiers {
|
||||
control: mouse_event.ctrl_key(),
|
||||
alt: mouse_event.alt_key(),
|
||||
shift: mouse_event.shift_key(),
|
||||
platform: mouse_event.meta_key(),
|
||||
function: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers {
|
||||
Modifiers {
|
||||
control: event.ctrl_key(),
|
||||
alt: event.alt_key(),
|
||||
shift: event.shift_key(),
|
||||
platform: event.meta_key(),
|
||||
function: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock {
|
||||
Capslock {
|
||||
on: event.get_modifier_state("CapsLock"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool {
|
||||
let navigator = browser_window.navigator();
|
||||
|
||||
#[allow(deprecated)]
|
||||
// navigator.platform() is deprecated but navigator.userAgentData is not widely available yet
|
||||
if let Ok(platform) = navigator.platform() {
|
||||
if platform.contains("Mac") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(user_agent) = navigator.user_agent() {
|
||||
return user_agent.contains("Mac");
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_modifier_only_key(key: &str) -> bool {
|
||||
matches!(key, "control" | "alt" | "shift" | "platform" | "capslock")
|
||||
}
|
||||
|
||||
fn compute_key_char(
|
||||
event: &web_sys::KeyboardEvent,
|
||||
gpui_key: &str,
|
||||
modifiers: &Modifiers,
|
||||
) -> Option<String> {
|
||||
if modifiers.platform || modifiers.control {
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_modifier_only_key(gpui_key) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if gpui_key == "space" {
|
||||
return Some(" ".to_string());
|
||||
}
|
||||
|
||||
let raw_key = event.key();
|
||||
|
||||
if raw_key.len() == 1 {
|
||||
return Some(raw_key);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point<Pixels> {
|
||||
let mouse_event: &web_sys::MouseEvent = event.as_ref();
|
||||
mouse_position_in_element(mouse_event)
|
||||
}
|
||||
|
||||
fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point<Pixels> {
|
||||
// offset_x/offset_y give position relative to the target element's padding edge
|
||||
point(px(event.offset_x() as f32), px(event.offset_y() as f32))
|
||||
}
|
||||
|
||||
fn extract_file_paths_from_drag(
|
||||
event: &web_sys::DragEvent,
|
||||
) -> smallvec::SmallVec<[std::path::PathBuf; 2]> {
|
||||
let mut paths = smallvec![];
|
||||
let Some(data_transfer) = event.data_transfer() else {
|
||||
return paths;
|
||||
};
|
||||
let file_list = data_transfer.files();
|
||||
let Some(files) = file_list else {
|
||||
return paths;
|
||||
};
|
||||
for index in 0..files.length() {
|
||||
if let Some(file) = files.get(index) {
|
||||
paths.push(std::path::PathBuf::from(file.name()));
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
16
crates/gpui_web/src/gpui_web.rs
Normal file
16
crates/gpui_web/src/gpui_web.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#![cfg(target_family = "wasm")]
|
||||
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
mod events;
|
||||
mod keyboard;
|
||||
mod logging;
|
||||
mod platform;
|
||||
mod window;
|
||||
|
||||
pub use dispatcher::WebDispatcher;
|
||||
pub use display::WebDisplay;
|
||||
pub use keyboard::WebKeyboardLayout;
|
||||
pub use logging::init_logging;
|
||||
pub use platform::WebPlatform;
|
||||
pub use window::WebWindow;
|
||||
19
crates/gpui_web/src/keyboard.rs
Normal file
19
crates/gpui_web/src/keyboard.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use gpui::PlatformKeyboardLayout;
|
||||
|
||||
pub struct WebKeyboardLayout;
|
||||
|
||||
impl WebKeyboardLayout {
|
||||
pub fn new() -> Self {
|
||||
WebKeyboardLayout
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformKeyboardLayout for WebKeyboardLayout {
|
||||
fn id(&self) -> &str {
|
||||
"us"
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"US"
|
||||
}
|
||||
}
|
||||
37
crates/gpui_web/src/logging.rs
Normal file
37
crates/gpui_web/src/logging.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use log::{Level, Log, Metadata, Record};
|
||||
|
||||
struct ConsoleLogger;
|
||||
|
||||
impl Log for ConsoleLogger {
|
||||
fn enabled(&self, _metadata: &Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = format!(
|
||||
"[{}] {}: {}",
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args()
|
||||
);
|
||||
let js_string = wasm_bindgen::JsValue::from_str(&message);
|
||||
|
||||
match record.level() {
|
||||
Level::Error => web_sys::console::error_1(&js_string),
|
||||
Level::Warn => web_sys::console::warn_1(&js_string),
|
||||
Level::Info => web_sys::console::info_1(&js_string),
|
||||
Level::Debug | Level::Trace => web_sys::console::log_1(&js_string),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
pub fn init_logging() {
|
||||
log::set_logger(&ConsoleLogger).ok();
|
||||
log::set_max_level(log::LevelFilter::Info);
|
||||
}
|
||||
341
crates/gpui_web/src/platform.rs
Normal file
341
crates/gpui_web/src/platform.rs
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
use crate::dispatcher::WebDispatcher;
|
||||
use crate::display::WebDisplay;
|
||||
use crate::keyboard::WebKeyboardLayout;
|
||||
use crate::window::WebWindow;
|
||||
use anyhow::Result;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper,
|
||||
ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
|
||||
ThermalState, WindowAppearance, WindowParams,
|
||||
};
|
||||
use gpui_wgpu::WgpuContext;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
static BUNDLED_FONTS: &[&[u8]] = &[
|
||||
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
|
||||
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
|
||||
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
|
||||
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
|
||||
include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
|
||||
include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
|
||||
include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
|
||||
include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
|
||||
];
|
||||
|
||||
pub struct WebPlatform {
|
||||
browser_window: web_sys::Window,
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
active_window: RefCell<Option<AnyWindowHandle>>,
|
||||
active_display: Rc<dyn PlatformDisplay>,
|
||||
callbacks: RefCell<WebPlatformCallbacks>,
|
||||
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WebPlatformCallbacks {
|
||||
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
reopen: Option<Box<dyn FnMut()>>,
|
||||
app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
will_open_app_menu: Option<Box<dyn FnMut()>>,
|
||||
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
keyboard_layout_change: Option<Box<dyn FnMut()>>,
|
||||
thermal_state_change: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
impl WebPlatform {
|
||||
pub fn new() -> Self {
|
||||
let browser_window =
|
||||
web_sys::window().expect("must be running in a browser window context");
|
||||
let dispatcher = Arc::new(WebDispatcher::new(browser_window.clone()));
|
||||
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
||||
let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
|
||||
"IBM Plex Sans",
|
||||
));
|
||||
let fonts = BUNDLED_FONTS
|
||||
.iter()
|
||||
.map(|bytes| Cow::Borrowed(*bytes))
|
||||
.collect();
|
||||
if let Err(error) = text_system.add_fonts(fonts) {
|
||||
log::error!("failed to load bundled fonts: {error:#}");
|
||||
}
|
||||
let text_system: Arc<dyn PlatformTextSystem> = text_system;
|
||||
let active_display: Rc<dyn PlatformDisplay> =
|
||||
Rc::new(WebDisplay::new(browser_window.clone()));
|
||||
|
||||
Self {
|
||||
browser_window,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
text_system,
|
||||
active_window: RefCell::new(None),
|
||||
active_display,
|
||||
callbacks: RefCell::new(WebPlatformCallbacks::default()),
|
||||
wgpu_context: Rc::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for WebPlatform {
|
||||
fn background_executor(&self) -> BackgroundExecutor {
|
||||
self.background_executor.clone()
|
||||
}
|
||||
|
||||
fn foreground_executor(&self) -> ForegroundExecutor {
|
||||
self.foreground_executor.clone()
|
||||
}
|
||||
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||
let wgpu_context = self.wgpu_context.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match WgpuContext::new_web().await {
|
||||
Ok(context) => {
|
||||
log::info!("WebGPU context initialized successfully");
|
||||
*wgpu_context.borrow_mut() = Some(context);
|
||||
on_finish_launching();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to initialize WebGPU context: {err:#}");
|
||||
on_finish_launching();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
|
||||
}
|
||||
|
||||
fn restart(&self, _binary_path: Option<PathBuf>) {}
|
||||
|
||||
fn activate(&self, _ignoring_other_apps: bool) {}
|
||||
|
||||
fn hide(&self) {}
|
||||
|
||||
fn hide_other_apps(&self) {}
|
||||
|
||||
fn unhide_other_apps(&self) {}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
vec![self.active_display.clone()]
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(self.active_display.clone())
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
*self.active_window.borrow()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
let context_ref = self.wgpu_context.borrow();
|
||||
let context = context_ref.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
|
||||
})?;
|
||||
|
||||
let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
|
||||
*self.active_window.borrow_mut() = Some(handle);
|
||||
Ok(Box::new(window))
|
||||
}
|
||||
|
||||
fn window_appearance(&self) -> WindowAppearance {
|
||||
let Ok(Some(media_query)) = self
|
||||
.browser_window
|
||||
.match_media("(prefers-color-scheme: dark)")
|
||||
else {
|
||||
return WindowAppearance::Light;
|
||||
};
|
||||
if media_query.matches() {
|
||||
WindowAppearance::Dark
|
||||
} else {
|
||||
WindowAppearance::Light
|
||||
}
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
if let Err(error) = self.browser_window.open_with_url(url) {
|
||||
log::warn!("Failed to open URL '{url}': {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
|
||||
self.callbacks.borrow_mut().open_urls = Some(callback);
|
||||
}
|
||||
|
||||
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
tx.send(Err(anyhow::anyhow!(
|
||||
"prompt_for_paths is not supported on the web"
|
||||
)))
|
||||
.ok();
|
||||
rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
_directory: &Path,
|
||||
_suggested_name: Option<&str>,
|
||||
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
sender
|
||||
.send(Err(anyhow::anyhow!(
|
||||
"prompt_for_new_path is not supported on the web"
|
||||
)))
|
||||
.ok();
|
||||
receiver
|
||||
}
|
||||
|
||||
fn can_select_mixed_files_and_dirs(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn reveal_path(&self, _path: &Path) {}
|
||||
|
||||
fn open_with_system(&self, _path: &Path) {}
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().quit = Some(callback);
|
||||
}
|
||||
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().reopen = Some(callback);
|
||||
}
|
||||
|
||||
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
|
||||
|
||||
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
|
||||
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
self.callbacks.borrow_mut().app_menu_action = Some(callback);
|
||||
}
|
||||
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
|
||||
}
|
||||
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn thermal_state(&self) -> ThermalState {
|
||||
ThermalState::Nominal
|
||||
}
|
||||
|
||||
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().thermal_state_change = Some(callback);
|
||||
}
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
"Web"
|
||||
}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
Err(anyhow::anyhow!("app_path is not available on the web"))
|
||||
}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
|
||||
Err(anyhow::anyhow!(
|
||||
"path_for_auxiliary_executable is not available on the web"
|
||||
))
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let css_cursor = match style {
|
||||
CursorStyle::Arrow => "default",
|
||||
CursorStyle::IBeam => "text",
|
||||
CursorStyle::Crosshair => "crosshair",
|
||||
CursorStyle::ClosedHand => "grabbing",
|
||||
CursorStyle::OpenHand => "grab",
|
||||
CursorStyle::PointingHand => "pointer",
|
||||
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
|
||||
"ew-resize"
|
||||
}
|
||||
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
|
||||
"ns-resize"
|
||||
}
|
||||
CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
|
||||
CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
|
||||
CursorStyle::ResizeColumn => "col-resize",
|
||||
CursorStyle::ResizeRow => "row-resize",
|
||||
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
||||
CursorStyle::OperationNotAllowed => "not-allowed",
|
||||
CursorStyle::DragLink => "alias",
|
||||
CursorStyle::DragCopy => "copy",
|
||||
CursorStyle::ContextualMenu => "context-menu",
|
||||
CursorStyle::None => "none",
|
||||
};
|
||||
|
||||
if let Some(document) = self.browser_window.document() {
|
||||
if let Some(body) = document.body() {
|
||||
if let Err(error) = body.style().set_property("cursor", css_cursor) {
|
||||
log::warn!("Failed to set cursor style: {error:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, _item: ClipboardItem) {}
|
||||
|
||||
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"credential storage is not available on the web"
|
||||
)))
|
||||
}
|
||||
|
||||
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"credential storage is not available on the web"
|
||||
)))
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(WebKeyboardLayout)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
|
||||
}
|
||||
}
|
||||
689
crates/gpui_web/src/window.rs
Normal file
689
crates/gpui_web/src/window.rs
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
use crate::display::WebDisplay;
|
||||
use crate::events::{ClickState, WebEventListeners, is_mac_platform};
|
||||
use std::sync::Arc;
|
||||
use std::{cell::Cell, cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs,
|
||||
Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
|
||||
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||||
ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowControlArea, WindowControls, WindowDecorations, WindowParams, px,
|
||||
};
|
||||
use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WebWindowCallbacks {
|
||||
pub(crate) request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
|
||||
pub(crate) input: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
|
||||
pub(crate) active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
pub(crate) hover_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
pub(crate) resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
pub(crate) moved: Option<Box<dyn FnMut()>>,
|
||||
pub(crate) should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
pub(crate) close: Option<Box<dyn FnOnce()>>,
|
||||
pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
pub(crate) hit_test_window_control: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
|
||||
}
|
||||
|
||||
pub(crate) struct WebWindowMutableState {
|
||||
pub(crate) renderer: WgpuRenderer,
|
||||
pub(crate) bounds: Bounds<Pixels>,
|
||||
pub(crate) scale_factor: f32,
|
||||
pub(crate) max_texture_dimension: u32,
|
||||
pub(crate) title: String,
|
||||
pub(crate) input_handler: Option<PlatformInputHandler>,
|
||||
pub(crate) is_fullscreen: bool,
|
||||
pub(crate) is_active: bool,
|
||||
pub(crate) is_hovered: bool,
|
||||
pub(crate) mouse_position: Point<Pixels>,
|
||||
pub(crate) modifiers: Modifiers,
|
||||
pub(crate) capslock: Capslock,
|
||||
}
|
||||
|
||||
pub(crate) struct WebWindowInner {
|
||||
pub(crate) browser_window: web_sys::Window,
|
||||
pub(crate) canvas: web_sys::HtmlCanvasElement,
|
||||
pub(crate) has_device_pixel_support: bool,
|
||||
pub(crate) is_mac: bool,
|
||||
pub(crate) state: RefCell<WebWindowMutableState>,
|
||||
pub(crate) callbacks: RefCell<WebWindowCallbacks>,
|
||||
pub(crate) click_state: RefCell<ClickState>,
|
||||
pub(crate) pressed_button: Cell<Option<MouseButton>>,
|
||||
pub(crate) last_physical_size: Cell<(u32, u32)>,
|
||||
pub(crate) notify_scale: Cell<bool>,
|
||||
mql_handle: RefCell<Option<MqlHandle>>,
|
||||
}
|
||||
|
||||
pub struct WebWindow {
|
||||
inner: Rc<WebWindowInner>,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
#[allow(dead_code)]
|
||||
handle: AnyWindowHandle,
|
||||
_raf_closure: Closure<dyn FnMut()>,
|
||||
_resize_observer: Option<web_sys::ResizeObserver>,
|
||||
_resize_observer_closure: Closure<dyn FnMut(js_sys::Array)>,
|
||||
_event_listeners: WebEventListeners,
|
||||
}
|
||||
|
||||
impl WebWindow {
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
_params: WindowParams,
|
||||
context: &WgpuContext,
|
||||
browser_window: web_sys::Window,
|
||||
) -> anyhow::Result<Self> {
|
||||
let document = browser_window
|
||||
.document()
|
||||
.ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?;
|
||||
|
||||
let canvas: web_sys::HtmlCanvasElement = document
|
||||
.create_element("canvas")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))?
|
||||
.dyn_into()
|
||||
.map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?;
|
||||
|
||||
let dpr = browser_window.device_pixel_ratio() as f32;
|
||||
let max_texture_dimension = context.device.limits().max_texture_dimension_2d;
|
||||
let has_device_pixel_support = check_device_pixel_support();
|
||||
|
||||
canvas.set_tab_index(0);
|
||||
|
||||
let style = canvas.style();
|
||||
style
|
||||
.set_property("width", "100%")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?;
|
||||
style
|
||||
.set_property("height", "100%")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?;
|
||||
style
|
||||
.set_property("display", "block")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?;
|
||||
style
|
||||
.set_property("outline", "none")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?;
|
||||
style
|
||||
.set_property("touch-action", "none")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?;
|
||||
|
||||
let body = document
|
||||
.body()
|
||||
.ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?;
|
||||
body.append_child(&canvas)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?;
|
||||
|
||||
canvas.focus().ok();
|
||||
|
||||
let device_size = Size {
|
||||
width: DevicePixels(0),
|
||||
height: DevicePixels(0),
|
||||
};
|
||||
|
||||
let renderer_config = WgpuSurfaceConfig {
|
||||
size: device_size,
|
||||
transparent: false,
|
||||
};
|
||||
|
||||
let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?;
|
||||
|
||||
let display: Rc<dyn PlatformDisplay> = Rc::new(WebDisplay::new(browser_window.clone()));
|
||||
|
||||
let initial_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: Size::default(),
|
||||
};
|
||||
|
||||
let mutable_state = WebWindowMutableState {
|
||||
renderer,
|
||||
bounds: initial_bounds,
|
||||
scale_factor: dpr,
|
||||
max_texture_dimension,
|
||||
title: String::new(),
|
||||
input_handler: None,
|
||||
is_fullscreen: false,
|
||||
is_active: true,
|
||||
is_hovered: false,
|
||||
mouse_position: Point::default(),
|
||||
modifiers: Modifiers::default(),
|
||||
capslock: Capslock::default(),
|
||||
};
|
||||
|
||||
let is_mac = is_mac_platform(&browser_window);
|
||||
|
||||
let inner = Rc::new(WebWindowInner {
|
||||
browser_window,
|
||||
canvas,
|
||||
has_device_pixel_support,
|
||||
is_mac,
|
||||
state: RefCell::new(mutable_state),
|
||||
callbacks: RefCell::new(WebWindowCallbacks::default()),
|
||||
click_state: RefCell::new(ClickState::default()),
|
||||
pressed_button: Cell::new(None),
|
||||
last_physical_size: Cell::new((0, 0)),
|
||||
notify_scale: Cell::new(false),
|
||||
mql_handle: RefCell::new(None),
|
||||
});
|
||||
|
||||
let raf_closure = inner.create_raf_closure();
|
||||
inner.schedule_raf(&raf_closure);
|
||||
|
||||
let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner));
|
||||
let resize_observer =
|
||||
web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok();
|
||||
|
||||
if let Some(ref observer) = resize_observer {
|
||||
inner.observe_canvas(observer);
|
||||
inner.watch_dpr_changes(observer);
|
||||
}
|
||||
|
||||
let event_listeners = inner.register_event_listeners();
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
display,
|
||||
handle,
|
||||
_raf_closure: raf_closure,
|
||||
_resize_observer: resize_observer,
|
||||
_resize_observer_closure: resize_observer_closure,
|
||||
_event_listeners: event_listeners,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_resize_observer_closure(
|
||||
inner: Rc<WebWindowInner>,
|
||||
) -> Closure<dyn FnMut(js_sys::Array)> {
|
||||
Closure::new(move |entries: js_sys::Array| {
|
||||
let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() {
|
||||
Some(entry) => entry,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let dpr = inner.browser_window.device_pixel_ratio();
|
||||
let dpr_f32 = dpr as f32;
|
||||
|
||||
let (physical_width, physical_height, logical_width, logical_height) =
|
||||
if inner.has_device_pixel_support {
|
||||
let size: web_sys::ResizeObserverSize = entry
|
||||
.device_pixel_content_box_size()
|
||||
.get(0)
|
||||
.unchecked_into();
|
||||
let pw = size.inline_size() as u32;
|
||||
let ph = size.block_size() as u32;
|
||||
let lw = pw as f64 / dpr;
|
||||
let lh = ph as f64 / dpr;
|
||||
(pw, ph, lw as f32, lh as f32)
|
||||
} else {
|
||||
// Safari fallback: use contentRect (always CSS px).
|
||||
let rect = entry.content_rect();
|
||||
let lw = rect.width() as f32;
|
||||
let lh = rect.height() as f32;
|
||||
let pw = (lw as f64 * dpr).round() as u32;
|
||||
let ph = (lh as f64 * dpr).round() as u32;
|
||||
(pw, ph, lw, lh)
|
||||
};
|
||||
|
||||
let scale_changed = inner.notify_scale.replace(false);
|
||||
let prev = inner.last_physical_size.get();
|
||||
let size_changed = prev != (physical_width, physical_height);
|
||||
|
||||
if !scale_changed && !size_changed {
|
||||
return;
|
||||
}
|
||||
inner
|
||||
.last_physical_size
|
||||
.set((physical_width, physical_height));
|
||||
|
||||
// Skip rendering to a zero-size canvas (e.g. display:none).
|
||||
if physical_width == 0 || physical_height == 0 {
|
||||
let mut s = inner.state.borrow_mut();
|
||||
s.bounds.size = Size::default();
|
||||
s.scale_factor = dpr_f32;
|
||||
// Still fire the callback so GPUI knows the window is gone.
|
||||
drop(s);
|
||||
let mut cbs = inner.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = cbs.resize {
|
||||
callback(Size::default(), dpr_f32);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let max_texture_dimension = inner.state.borrow().max_texture_dimension;
|
||||
let clamped_width = physical_width.min(max_texture_dimension);
|
||||
let clamped_height = physical_height.min(max_texture_dimension);
|
||||
|
||||
inner.canvas.set_width(clamped_width);
|
||||
inner.canvas.set_height(clamped_height);
|
||||
|
||||
{
|
||||
let mut s = inner.state.borrow_mut();
|
||||
s.bounds.size = Size {
|
||||
width: px(logical_width),
|
||||
height: px(logical_height),
|
||||
};
|
||||
s.scale_factor = dpr_f32;
|
||||
s.renderer.update_drawable_size(Size {
|
||||
width: DevicePixels(clamped_width as i32),
|
||||
height: DevicePixels(clamped_height as i32),
|
||||
});
|
||||
}
|
||||
|
||||
let new_size = Size {
|
||||
width: px(logical_width),
|
||||
height: px(logical_height),
|
||||
};
|
||||
|
||||
let mut cbs = inner.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = cbs.resize {
|
||||
callback(new_size, dpr_f32);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WebWindowInner {
|
||||
fn create_raf_closure(self: &Rc<Self>) -> Closure<dyn FnMut()> {
|
||||
let raf_handle: Rc<RefCell<Option<js_sys::Function>>> = Rc::new(RefCell::new(None));
|
||||
let raf_handle_inner = Rc::clone(&raf_handle);
|
||||
|
||||
let this = Rc::clone(self);
|
||||
let closure = Closure::new(move || {
|
||||
{
|
||||
let mut callbacks = this.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = callbacks.request_frame {
|
||||
callback(RequestFrameOptions {
|
||||
require_presentation: true,
|
||||
force_render: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Re-schedule for the next frame
|
||||
if let Some(ref func) = *raf_handle_inner.borrow() {
|
||||
this.browser_window.request_animation_frame(func).ok();
|
||||
}
|
||||
});
|
||||
|
||||
let js_func: js_sys::Function =
|
||||
closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
|
||||
*raf_handle.borrow_mut() = Some(js_func);
|
||||
|
||||
closure
|
||||
}
|
||||
|
||||
fn schedule_raf(&self, closure: &Closure<dyn FnMut()>) {
|
||||
self.browser_window
|
||||
.request_animation_frame(closure.as_ref().unchecked_ref())
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn observe_canvas(&self, observer: &web_sys::ResizeObserver) {
|
||||
observer.unobserve(&self.canvas);
|
||||
if self.has_device_pixel_support {
|
||||
let options = web_sys::ResizeObserverOptions::new();
|
||||
options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox);
|
||||
observer.observe_with_options(&self.canvas, &options);
|
||||
} else {
|
||||
observer.observe(&self.canvas);
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_dpr_changes(self: &Rc<Self>, observer: &web_sys::ResizeObserver) {
|
||||
let current_dpr = self.browser_window.device_pixel_ratio();
|
||||
let media_query =
|
||||
format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})");
|
||||
let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let this = Rc::clone(self);
|
||||
let observer = observer.clone();
|
||||
|
||||
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
|
||||
this.notify_scale.set(true);
|
||||
this.observe_canvas(&observer);
|
||||
this.watch_dpr_changes(&observer);
|
||||
});
|
||||
|
||||
mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
|
||||
.ok();
|
||||
|
||||
*self.mql_handle.borrow_mut() = Some(MqlHandle {
|
||||
mql,
|
||||
_closure: closure,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn register_visibility_change(
|
||||
self: &Rc<Self>,
|
||||
) -> Option<Closure<dyn FnMut(JsValue)>> {
|
||||
let document = self.browser_window.document()?;
|
||||
let this = Rc::clone(self);
|
||||
|
||||
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
|
||||
let is_visible = this
|
||||
.browser_window
|
||||
.document()
|
||||
.map(|doc| {
|
||||
let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_string())
|
||||
.unwrap_or_default();
|
||||
state_str == "visible"
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
{
|
||||
let mut state = this.state.borrow_mut();
|
||||
state.is_active = is_visible;
|
||||
}
|
||||
let mut callbacks = this.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = callbacks.active_status_change {
|
||||
callback(is_visible);
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref())
|
||||
.ok();
|
||||
|
||||
Some(closure)
|
||||
}
|
||||
|
||||
pub(crate) fn register_appearance_change(
|
||||
self: &Rc<Self>,
|
||||
) -> Option<Closure<dyn FnMut(JsValue)>> {
|
||||
let mql = self
|
||||
.browser_window
|
||||
.match_media("(prefers-color-scheme: dark)")
|
||||
.ok()??;
|
||||
|
||||
let this = Rc::clone(self);
|
||||
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
|
||||
let mut callbacks = this.callbacks.borrow_mut();
|
||||
if let Some(ref mut callback) = callbacks.appearance_changed {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
|
||||
.ok();
|
||||
|
||||
Some(closure)
|
||||
}
|
||||
}
|
||||
|
||||
fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance {
|
||||
let is_dark = browser_window
|
||||
.match_media("(prefers-color-scheme: dark)")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|mql| mql.matches())
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_dark {
|
||||
WindowAppearance::Dark
|
||||
} else {
|
||||
WindowAppearance::Light
|
||||
}
|
||||
}
|
||||
|
||||
struct MqlHandle {
|
||||
mql: web_sys::MediaQueryList,
|
||||
_closure: Closure<dyn FnMut(JsValue)>,
|
||||
}
|
||||
|
||||
impl Drop for MqlHandle {
|
||||
fn drop(&mut self) {
|
||||
self.mql
|
||||
.remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
// Safari does not support `devicePixelContentBoxSize`, so detect whether it's available.
|
||||
fn check_device_pixel_support() -> bool {
|
||||
let global: JsValue = js_sys::global().into();
|
||||
let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else {
|
||||
return false;
|
||||
};
|
||||
let descriptor = js_sys::Object::get_own_property_descriptor(
|
||||
&prototype.unchecked_into::<js_sys::Object>(),
|
||||
&"devicePixelContentBoxSize".into(),
|
||||
);
|
||||
!descriptor.is_undefined()
|
||||
}
|
||||
|
||||
impl raw_window_handle::HasWindowHandle for WebWindow {
|
||||
fn window_handle(
|
||||
&self,
|
||||
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
||||
let canvas_ref: &JsValue = self.inner.canvas.as_ref();
|
||||
let obj = std::ptr::NonNull::from(canvas_ref).cast::<std::ffi::c_void>();
|
||||
let handle = raw_window_handle::WebCanvasWindowHandle::new(obj);
|
||||
Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
|
||||
impl raw_window_handle::HasDisplayHandle for WebWindow {
|
||||
fn display_handle(
|
||||
&self,
|
||||
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||||
Ok(raw_window_handle::DisplayHandle::web())
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for WebWindow {
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.inner.state.borrow().bounds
|
||||
}
|
||||
|
||||
fn is_maximized(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
WindowBounds::Windowed(self.bounds())
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.inner.state.borrow().bounds.size
|
||||
}
|
||||
|
||||
fn resize(&mut self, size: Size<Pixels>) {
|
||||
let style = self.inner.canvas.style();
|
||||
style
|
||||
.set_property("width", &format!("{}px", f32::from(size.width)))
|
||||
.ok();
|
||||
style
|
||||
.set_property("height", &format!("{}px", f32::from(size.height)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.inner.state.borrow().scale_factor
|
||||
}
|
||||
|
||||
fn appearance(&self) -> WindowAppearance {
|
||||
current_appearance(&self.inner.browser_window)
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(self.display.clone())
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
self.inner.state.borrow().mouse_position
|
||||
}
|
||||
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
self.inner.state.borrow().modifiers
|
||||
}
|
||||
|
||||
fn capslock(&self) -> Capslock {
|
||||
self.inner.state.borrow().capslock
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
self.inner.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||||
self.inner.state.borrow_mut().input_handler.take()
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_level: PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[PromptButton],
|
||||
) -> Option<futures::channel::oneshot::Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
self.inner.state.borrow_mut().is_active = true;
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.inner.state.borrow().is_active
|
||||
}
|
||||
|
||||
fn is_hovered(&self) -> bool {
|
||||
self.inner.state.borrow().is_hovered
|
||||
}
|
||||
|
||||
fn background_appearance(&self) -> WindowBackgroundAppearance {
|
||||
WindowBackgroundAppearance::Opaque
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.inner.state.borrow_mut().title = title.to_owned();
|
||||
if let Some(document) = self.inner.browser_window.document() {
|
||||
document.set_title(title);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
|
||||
|
||||
fn minimize(&self) {
|
||||
log::warn!("WebWindow::minimize is not supported in the browser");
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
log::warn!("WebWindow::zoom is not supported in the browser");
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
let mut state = self.inner.state.borrow_mut();
|
||||
state.is_fullscreen = !state.is_fullscreen;
|
||||
|
||||
if state.is_fullscreen {
|
||||
let canvas: &web_sys::Element = self.inner.canvas.as_ref();
|
||||
canvas.request_fullscreen().ok();
|
||||
} else {
|
||||
if let Some(document) = self.inner.browser_window.document() {
|
||||
document.exit_fullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
self.inner.state.borrow().is_fullscreen
|
||||
}
|
||||
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
|
||||
self.inner.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
|
||||
self.inner.callbacks.borrow_mut().input = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.inner.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.inner.callbacks.borrow_mut().hover_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.inner.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.inner.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>) {
|
||||
self.inner.callbacks.borrow_mut().close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
|
||||
self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback);
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
self.inner.state.borrow_mut().renderer.draw(scene);
|
||||
}
|
||||
|
||||
fn completed_frame(&self) {
|
||||
// On web, presentation happens automatically via wgpu surface present
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
self.inner.state.borrow().renderer.sprite_atlas().clone()
|
||||
}
|
||||
|
||||
fn is_subpixel_rendering_supported(&self) -> bool {
|
||||
self.inner
|
||||
.state
|
||||
.borrow()
|
||||
.renderer
|
||||
.supports_dual_source_blending()
|
||||
}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
Some(self.inner.state.borrow().renderer.gpu_specs())
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
|
||||
|
||||
fn request_decorations(&self, _decorations: WindowDecorations) {}
|
||||
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {}
|
||||
|
||||
fn start_window_move(&self) {}
|
||||
|
||||
fn start_window_resize(&self, _edge: ResizeEdge) {}
|
||||
|
||||
fn window_decorations(&self) -> Decorations {
|
||||
Decorations::Server
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn window_controls(&self) -> WindowControls {
|
||||
WindowControls {
|
||||
fullscreen: true,
|
||||
maximize: false,
|
||||
minimize: false,
|
||||
window_menu: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_client_inset(&self, _inset: Pixels) {}
|
||||
}
|
||||
|
|
@ -11,16 +11,36 @@ workspace = true
|
|||
[lib]
|
||||
path = "src/gpui_wgpu.rs"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
[features]
|
||||
default = []
|
||||
font-kit = ["dep:font-kit"]
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
anyhow.workspace = true
|
||||
bytemuck = "1"
|
||||
collections.workspace = true
|
||||
cosmic-text = "0.17.0"
|
||||
etagere = "0.2"
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
profiling.workspace = true
|
||||
raw-window-handle = "0.6"
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
smallvec.workspace = true
|
||||
swash = "0.2.6"
|
||||
gpui_util.workspace = true
|
||||
wgpu.workspace = true
|
||||
|
||||
# Optional: only needed on platforms with multiple font sources (e.g. Linux)
|
||||
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
pollster.workspace = true
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = ["HtmlCanvasElement"] }
|
||||
js-sys = "0.3"
|
||||
645
crates/gpui_wgpu/src/cosmic_text_system.rs
Normal file
645
crates/gpui_wgpu/src/cosmic_text_system.rs
Normal file
|
|
@ -0,0 +1,645 @@
|
|||
use anyhow::{Context as _, Ok, Result};
|
||||
use collections::HashMap;
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
|
||||
FontSystem, ShapeBuffer, ShapeLine,
|
||||
};
|
||||
use gpui::{
|
||||
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, GlyphId, LineLayout,
|
||||
Pixels, PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y,
|
||||
ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point, size,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use parking_lot::RwLock;
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use swash::{
|
||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||
zeno::{Format, Vector},
|
||||
};
|
||||
|
||||
pub struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct FontKey {
|
||||
family: SharedString,
|
||||
features: FontFeatures,
|
||||
}
|
||||
|
||||
impl FontKey {
|
||||
fn new(family: SharedString, features: FontFeatures) -> Self {
|
||||
Self { family, features }
|
||||
}
|
||||
}
|
||||
|
||||
struct CosmicTextSystemState {
|
||||
font_system: FontSystem,
|
||||
scratch: ShapeBuffer,
|
||||
swash_scale_context: ScaleContext,
|
||||
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
|
||||
loaded_fonts: Vec<LoadedFont>,
|
||||
/// 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<FontKey, SmallVec<[FontId; 4]>>,
|
||||
system_font_fallback: String,
|
||||
}
|
||||
|
||||
struct LoadedFont {
|
||||
font: Arc<CosmicTextFont>,
|
||||
features: CosmicFontFeatures,
|
||||
is_known_emoji_font: bool,
|
||||
}
|
||||
|
||||
impl CosmicTextSystem {
|
||||
pub fn new(system_font_fallback: &str) -> Self {
|
||||
let font_system = FontSystem::new();
|
||||
|
||||
Self(RwLock::new(CosmicTextSystemState {
|
||||
font_system,
|
||||
scratch: ShapeBuffer::default(),
|
||||
swash_scale_context: ScaleContext::new(),
|
||||
loaded_fonts: Vec::new(),
|
||||
font_ids_by_family_cache: HashMap::default(),
|
||||
system_font_fallback: system_font_fallback.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn new_without_system_fonts(system_font_fallback: &str) -> Self {
|
||||
let font_system = FontSystem::new_with_locale_and_db(
|
||||
"en-US".to_string(),
|
||||
cosmic_text::fontdb::Database::new(),
|
||||
);
|
||||
|
||||
Self(RwLock::new(CosmicTextSystemState {
|
||||
font_system,
|
||||
scratch: ShapeBuffer::default(),
|
||||
swash_scale_context: ScaleContext::new(),
|
||||
loaded_fonts: Vec::new(),
|
||||
font_ids_by_family_cache: HashMap::default(),
|
||||
system_font_fallback: system_font_fallback.to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformTextSystem for CosmicTextSystem {
|
||||
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
self.0.write().add_fonts(fonts)
|
||||
}
|
||||
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
let mut result = self
|
||||
.0
|
||||
.read()
|
||||
.font_system
|
||||
.db()
|
||||
.faces()
|
||||
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
|
||||
.collect_vec();
|
||||
result.sort();
|
||||
result.dedup();
|
||||
result
|
||||
}
|
||||
|
||||
fn font_id(&self, font: &Font) -> Result<FontId> {
|
||||
let mut state = self.0.write();
|
||||
let key = FontKey::new(font.family.clone(), font.features.clone());
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||
state.font_ids_by_family_cache.insert(key.clone(), font_ids);
|
||||
state.font_ids_by_family_cache[&key].as_ref()
|
||||
};
|
||||
|
||||
let ix = find_best_match(font, candidates, &state)?;
|
||||
|
||||
Ok(candidates[ix])
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
|
||||
let metrics = self
|
||||
.0
|
||||
.read()
|
||||
.loaded_font(font_id)
|
||||
.font
|
||||
.as_swash()
|
||||
.metrics(&[]);
|
||||
|
||||
FontMetrics {
|
||||
units_per_em: metrics.units_per_em as u32,
|
||||
ascent: metrics.ascent,
|
||||
descent: -metrics.descent,
|
||||
line_gap: metrics.leading,
|
||||
underline_position: metrics.underline_offset,
|
||||
underline_thickness: metrics.stroke_size,
|
||||
cap_height: metrics.cap_height,
|
||||
x_height: metrics.x_height,
|
||||
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<Bounds<f32>> {
|
||||
let lock = self.0.read();
|
||||
let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
|
||||
let glyph_id = glyph_id.0 as u16;
|
||||
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<Size<f32>> {
|
||||
self.0.read().advance(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.0.read().glyph_for_char(font_id, ch)
|
||||
}
|
||||
|
||||
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
self.0.write().raster_bounds(params)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn recommended_rendering_mode(
|
||||
&self,
|
||||
_font_id: FontId,
|
||||
_font_size: Pixels,
|
||||
) -> TextRenderingMode {
|
||||
TextRenderingMode::Subpixel
|
||||
}
|
||||
}
|
||||
|
||||
impl CosmicTextSystemState {
|
||||
fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
|
||||
&self.loaded_fonts[font_id.0]
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> 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(())
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
let name = gpui::font_name_with_fallbacks(name, &self.system_font_fallback);
|
||||
|
||||
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::<SmallVec<[_; 4]>>();
|
||||
|
||||
let mut loaded_font_ids = SmallVec::new();
|
||||
for (font_id, postscript_name) in families {
|
||||
let font = self
|
||||
.font_system
|
||||
.get_font(font_id, cosmic_text::Weight::NORMAL)
|
||||
.context("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.len());
|
||||
loaded_font_ids.push(font_id);
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: cosmic_font_features(features)?,
|
||||
is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(loaded_font_ids)
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
|
||||
Ok(Size {
|
||||
width: glyph_metrics.advance_width(glyph_id.0 as u16),
|
||||
height: glyph_metrics.advance_height(glyph_id.0 as u16),
|
||||
})
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
|
||||
if glyph_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(GlyphId(glyph_id.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let image = self.render_glyph_image(params)?;
|
||||
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<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
|
||||
anyhow::bail!("glyph bounds are empty");
|
||||
}
|
||||
|
||||
let mut image = self.render_glyph_image(params)?;
|
||||
let bitmap_size = glyph_bounds.size;
|
||||
match image.content {
|
||||
swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in image.data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
Ok((bitmap_size, image.data))
|
||||
}
|
||||
swash::scale::image::Content::Mask => Ok((bitmap_size, image.data)),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_glyph_image(
|
||||
&mut self,
|
||||
params: &RenderGlyphParams,
|
||||
) -> Result<swash::scale::image::Image> {
|
||||
let loaded_font = &self.loaded_fonts[params.font_id.0];
|
||||
let font_ref = loaded_font.font.as_swash();
|
||||
let pixel_size = f32::from(params.font_size);
|
||||
|
||||
let subpixel_offset = Vector::new(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
);
|
||||
|
||||
let mut scaler = self
|
||||
.swash_scale_context
|
||||
.builder(font_ref)
|
||||
.size(pixel_size * params.scale_factor)
|
||||
.hint(true)
|
||||
.build();
|
||||
|
||||
let sources: &[Source] = if params.is_emoji {
|
||||
&[
|
||||
Source::ColorOutline(0),
|
||||
Source::ColorBitmap(StrikeWith::BestFit),
|
||||
Source::Outline,
|
||||
]
|
||||
} else {
|
||||
&[Source::Outline]
|
||||
};
|
||||
|
||||
let mut renderer = Render::new(sources);
|
||||
if params.subpixel_rendering {
|
||||
// There seems to be a bug in Swash where the B and R values are swapped.
|
||||
renderer
|
||||
.format(Format::subpixel_bgra())
|
||||
.offset(subpixel_offset);
|
||||
} else {
|
||||
renderer.format(Format::Alpha).offset(subpixel_offset);
|
||||
}
|
||||
|
||||
let glyph_id: u16 = params.glyph_id.0.try_into()?;
|
||||
renderer
|
||||
.render(&mut scaler, glyph_id)
|
||||
.with_context(|| format!("unable to render glyph via swash for {params:?}"))
|
||||
}
|
||||
|
||||
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
|
||||
/// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
|
||||
/// yet have an entry for this fallback font, and so one is added.
|
||||
///
|
||||
/// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
|
||||
/// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
|
||||
/// current use of this field is for the *input* of `layout_line`, and so it's fine to use
|
||||
/// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
|
||||
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> Result<FontId> {
|
||||
if let Some(ix) = self
|
||||
.loaded_fonts
|
||||
.iter()
|
||||
.position(|loaded_font| loaded_font.font.id() == id)
|
||||
{
|
||||
Ok(FontId(ix))
|
||||
} else {
|
||||
let font = self
|
||||
.font_system
|
||||
.get_font(id, cosmic_text::Weight::NORMAL)
|
||||
.context("failed to get fallback font from cosmic-text font system")?;
|
||||
let face = self
|
||||
.font_system
|
||||
.db()
|
||||
.face(id)
|
||||
.context("fallback font face not found in cosmic-text database")?;
|
||||
|
||||
let font_id = FontId(self.loaded_fonts.len());
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: CosmicFontFeatures::new(),
|
||||
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
|
||||
});
|
||||
|
||||
Ok(font_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
let loaded_font = self.loaded_font(run.font_id);
|
||||
let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
|
||||
log::warn!(
|
||||
"font face not found in database for font_id {:?}",
|
||||
run.font_id
|
||||
);
|
||||
offs += run.len;
|
||||
continue;
|
||||
};
|
||||
let Some(first_family) = face.families.first() else {
|
||||
log::warn!(
|
||||
"font face has no family names for font_id {:?}",
|
||||
run.font_id
|
||||
);
|
||||
offs += run.len;
|
||||
continue;
|
||||
};
|
||||
|
||||
attrs_list.add_span(
|
||||
offs..(offs + run.len),
|
||||
&Attrs::new()
|
||||
.metadata(run.font_id.0)
|
||||
.family(Family::Name(&first_family.0))
|
||||
.stretch(face.stretch)
|
||||
.style(face.style)
|
||||
.weight(face.weight)
|
||||
.font_features(loaded_font.features.clone()),
|
||||
);
|
||||
offs += run.len;
|
||||
}
|
||||
|
||||
let line = ShapeLine::new(
|
||||
&mut self.font_system,
|
||||
text,
|
||||
&attrs_list,
|
||||
cosmic_text::Shaping::Advanced,
|
||||
4,
|
||||
);
|
||||
let mut layout_lines = Vec::with_capacity(1);
|
||||
line.layout_to_buffer(
|
||||
&mut self.scratch,
|
||||
f32::from(font_size),
|
||||
None, // We do our own wrapping
|
||||
cosmic_text::Wrap::None,
|
||||
None,
|
||||
&mut layout_lines,
|
||||
None,
|
||||
cosmic_text::Hinting::Disabled,
|
||||
);
|
||||
|
||||
let Some(layout) = layout_lines.first() else {
|
||||
return LineLayout {
|
||||
font_size,
|
||||
width: Pixels::ZERO,
|
||||
ascent: Pixels::ZERO,
|
||||
descent: Pixels::ZERO,
|
||||
runs: Vec::new(),
|
||||
len: text.len(),
|
||||
};
|
||||
};
|
||||
|
||||
let mut runs: Vec<ShapedRun> = Vec::new();
|
||||
for glyph in &layout.glyphs {
|
||||
let mut font_id = FontId(glyph.metadata);
|
||||
let mut loaded_font = self.loaded_font(font_id);
|
||||
if loaded_font.font.id() != glyph.font_id {
|
||||
match self.font_id_for_cosmic_id(glyph.font_id) {
|
||||
std::result::Result::Ok(resolved_id) => {
|
||||
font_id = resolved_id;
|
||||
loaded_font = self.loaded_font(font_id);
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"failed to resolve cosmic font id {:?}: {error:#}",
|
||||
glyph.font_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let is_emoji = loaded_font.is_known_emoji_font;
|
||||
|
||||
// HACK: Prevent crash caused by variation selectors.
|
||||
if glyph.glyph_id == 3 && is_emoji {
|
||||
continue;
|
||||
}
|
||||
|
||||
let shaped_glyph = ShapedGlyph {
|
||||
id: GlyphId(glyph.glyph_id as u32),
|
||||
position: point(glyph.x.into(), glyph.y.into()),
|
||||
index: glyph.start,
|
||||
is_emoji,
|
||||
};
|
||||
|
||||
if let Some(last_run) = runs
|
||||
.last_mut()
|
||||
.filter(|last_run| last_run.font_id == font_id)
|
||||
{
|
||||
last_run.glyphs.push(shaped_glyph);
|
||||
} else {
|
||||
runs.push(ShapedRun {
|
||||
font_id,
|
||||
glyphs: vec![shaped_glyph],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LineLayout {
|
||||
font_size,
|
||||
width: layout.w.into(),
|
||||
ascent: layout.max_ascent.into(),
|
||||
descent: layout.max_descent.into(),
|
||||
runs,
|
||||
len: text.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
fn find_best_match(
|
||||
font: &Font,
|
||||
candidates: &[FontId],
|
||||
state: &CosmicTextSystemState,
|
||||
) -> Result<usize> {
|
||||
let candidate_properties = candidates
|
||||
.iter()
|
||||
.map(|font_id| {
|
||||
let database_id = state.loaded_font(*font_id).font.id();
|
||||
let face_info = state
|
||||
.font_system
|
||||
.db()
|
||||
.face(database_id)
|
||||
.context("font face not found in database")?;
|
||||
Ok(face_info_into_properties(face_info))
|
||||
})
|
||||
.collect::<Result<SmallVec<[_; 4]>>>()?;
|
||||
|
||||
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(ix)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "font-kit"))]
|
||||
fn find_best_match(
|
||||
font: &Font,
|
||||
candidates: &[FontId],
|
||||
state: &CosmicTextSystemState,
|
||||
) -> Result<usize> {
|
||||
if candidates.is_empty() {
|
||||
anyhow::bail!("requested font family contains no font matching the other parameters");
|
||||
}
|
||||
if candidates.len() == 1 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let target_weight = font.weight.0;
|
||||
let target_italic = matches!(
|
||||
font.style,
|
||||
gpui::FontStyle::Italic | gpui::FontStyle::Oblique
|
||||
);
|
||||
|
||||
let mut best_index = 0;
|
||||
let mut best_score = u32::MAX;
|
||||
|
||||
for (index, font_id) in candidates.iter().enumerate() {
|
||||
let database_id = state.loaded_font(*font_id).font.id();
|
||||
let face_info = state
|
||||
.font_system
|
||||
.db()
|
||||
.face(database_id)
|
||||
.context("font face not found in database")?;
|
||||
|
||||
let is_italic = matches!(
|
||||
face_info.style,
|
||||
cosmic_text::Style::Italic | cosmic_text::Style::Oblique
|
||||
);
|
||||
let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 };
|
||||
let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs();
|
||||
let score = style_penalty + weight_diff;
|
||||
|
||||
if score < best_score {
|
||||
best_score = score;
|
||||
best_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(best_index)
|
||||
}
|
||||
|
||||
fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
|
||||
let mut result = CosmicFontFeatures::new();
|
||||
for feature in features.0.iter() {
|
||||
let name_bytes: [u8; 4] = feature
|
||||
.0
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.context("Incorrect feature flag format")?;
|
||||
|
||||
let tag = cosmic_text::FeatureTag::new(&name_bytes);
|
||||
|
||||
result.set(tag, feature.1);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
fn font_into_properties(font: &gpui::Font) -> font_kit::properties::Properties {
|
||||
font_kit::properties::Properties {
|
||||
style: match font.style {
|
||||
gpui::FontStyle::Normal => font_kit::properties::Style::Normal,
|
||||
gpui::FontStyle::Italic => font_kit::properties::Style::Italic,
|
||||
gpui::FontStyle::Oblique => font_kit::properties::Style::Oblique,
|
||||
},
|
||||
weight: font_kit::properties::Weight(font.weight.0),
|
||||
stretch: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn check_is_known_emoji_font(postscript_name: &str) -> bool {
|
||||
// TODO: Include other common emoji fonts
|
||||
postscript_name == "NotoColorEmoji"
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
#![cfg(not(target_os = "windows"))]
|
||||
mod cosmic_text_system;
|
||||
mod wgpu_atlas;
|
||||
mod wgpu_context;
|
||||
mod wgpu_renderer;
|
||||
|
||||
pub use cosmic_text_system::*;
|
||||
pub use wgpu_atlas::*;
|
||||
pub use wgpu_context::*;
|
||||
pub use wgpu_renderer::*;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
enable dual_source_blending;
|
||||
/* Functions useful for debugging:
|
||||
|
||||
// A heat map color for debugging (blue -> cyan -> green -> yellow -> red).
|
||||
|
|
@ -501,11 +500,11 @@ fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
|
|||
// checkerboard
|
||||
let size = background.gradient_angle_or_pattern_height;
|
||||
let relative_position = position - bounds.origin;
|
||||
|
||||
|
||||
let x_index = floor(relative_position.x / size);
|
||||
let y_index = floor(relative_position.y / size);
|
||||
let should_be_colored = (x_index + y_index) % 2.0;
|
||||
|
||||
|
||||
background_color = solid_color;
|
||||
background_color.a *= saturate(should_be_colored);
|
||||
}
|
||||
|
|
@ -1033,7 +1032,7 @@ struct PathRasterizationVertex {
|
|||
struct PathRasterizationVarying {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) st_position: vec2<f32>,
|
||||
@location(1) vertex_id: u32,
|
||||
@location(1) @interpolate(flat) vertex_id: u32,
|
||||
//TODO: use `clip_distance` once Naga supports it
|
||||
@location(3) clip_distances: vec4<f32>,
|
||||
}
|
||||
|
|
@ -1072,14 +1071,14 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4<f
|
|||
let distance = f / length(gradient);
|
||||
alpha = saturate(0.5 - distance);
|
||||
}
|
||||
let gradient_color = prepare_gradient_color(
|
||||
let prepared_gradient = prepare_gradient_color(
|
||||
background.tag,
|
||||
background.color_space,
|
||||
background.solid,
|
||||
background.colors,
|
||||
);
|
||||
let color = gradient_color(background, input.position.xy, bounds,
|
||||
gradient_color.solid, gradient_color.color0, gradient_color.color1);
|
||||
prepared_gradient.solid, prepared_gradient.color0, prepared_gradient.color1);
|
||||
return vec4<f32>(color.rgb * color.a * alpha, color.a * alpha);
|
||||
}
|
||||
|
||||
|
|
@ -1334,57 +1333,3 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
|
|||
|
||||
return ycbcr_to_RGB * y_cb_cr;
|
||||
}
|
||||
|
||||
// --- subpixel sprites --- //
|
||||
|
||||
struct SubpixelSprite {
|
||||
order: u32,
|
||||
pad: u32,
|
||||
bounds: Bounds,
|
||||
content_mask: Bounds,
|
||||
color: Hsla,
|
||||
tile: AtlasTile,
|
||||
transformation: TransformationMatrix,
|
||||
}
|
||||
@group(1) @binding(0) var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
|
||||
|
||||
struct SubpixelSpriteOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) tile_position: vec2<f32>,
|
||||
@location(1) @interpolate(flat) color: vec4<f32>,
|
||||
@location(3) clip_distances: vec4<f32>,
|
||||
}
|
||||
|
||||
struct SubpixelSpriteFragmentOutput {
|
||||
@location(0) @blend_src(0) foreground: vec4<f32>,
|
||||
@location(0) @blend_src(1) alpha: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
|
||||
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||
let sprite = b_subpixel_sprites[instance_id];
|
||||
|
||||
var out = SubpixelSpriteOutput();
|
||||
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_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
|
||||
let sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
|
||||
let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios);
|
||||
|
||||
// Alpha clip after using the derivatives.
|
||||
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||
return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
|
||||
}
|
||||
|
||||
var out = SubpixelSpriteFragmentOutput();
|
||||
out.foreground = vec4<f32>(input.color.rgb, 1.0);
|
||||
out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
53
crates/gpui_wgpu/src/shaders_subpixel.wgsl
Normal file
53
crates/gpui_wgpu/src/shaders_subpixel.wgsl
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// --- subpixel sprites --- //
|
||||
|
||||
struct SubpixelSprite {
|
||||
order: u32,
|
||||
pad: u32,
|
||||
bounds: Bounds,
|
||||
content_mask: Bounds,
|
||||
color: Hsla,
|
||||
tile: AtlasTile,
|
||||
transformation: TransformationMatrix,
|
||||
}
|
||||
@group(1) @binding(0) var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
|
||||
|
||||
struct SubpixelSpriteOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) tile_position: vec2<f32>,
|
||||
@location(1) @interpolate(flat) color: vec4<f32>,
|
||||
@location(3) clip_distances: vec4<f32>,
|
||||
}
|
||||
|
||||
struct SubpixelSpriteFragmentOutput {
|
||||
@location(0) @blend_src(0) foreground: vec4<f32>,
|
||||
@location(0) @blend_src(1) alpha: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
|
||||
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||
let sprite = b_subpixel_sprites[instance_id];
|
||||
|
||||
var out = SubpixelSpriteOutput();
|
||||
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_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
|
||||
let sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
|
||||
let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios);
|
||||
|
||||
// Alpha clip after using the derivatives.
|
||||
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||
return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
|
||||
}
|
||||
|
||||
var out = SubpixelSpriteFragmentOutput();
|
||||
out.foreground = vec4<f32>(input.color.rgb, 1.0);
|
||||
out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#[cfg(not(target_family = "wasm"))]
|
||||
use anyhow::Context as _;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use gpui_util::ResultExt;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct WgpuContext {
|
||||
pub instance: wgpu::Instance,
|
||||
|
|
@ -11,6 +13,7 @@ pub struct WgpuContext {
|
|||
}
|
||||
|
||||
impl WgpuContext {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn new(instance: wgpu::Instance, surface: &wgpu::Surface<'_>) -> anyhow::Result<Self> {
|
||||
let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
|
||||
Ok(val) => parse_pci_id(&val)
|
||||
|
|
@ -24,7 +27,7 @@ impl WgpuContext {
|
|||
}
|
||||
};
|
||||
|
||||
let adapter = smol::block_on(Self::select_adapter(
|
||||
let adapter = pollster::block_on(Self::select_adapter(
|
||||
&instance,
|
||||
device_id_filter,
|
||||
Some(surface),
|
||||
|
|
@ -60,6 +63,73 @@ impl WgpuContext {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub async fn new_web() -> anyhow::Result<Self> {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
|
||||
flags: wgpu::InstanceFlags::default(),
|
||||
backend_options: wgpu::BackendOptions::default(),
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
});
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::None,
|
||||
compatible_surface: None,
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?;
|
||||
Self::create_context(instance, adapter).await
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
async fn create_context(
|
||||
instance: wgpu::Instance,
|
||||
adapter: wgpu::Adapter,
|
||||
) -> anyhow::Result<Self> {
|
||||
log::info!(
|
||||
"Selected GPU adapter: {:?} ({:?})",
|
||||
adapter.get_info().name,
|
||||
adapter.get_info().backend
|
||||
);
|
||||
|
||||
let dual_source_blending_available = adapter
|
||||
.features()
|
||||
.contains(wgpu::Features::DUAL_SOURCE_BLENDING);
|
||||
|
||||
let mut required_features = wgpu::Features::empty();
|
||||
if dual_source_blending_available {
|
||||
required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
|
||||
} else {
|
||||
log::info!(
|
||||
"Dual-source blending not available on this GPU. \
|
||||
Subpixel text antialiasing will be disabled."
|
||||
);
|
||||
}
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: Some("gpui_device"),
|
||||
required_features,
|
||||
required_limits: wgpu::Limits::default(),
|
||||
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||
trace: wgpu::Trace::Off,
|
||||
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
|
||||
|
||||
Ok(Self {
|
||||
instance,
|
||||
adapter,
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
dual_source_blending: dual_source_blending_available,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn instance() -> wgpu::Instance {
|
||||
wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
|
||||
|
|
@ -84,6 +154,7 @@ impl WgpuContext {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn create_device(adapter: &wgpu::Adapter) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
|
||||
let dual_source_blending_available = adapter
|
||||
.features()
|
||||
|
|
@ -99,7 +170,7 @@ impl WgpuContext {
|
|||
);
|
||||
}
|
||||
|
||||
let (device, queue) = smol::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
label: Some("gpui_device"),
|
||||
required_features,
|
||||
required_limits: wgpu::Limits::default(),
|
||||
|
|
@ -112,6 +183,7 @@ impl WgpuContext {
|
|||
Ok((device, queue, dual_source_blending_available))
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
async fn select_adapter(
|
||||
instance: &wgpu::Instance,
|
||||
device_id_filter: Option<u32>,
|
||||
|
|
@ -182,6 +254,7 @@ impl WgpuContext {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
|
||||
let mut id = id.trim();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use gpui::{
|
|||
PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, SubpixelSprite,
|
||||
Underline, get_gamma_correction_ratios,
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
use std::num::NonZeroU64;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -123,6 +124,7 @@ impl WgpuRenderer {
|
|||
/// # Safety
|
||||
/// The caller must ensure that the window handle remains valid for the lifetime
|
||||
/// of the returned renderer.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn new<W: HasWindowHandle + HasDisplayHandle>(
|
||||
gpu_context: &mut Option<WgpuContext>,
|
||||
window: &W,
|
||||
|
|
@ -165,6 +167,27 @@ impl WgpuRenderer {
|
|||
None => gpu_context.insert(WgpuContext::new(instance, &surface)?),
|
||||
};
|
||||
|
||||
Self::new_with_surface(context, surface, config)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn new_from_canvas(
|
||||
context: &WgpuContext,
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
config: WgpuSurfaceConfig,
|
||||
) -> anyhow::Result<Self> {
|
||||
let surface = context
|
||||
.instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?;
|
||||
Self::new_with_surface(context, surface, config)
|
||||
}
|
||||
|
||||
pub fn new_with_surface(
|
||||
context: &WgpuContext,
|
||||
surface: wgpu::Surface<'static>,
|
||||
config: WgpuSurfaceConfig,
|
||||
) -> anyhow::Result<Self> {
|
||||
let surface_caps = surface.get_capabilities(&context.adapter);
|
||||
let preferred_formats = [
|
||||
wgpu::TextureFormat::Bgra8Unorm,
|
||||
|
|
@ -497,12 +520,25 @@ impl WgpuRenderer {
|
|||
path_sample_count: u32,
|
||||
dual_source_blending: bool,
|
||||
) -> WgpuPipelines {
|
||||
let shader_source = include_str!("shaders.wgsl");
|
||||
let base_shader_source = include_str!("shaders.wgsl");
|
||||
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("gpui_shaders"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(base_shader_source)),
|
||||
});
|
||||
|
||||
let subpixel_shader_source = include_str!("shaders_subpixel.wgsl");
|
||||
let subpixel_shader_module = if dual_source_blending {
|
||||
let combined = format!(
|
||||
"enable dual_source_blending;\n{base_shader_source}\n{subpixel_shader_source}"
|
||||
);
|
||||
Some(device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("gpui_subpixel_shaders"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(combined)),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let blend_mode = match alpha_mode {
|
||||
wgpu::CompositeAlphaMode::PreMultiplied => {
|
||||
wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING
|
||||
|
|
@ -523,7 +559,8 @@ impl WgpuRenderer {
|
|||
data_layout: &wgpu::BindGroupLayout,
|
||||
topology: wgpu::PrimitiveTopology,
|
||||
color_targets: &[Option<wgpu::ColorTargetState>],
|
||||
sample_count: u32| {
|
||||
sample_count: u32,
|
||||
module: &wgpu::ShaderModule| {
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some(&format!("{name}_layout")),
|
||||
bind_group_layouts: &[globals_layout, data_layout],
|
||||
|
|
@ -534,13 +571,13 @@ impl WgpuRenderer {
|
|||
label: Some(name),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader_module,
|
||||
module,
|
||||
entry_point: Some(vs_entry),
|
||||
buffers: &[],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader_module,
|
||||
module,
|
||||
entry_point: Some(fs_entry),
|
||||
targets: color_targets,
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
|
|
@ -574,6 +611,7 @@ impl WgpuRenderer {
|
|||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
&[Some(color_target.clone())],
|
||||
1,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
let shadows = create_pipeline(
|
||||
|
|
@ -585,6 +623,7 @@ impl WgpuRenderer {
|
|||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
&[Some(color_target.clone())],
|
||||
1,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
let path_rasterization = create_pipeline(
|
||||
|
|
@ -600,6 +639,7 @@ impl WgpuRenderer {
|
|||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
path_sample_count,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
let paths_blend = wgpu::BlendState {
|
||||
|
|
@ -628,6 +668,7 @@ impl WgpuRenderer {
|
|||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
1,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
let underlines = create_pipeline(
|
||||
|
|
@ -639,6 +680,7 @@ impl WgpuRenderer {
|
|||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
&[Some(color_target.clone())],
|
||||
1,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
let mono_sprites = create_pipeline(
|
||||
|
|
@ -650,9 +692,10 @@ impl WgpuRenderer {
|
|||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
&[Some(color_target.clone())],
|
||||
1,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
let subpixel_sprites = if dual_source_blending {
|
||||
let subpixel_sprites = if let Some(subpixel_module) = &subpixel_shader_module {
|
||||
let subpixel_blend = wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Src1,
|
||||
|
|
@ -679,6 +722,7 @@ impl WgpuRenderer {
|
|||
write_mask: wgpu::ColorWrites::COLOR,
|
||||
})],
|
||||
1,
|
||||
subpixel_module,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
|
@ -693,6 +737,7 @@ impl WgpuRenderer {
|
|||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
&[Some(color_target.clone())],
|
||||
1,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
let surfaces = create_pipeline(
|
||||
|
|
@ -704,6 +749,7 @@ impl WgpuRenderer {
|
|||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
&[Some(color_target)],
|
||||
1,
|
||||
&shader_module,
|
||||
);
|
||||
|
||||
WgpuPipelines {
|
||||
|
|
@ -837,6 +883,10 @@ impl WgpuRenderer {
|
|||
&self.atlas
|
||||
}
|
||||
|
||||
pub fn supports_dual_source_blending(&self) -> bool {
|
||||
self.dual_source_blending
|
||||
}
|
||||
|
||||
pub fn gpu_specs(&self) -> GpuSpecs {
|
||||
GpuSpecs {
|
||||
is_software_emulated: self.adapter_info.device_type == wgpu::DeviceType::Cpu,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue