Detailed changes
@@ -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 }}"
@@ -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"
@@ -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")
}
+ #[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) {
+ 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;
+ 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;
}
- 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"))]
@@ -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
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -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))
+}
@@ -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",
+] }
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -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"]
@@ -0,0 +1,3 @@
+/dist
+/target
+Cargo.lock
@@ -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"
@@ -0,0 +1 @@
+../../../../LICENSE-APACHE
@@ -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>
@@ -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);
+ });
+}
@@ -0,0 +1,4 @@
+[toolchain]
+channel = "nightly"
+targets = ["wasm32-unknown-unknown"]
+components = ["rust-src", "rustfmt", "clippy"]
@@ -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" }
@@ -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();
+ }
+ }
+}
@@ -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 },
+ }
+ }
+}
@@ -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
+}
@@ -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;
@@ -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"
+ }
+}
@@ -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);
+}
@@ -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);
+ }
+}
@@ -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"
@@ -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;
-}
@@ -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,
@@ -38,7 +38,7 @@ futures.workspace = true
git.workspace = true
git_hosting_providers.workspace = true
git2 = { workspace = true, features = ["vendored-libgit2"] }
-gpui = { workspace = true, features = ["windows-manifest"] }
+gpui.workspace = true
gpui_platform.workspace = true
gpui_tokio.workspace = true
http_client.workspace = true
@@ -82,6 +82,7 @@ minidumper.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
+gpui = { workspace = true, features = ["windows-manifest"] }
[dev-dependencies]
action_log.workspace = true
@@ -23,3 +23,4 @@ flume = "0.11"
futures.workspace = true
parking_lot.workspace = true
rand.workspace = true
+web-time.workspace = true
@@ -1,6 +1,8 @@
use chrono::{DateTime, Utc};
use parking_lot::Mutex;
-use std::time::{Duration, Instant};
+use std::time::Duration;
+
+pub use web_time::Instant;
pub trait Clock {
fn utc_now(&self) -> DateTime<Utc>;
@@ -1,4 +1,4 @@
-use crate::{Priority, RunnableMeta, Scheduler, SessionId, Timer};
+use crate::{Instant, Priority, RunnableMeta, Scheduler, SessionId, Timer};
use std::{
future::Future,
marker::PhantomData,
@@ -12,7 +12,7 @@ use std::{
},
task::{Context, Poll},
thread::{self, ThreadId},
- time::{Duration, Instant},
+ time::Duration,
};
#[derive(Clone)]
@@ -1,6 +1,6 @@
use crate::{
- BackgroundExecutor, Clock, ForegroundExecutor, Priority, RunnableMeta, Scheduler, SessionId,
- TestClock, Timer,
+ BackgroundExecutor, Clock, ForegroundExecutor, Instant, Priority, RunnableMeta, Scheduler,
+ SessionId, TestClock, Timer,
};
use async_task::Runnable;
use backtrace::{Backtrace, BacktraceFrame};
@@ -26,7 +26,7 @@ use std::{
},
task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
thread::{self, Thread},
- time::{Duration, Instant},
+ time::Duration,
};
const PENDING_TRACES_VAR_NAME: &str = "PENDING_TRACES";
@@ -19,14 +19,11 @@ test-support = ["git2", "rand", "util_macros"]
[dependencies]
anyhow.workspace = true
-async-fs.workspace = true
async_zip.workspace = true
collections.workspace = true
-dirs.workspace = true
dunce = "1.0"
futures-lite.workspace = true
futures.workspace = true
-git2 = { workspace = true, optional = true }
globset.workspace = true
itertools.workspace = true
log.workspace = true
@@ -38,15 +35,21 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
shlex.workspace = true
-smol.workspace = true
take-until.workspace = true
tempfile.workspace = true
unicase.workspace = true
url.workspace = true
percent-encoding.workspace = true
util_macros = { workspace = true, optional = true }
-walkdir.workspace = true
+gpui_util.workspace = true
+
+[target.'cfg(not(target_family = "wasm"))'.dependencies]
+smol.workspace = true
which.workspace = true
+git2 = { workspace = true, optional = true }
+async-fs.workspace = true
+walkdir.workspace = true
+dirs.workspace = true
[target.'cfg(unix)'.dependencies]
command-fds = "0.3.1"
@@ -6,6 +6,7 @@ use async_zip::base::read;
use futures::AsyncSeek;
use futures::{AsyncRead, io::BufReader};
+#[cfg(any(unix, windows))]
fn archive_path_is_normal(filename: &str) -> bool {
Path::new(filename).components().all(|c| {
matches!(
@@ -64,7 +65,7 @@ pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) ->
Ok(())
}
-#[cfg(not(windows))]
+#[cfg(unix)]
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
// Unix needs file permissions copied when extracting.
// This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used.
@@ -81,7 +82,7 @@ pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) ->
extract_seekable_zip(destination, file).await
}
-#[cfg(not(windows))]
+#[cfg(unix)]
pub async fn extract_seekable_zip<R: AsyncRead + AsyncSeek + Unpin>(
destination: &Path,
reader: R,
@@ -1,4 +1,3 @@
-use anyhow::Context;
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use itertools::Itertools;
use regex::Regex;
@@ -9,20 +8,19 @@ use std::error::Error;
use std::fmt::{Display, Formatter};
use std::mem;
use std::path::StripPrefixError;
-use std::sync::{Arc, OnceLock};
+use std::sync::Arc;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::LazyLock,
};
+use crate::rel_path::RelPath;
use crate::rel_path::RelPathBuf;
-use crate::{rel_path::RelPath, shell::ShellKind};
-
-static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Returns the path to the user's home directory.
pub fn home_dir() -> &'static PathBuf {
+ static HOME_DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
HOME_DIR.get_or_init(|| {
if cfg!(any(test, feature = "test-support")) {
if cfg!(target_os = "macos") {
@@ -56,6 +54,13 @@ pub trait PathExt {
where
Self: From<&'a Path>,
{
+ #[cfg(target_family = "wasm")]
+ {
+ std::str::from_utf8(bytes)
+ .map(Path::new)
+ .map(Into::into)
+ .map_err(Into::into)
+ }
#[cfg(unix)]
{
use std::os::unix::prelude::OsStrExt;
@@ -63,6 +68,7 @@ pub trait PathExt {
}
#[cfg(windows)]
{
+ use anyhow::Context;
use tendril::fmt::{Format, WTF8};
WTF8::validate(bytes)
.then(|| {
@@ -86,11 +92,17 @@ pub trait PathExt {
fn multiple_extensions(&self) -> Option<String>;
/// Try to make a shell-safe representation of the path.
- fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result<String>;
+ #[cfg(not(target_family = "wasm"))]
+ fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result<String>;
}
impl<T: AsRef<Path>> PathExt for T {
fn compact(&self) -> PathBuf {
+ #[cfg(target_family = "wasm")]
+ {
+ self.as_ref().to_path_buf()
+ }
+ #[cfg(not(target_family = "wasm"))]
if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
match self.as_ref().strip_prefix(home_dir().as_path()) {
Ok(relative_path) => {
@@ -164,7 +176,9 @@ impl<T: AsRef<Path>> PathExt for T {
Some(parts.into_iter().join("."))
}
- fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result<String> {
+ #[cfg(not(target_family = "wasm"))]
+ fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result<String> {
+ use anyhow::Context;
let path_str = self
.as_ref()
.to_str()
@@ -1,16 +1,14 @@
mod assertions;
mod marked_text;
-use git2;
-use std::{
- ffi::OsStr,
- path::{Path, PathBuf},
-};
-use tempfile::TempDir;
-
pub use assertions::*;
pub use marked_text::*;
+use git2;
+use std::ffi::OsStr;
+use std::path::{Path, PathBuf};
+use tempfile::TempDir;
+
pub struct TempTree {
_temp_dir: TempDir,
path: PathBuf,
@@ -45,6 +43,7 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
Value::Object(_) => {
fs::create_dir(&path).unwrap();
+ #[cfg(not(target_family = "wasm"))]
if path.file_name() == Some(OsStr::new(".git")) {
git2::Repository::init(path.parent().unwrap()).unwrap();
}
@@ -1,4 +1,3 @@
-pub mod arc_cow;
pub mod archive;
pub mod command;
pub mod fs;
@@ -17,40 +16,27 @@ pub mod size;
pub mod test;
pub mod time;
-use anyhow::{Context as _, Result};
-use futures::Future;
+use anyhow::Result;
use itertools::Either;
-use paths::PathExt;
use regex::Regex;
use std::path::{Path, PathBuf};
-use std::sync::{LazyLock, OnceLock};
+use std::sync::LazyLock;
use std::{
borrow::Cow,
cmp::{self, Ordering},
- env,
- ops::{AddAssign, Range, RangeInclusive},
- panic::Location,
- pin::Pin,
- task::{Context, Poll},
- time::Instant,
+ ops::{Range, RangeInclusive},
};
use unicase::UniCase;
+pub use gpui_util::*;
+
pub use take_until::*;
#[cfg(any(test, feature = "test-support"))]
pub use util_macros::{line_endings, path, uri};
-#[macro_export]
-macro_rules! debug_panic {
- ( $($fmt_arg:tt)* ) => {
- if cfg!(debug_assertions) {
- panic!( $($fmt_arg)* );
- } else {
- let backtrace = std::backtrace::Backtrace::capture();
- log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace);
- }
- };
-}
+pub use self::shell::{
+ get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell,
+};
#[inline]
pub const fn is_utf8_char_boundary(u8: u8) -> bool {
@@ -174,12 +160,6 @@ fn test_truncate_lines_to_byte_limit() {
);
}
-pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
- let prev = *value;
- *value += T::from(1);
- prev
-}
-
/// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and
/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this,
/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator.
@@ -287,7 +267,7 @@ fn load_shell_from_passwd() -> Result<()> {
);
let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };
- let should_set_shell = env::var("SHELL").map_or(true, |shell_env| {
+ let should_set_shell = std::env::var("SHELL").map_or(true, |shell_env| {
shell_env != shell && !std::path::Path::new(&shell_env).exists()
});
@@ -296,7 +276,7 @@ fn load_shell_from_passwd() -> Result<()> {
"updating SHELL environment variable to value from passwd entry: {:?}",
shell,
);
- unsafe { env::set_var("SHELL", shell) };
+ unsafe { std::env::set_var("SHELL", shell) };
}
Ok(())
@@ -304,6 +284,8 @@ fn load_shell_from_passwd() -> Result<()> {
/// Returns a shell escaped path for the current zed executable
pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<String> {
+ use anyhow::Context as _;
+ use paths::PathExt;
let mut zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
if cfg!(target_os = "linux")
@@ -326,6 +308,7 @@ pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<S
/// Returns a path for the zed cli executable, this function
/// should be called from the zed executable, not zed-cli.
pub fn get_zed_cli_path() -> Result<PathBuf> {
+ use anyhow::Context as _;
let zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let parent = zed_path
@@ -365,6 +348,8 @@ pub fn get_zed_cli_path() -> Result<PathBuf> {
#[cfg(unix)]
pub async fn load_login_shell_environment() -> Result<()> {
+ use anyhow::Context as _;
+
load_shell_from_passwd().log_err();
// If possible, we want to `cd` in the user's `$HOME` to trigger programs
@@ -383,7 +368,7 @@ pub async fn load_login_shell_environment() -> Result<()> {
if name == "SHLVL" {
continue;
}
- unsafe { env::set_var(&name, &value) };
+ unsafe { std::env::set_var(&name, &value) };
}
log::info!(
@@ -404,7 +389,7 @@ pub fn set_pre_exec_to_start_new_session(
) -> &mut std::process::Command {
// safety: code in pre_exec should be signal safe.
// https://man7.org/linux/man-pages/man7/signal-safety.7.html
- #[cfg(not(target_os = "windows"))]
+ #[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
command.pre_exec(|| {
@@ -485,25 +470,6 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se
}
}
-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()
- }
-}
-
pub fn expanded_and_wrapped_usize_range(
range: Range<usize>,
additional_before: usize,
@@ -570,222 +536,6 @@ pub fn wrapped_usize_outward_from(
})
}
-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(target_os = "windows"))]
- let file = caller.file();
- #[cfg(target_os = "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))
-}
-
#[cfg(any(test, feature = "test-support"))]
mod rng {
use rand::prelude::*;
@@ -849,23 +599,6 @@ pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
}
}
-/// Expands to an immediately-invoked function expression. Good for using the ? operator
-/// in functions which do not return an Option or Result.
-///
-/// Accepts a normal block, an async block, or an async move block.
-#[macro_export]
-macro_rules! maybe {
- ($block:block) => {
- (|| $block)()
- };
- (async $block:block) => {
- (async || $block)()
- };
- (async move $block:block) => {
- (async move || $block)()
- };
-}
-
pub trait RangeExt<T> {
fn sorted(&self) -> Self;
fn to_inclusive(&self) -> RangeInclusive<T>;
@@ -1022,10 +755,6 @@ pub fn default<D: Default>() -> D {
Default::default()
}
-pub use self::shell::{
- get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell,
-};
-
#[derive(Debug)]
pub enum ConnectionResult<O> {
Timeout,
@@ -1049,15 +778,6 @@ impl<O> From<anyhow::Result<O>> for ConnectionResult<O> {
}
}
-#[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
-}
-
/// Normalizes a path by resolving `.` and `..` components without
/// requiring the path to exist on disk (unlike `canonicalize`).
pub fn normalize_path(path: &Path) -> PathBuf {
@@ -118,11 +118,7 @@ git_hosting_providers.workspace = true
git_ui.workspace = true
go_to_line.workspace = true
system_specs.workspace = true
-gpui = { workspace = true, features = [
- "wayland",
- "windows-manifest",
- "x11",
-] }
+gpui.workspace = true
gpui_platform = {workspace = true, features=["screen-capture", "font-kit", "wayland", "x11"]}
image = { workspace = true, optional = true }
semver = { workspace = true, optional = true }
@@ -232,11 +228,18 @@ zlog_settings.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
etw_tracing.workspace = true
windows.workspace = true
+gpui = { workspace = true, features = [
+ "windows-manifest",
+] }
[target.'cfg(target_os = "windows")'.build-dependencies]
winresource = "0.1"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
+gpui = { workspace = true, features = [
+ "wayland",
+ "x11",
+] }
ashpd.workspace = true
[dev-dependencies]
@@ -4,5 +4,6 @@ profile = "minimal"
components = [ "rustfmt", "clippy", "rust-analyzer", "rust-src" ]
targets = [
"wasm32-wasip2", # extensions
+ "wasm32-unknown-unknown", # gpui on the web
"x86_64-unknown-linux-musl", # remote server
]
@@ -20,6 +20,8 @@ enum CliCommand {
PackageConformity(tasks::package_conformity::PackageConformityArgs),
/// Publishes GPUI and its dependencies to crates.io.
PublishGpui(tasks::publish_gpui::PublishGpuiArgs),
+ /// Builds GPUI web examples and serves them.
+ WebExamples(tasks::web_examples::WebExamplesArgs),
Workflows(tasks::workflows::GenerateWorkflowArgs),
}
@@ -33,6 +35,7 @@ fn main() -> Result<()> {
tasks::package_conformity::run_package_conformity(args)
}
CliCommand::PublishGpui(args) => tasks::publish_gpui::run_publish_gpui(args),
+ CliCommand::WebExamples(args) => tasks::web_examples::run_web_examples(args),
CliCommand::Workflows(args) => tasks::workflows::run_workflows(args),
}
}
@@ -2,4 +2,5 @@ pub mod clippy;
pub mod licenses;
pub mod package_conformity;
pub mod publish_gpui;
+pub mod web_examples;
pub mod workflows;
@@ -0,0 +1,334 @@
+#![allow(clippy::disallowed_methods, reason = "tooling is exempt")]
+
+use std::io::Write;
+use std::path::Path;
+use std::process::Command;
+
+use anyhow::{Context as _, Result, bail};
+use clap::Parser;
+
+#[derive(Parser)]
+pub struct WebExamplesArgs {
+ #[arg(long)]
+ pub release: bool,
+ #[arg(long, default_value = "8080")]
+ pub port: u16,
+ #[arg(long)]
+ pub no_serve: bool,
+}
+
+fn check_program(binary: &str, install_hint: &str) -> Result<()> {
+ match Command::new(binary).arg("--version").output() {
+ Ok(output) if output.status.success() => Ok(()),
+ _ => bail!("`{binary}` not found. Install with: {install_hint}"),
+ }
+}
+
+fn discover_examples() -> Result<Vec<String>> {
+ let examples_dir = Path::new("crates/gpui/examples");
+ let mut names = Vec::new();
+
+ for entry in std::fs::read_dir(examples_dir).context("failed to read crates/gpui/examples")? {
+ let path = entry?.path();
+ if path.extension().and_then(|e| e.to_str()) == Some("rs") {
+ if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
+ names.push(stem.to_string());
+ }
+ }
+ }
+
+ if names.is_empty() {
+ bail!("no examples found in crates/gpui/examples");
+ }
+
+ names.sort();
+ Ok(names)
+}
+
+pub fn run_web_examples(args: WebExamplesArgs) -> Result<()> {
+ let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
+ let profile = if args.release { "release" } else { "debug" };
+ let out_dir = "target/web-examples";
+
+ check_program("wasm-bindgen", "cargo install wasm-bindgen-cli")?;
+
+ let examples = discover_examples()?;
+ eprintln!(
+ "Building {} example(s) for wasm32-unknown-unknown ({profile})...\n",
+ examples.len()
+ );
+
+ std::fs::create_dir_all(out_dir).context("failed to create output directory")?;
+
+ eprintln!("Building all examples...");
+
+ let mut cmd = Command::new(&cargo);
+ cmd.args([
+ "build",
+ "--target",
+ "wasm32-unknown-unknown",
+ "-p",
+ "gpui",
+ "--keep-going",
+ ]);
+ for name in &examples {
+ cmd.args(["--example", name]);
+ }
+ if args.release {
+ cmd.arg("--release");
+ }
+
+ let _ = cmd.status().context("failed to run cargo build")?;
+
+ // Run wasm-bindgen on each .wasm that was produced.
+ let mut succeeded: Vec<String> = Vec::new();
+ let mut failed: Vec<String> = Vec::new();
+
+ for name in &examples {
+ let wasm_path = format!("target/wasm32-unknown-unknown/{profile}/examples/{name}.wasm");
+ if !Path::new(&wasm_path).exists() {
+ eprintln!("[{name}] SKIPPED (build failed)");
+ failed.push(name.clone());
+ continue;
+ }
+
+ eprintln!("[{name}] Running wasm-bindgen...");
+
+ let example_dir = format!("{out_dir}/{name}");
+ std::fs::create_dir_all(&example_dir)
+ .with_context(|| format!("failed to create {example_dir}"))?;
+
+ let status = Command::new("wasm-bindgen")
+ .args([
+ &wasm_path,
+ "--target",
+ "web",
+ "--no-typescript",
+ "--out-dir",
+ &example_dir,
+ "--out-name",
+ name,
+ ])
+ .status()
+ .context("failed to run wasm-bindgen")?;
+ if !status.success() {
+ eprintln!("[{name}] SKIPPED (wasm-bindgen failed)");
+ failed.push(name.clone());
+ continue;
+ }
+
+ // Write per-example index.html.
+ let html_path = format!("{example_dir}/index.html");
+ std::fs::File::create(&html_path)
+ .and_then(|mut file| file.write_all(make_example_html(name).as_bytes()))
+ .with_context(|| format!("failed to write {html_path}"))?;
+
+ eprintln!("[{name}] OK");
+ succeeded.push(name.clone());
+ }
+
+ if succeeded.is_empty() {
+ bail!("all {} examples failed to build", examples.len());
+ }
+
+ let example_names: Vec<&str> = succeeded.iter().map(|s| s.as_str()).collect();
+ let index_path = format!("{out_dir}/index.html");
+ std::fs::File::create(&index_path)
+ .and_then(|mut file| file.write_all(make_gallery_html(&example_names).as_bytes()))
+ .context("failed to write index.html")?;
+
+ if args.no_serve {
+ return Ok(());
+ }
+
+ // Serve with COEP/COOP headers required for WebGPU / SharedArrayBuffer.
+ eprintln!("Serving on http://127.0.0.1:{}...", args.port);
+
+ let server_script = format!(
+ r#"
+import http.server
+class Handler(http.server.SimpleHTTPRequestHandler):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, directory="{out_dir}", **kwargs)
+ def end_headers(self):
+ self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
+ self.send_header("Cross-Origin-Opener-Policy", "same-origin")
+ super().end_headers()
+http.server.HTTPServer(("127.0.0.1", {port}), Handler).serve_forever()
+"#,
+ port = args.port,
+ );
+
+ let status = Command::new("python3")
+ .args(["-c", &server_script])
+ .status()
+ .context("failed to run python3 http server (is python3 installed?)")?;
+ if !status.success() {
+ bail!("python3 http server exited with: {status}");
+ }
+
+ Ok(())
+}
+
+fn make_example_html(name: &str) -> String {
+ format!(
+ r#"<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>GPUI Web: {name}</title>
+ <style>
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
+ html, body {{
+ width: 100%; height: 100%; overflow: hidden;
+ background: #1e1e2e; color: #cdd6f4;
+ font-family: system-ui, -apple-system, sans-serif;
+ }}
+ canvas {{ display: block; width: 100%; height: 100%; }}
+ #loading {{
+ position: fixed; inset: 0;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.25rem; opacity: 0.6;
+ }}
+ #loading.hidden {{ display: none; }}
+ </style>
+</head>
+<body>
+ <div id="loading">Loading {name}…</div>
+ <script type="module">
+ import init from './{name}.js';
+ await init();
+ document.getElementById('loading').classList.add('hidden');
+ </script>
+</body>
+</html>
+"#
+ )
+}
+
+fn make_gallery_html(examples: &[&str]) -> String {
+ let mut buttons = String::new();
+ for name in examples {
+ buttons.push_str(&format!(
+ " <button class=\"example-btn\" data-name=\"{name}\">{name}</button>\n"
+ ));
+ }
+
+ let first = examples.first().copied().unwrap_or("hello_web");
+
+ format!(
+ r##"<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>GPUI Web Examples</title>
+ <style>
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
+ html, body {{
+ width: 100%; height: 100%; overflow: hidden;
+ background: #1e1e2e; color: #cdd6f4;
+ font-family: system-ui, -apple-system, sans-serif;
+ }}
+ #app {{ display: flex; width: 100%; height: 100%; }}
+
+ #sidebar {{
+ width: 240px; min-width: 240px;
+ background: #181825;
+ border-right: 1px solid #313244;
+ display: flex; flex-direction: column;
+ }}
+ #sidebar-header {{
+ padding: 16px 14px 12px;
+ font-size: 0.8rem; font-weight: 700;
+ text-transform: uppercase; letter-spacing: 0.08em;
+ color: #a6adc8; border-bottom: 1px solid #313244;
+ }}
+ #sidebar-header span {{
+ font-size: 1rem; text-transform: none; letter-spacing: normal;
+ color: #cdd6f4; display: block; margin-top: 2px;
+ }}
+ #example-list {{
+ flex: 1; overflow-y: auto; padding: 8px 0;
+ }}
+ .example-btn {{
+ display: block; width: 100%;
+ padding: 8px 14px; border: none;
+ background: transparent; color: #bac2de;
+ font-size: 0.85rem; text-align: left;
+ cursor: pointer;
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
+ }}
+ .example-btn:hover {{ background: #313244; color: #cdd6f4; }}
+ .example-btn.active {{ background: #45475a; color: #f5e0dc; font-weight: 600; }}
+
+ #main {{ flex: 1; display: flex; flex-direction: column; min-width: 0; }}
+ #toolbar {{
+ height: 40px; display: flex; align-items: center;
+ padding: 0 16px; gap: 12px;
+ background: #1e1e2e; border-bottom: 1px solid #313244;
+ font-size: 0.8rem; color: #a6adc8;
+ }}
+ #current-name {{
+ font-weight: 600; color: #cdd6f4;
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
+ }}
+ #open-tab {{
+ margin-left: auto; padding: 4px 10px;
+ border: 1px solid #585b70; border-radius: 4px;
+ background: transparent; color: #a6adc8;
+ font-size: 0.75rem; cursor: pointer;
+ text-decoration: none;
+ }}
+ #open-tab:hover {{ background: #313244; color: #cdd6f4; }}
+ #viewer {{ flex: 1; border: none; width: 100%; background: #11111b; }}
+ </style>
+</head>
+<body>
+ <div id="app">
+ <div id="sidebar">
+ <div id="sidebar-header">
+ GPUI Examples
+ <span>{count} available</span>
+ </div>
+ <div id="example-list">
+{buttons} </div>
+ </div>
+ <div id="main">
+ <div id="toolbar">
+ <span id="current-name">{first}</span>
+ <a id="open-tab" href="./{first}/" target="_blank">Open in new tab ↗</a>
+ </div>
+ <iframe id="viewer" src="./{first}/"></iframe>
+ </div>
+ </div>
+ <script>
+ const buttons = document.querySelectorAll('.example-btn');
+ const viewer = document.getElementById('viewer');
+ const nameEl = document.getElementById('current-name');
+ const openEl = document.getElementById('open-tab');
+
+ function select(name) {{
+ buttons.forEach(b => b.classList.toggle('active', b.dataset.name === name));
+ viewer.src = './' + name + '/';
+ nameEl.textContent = name;
+ openEl.href = './' + name + '/';
+ history.replaceState(null, '', '#' + name);
+ }}
+
+ buttons.forEach(b => b.addEventListener('click', () => select(b.dataset.name)));
+
+ const hash = location.hash.slice(1);
+ if (hash && [...buttons].some(b => b.dataset.name === hash)) {{
+ select(hash);
+ }} else {{
+ select('{first}');
+ }}
+ </script>
+</body>
+</html>
+"##,
+ count = examples.len(),
+ )
+}
@@ -50,6 +50,7 @@ pub(crate) fn run_tests() -> Workflow {
should_run_tests.guard(run_platform_tests(Platform::Mac)),
should_run_tests.guard(doctests()),
should_run_tests.guard(check_workspace_binaries()),
+ should_run_tests.guard(check_wasm()),
should_run_tests.guard(check_dependencies()), // could be more specific here?
should_check_docs.guard(check_docs()),
should_check_licences.guard(check_licenses()),
@@ -335,6 +336,38 @@ fn check_dependencies() -> NamedJob {
)
}
+fn check_wasm() -> NamedJob {
+ fn install_nightly_wasm_toolchain() -> Step<Run> {
+ named::bash(
+ "rustup toolchain install nightly --component rust-src --target wasm32-unknown-unknown",
+ )
+ }
+
+ fn cargo_check_wasm() -> Step<Run> {
+ named::bash(concat!(
+ "cargo +nightly -Zbuild-std=std,panic_abort ",
+ "check --target wasm32-unknown-unknown -p gpui_platform",
+ ))
+ .add_env((
+ "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS",
+ "-C target-feature=+atomics,+bulk-memory,+mutable-globals",
+ ))
+ }
+
+ named::job(
+ release_job(&[])
+ .runs_on(runners::LINUX_LARGE)
+ .add_step(steps::checkout_repo())
+ .add_step(steps::setup_cargo_config(Platform::Linux))
+ .add_step(steps::cache_rust_dependencies_namespace())
+ .add_step(install_nightly_wasm_toolchain())
+ .add_step(steps::setup_sccache(Platform::Linux))
+ .add_step(cargo_check_wasm())
+ .add_step(steps::show_sccache_stats(Platform::Linux))
+ .add_step(steps::cleanup_cargo_config(Platform::Linux)),
+ )
+}
+
fn check_workspace_binaries() -> NamedJob {
named::job(
release_job(&[])