GPUI on the web (#50228)

Implements a basic web platform for the wasm32-unknown-unknown target
for gpui

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: John Tur <john-tur@outlook.com>
This commit is contained in:
Lukas Wirth 2026-02-26 18:36:50 +01:00 committed by GitHub
parent e8325318a0
commit 14f37ed502
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
117 changed files with 5304 additions and 1132 deletions

View file

@ -466,6 +466,45 @@ jobs:
run: |
rm -rf ./../.cargo
timeout-minutes: 60
check_wasm:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: namespace-profile-8x16-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
path: ~/.rustup
- name: run_tests::check_wasm::install_nightly_wasm_toolchain
run: rustup toolchain install nightly --component rust-src --target wasm32-unknown-unknown
- name: steps::setup_sccache
run: ./script/setup-sccache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
SCCACHE_BUCKET: sccache-zed
- name: run_tests::check_wasm::cargo_check_wasm
run: cargo +nightly -Zbuild-std=std,panic_abort check --target wasm32-unknown-unknown -p gpui_platform
env:
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: -C target-feature=+atomics,+bulk-memory,+mutable-globals
- name: steps::show_sccache_stats
run: sccache --show-stats || true
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
timeout-minutes: 60
check_dependencies:
needs:
- orchestrate
@ -641,6 +680,7 @@ jobs:
- run_tests_mac
- doctests
- check_workspace_binaries
- check_wasm
- check_dependencies
- check_docs
- check_licenses
@ -668,6 +708,7 @@ jobs:
check_result "run_tests_mac" "${{ needs.run_tests_mac.result }}"
check_result "doctests" "${{ needs.doctests.result }}"
check_result "check_workspace_binaries" "${{ needs.check_workspace_binaries.result }}"
check_result "check_wasm" "${{ needs.check_wasm.result }}"
check_result "check_dependencies" "${{ needs.check_dependencies.result }}"
check_result "check_docs" "${{ needs.check_docs.result }}"
check_result "check_licenses" "${{ needs.check_licenses.result }}"

111
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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");

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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};

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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,

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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::*;

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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,

View file

@ -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();
})
}

View file

@ -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();
})
}

View file

@ -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(

View file

@ -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.

View file

@ -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();

View file

@ -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,

View file

@ -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;

View file

@ -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.

View file

@ -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 {

View file

@ -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;

View file

@ -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)
}

View file

@ -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::*;

View file

@ -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>,

View file

@ -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

View file

@ -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.

View file

@ -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>>>,

View file

@ -2,7 +2,10 @@ use crate::{PlatformDispatcher, RunnableMeta};
use async_task::Runnable;
use chrono::{DateTime, Utc};
use futures::channel::oneshot;
use scheduler::Instant;
use scheduler::{Clock, Priority, Scheduler, SessionId, TestScheduler, Timer};
#[cfg(not(target_family = "wasm"))]
use std::task::{Context, Poll};
use std::{
future::Future,
pin::Pin,
@ -10,10 +13,8 @@ use std::{
Arc,
atomic::{AtomicU16, Ordering},
},
task::{Context, Poll},
time::{Duration, Instant},
time::Duration,
};
use waker_fn::waker_fn;
/// A production implementation of [`Scheduler`] that wraps a [`PlatformDispatcher`].
///
@ -43,37 +44,48 @@ impl Scheduler for PlatformScheduler {
fn block(
&self,
_session_id: Option<SessionId>,
mut future: Pin<&mut dyn Future<Output = ()>>,
timeout: Option<Duration>,
#[cfg_attr(target_family = "wasm", allow(unused_mut))] mut future: Pin<
&mut dyn Future<Output = ()>,
>,
#[cfg_attr(target_family = "wasm", allow(unused_variables))] timeout: Option<Duration>,
) -> bool {
let deadline = timeout.map(|t| Instant::now() + t);
let parker = parking::Parker::new();
let unparker = parker.unparker();
let waker = waker_fn(move || {
unparker.unpark();
});
let mut cx = Context::from_waker(&waker);
if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
return true;
#[cfg(target_family = "wasm")]
{
let _ = (&future, &timeout);
panic!("Cannot block on wasm")
}
let park_deadline = |deadline: Instant| {
// Timer expirations are only delivered every ~15.6 milliseconds by default on Windows.
// We increase the resolution during this wait so that short timeouts stay reasonably short.
let _timer_guard = self.dispatcher.increase_timer_resolution();
parker.park_deadline(deadline)
};
loop {
match deadline {
Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => {
return false;
}
Some(_) => (),
None => parker.park(),
}
#[cfg(not(target_family = "wasm"))]
{
use waker_fn::waker_fn;
let deadline = timeout.map(|t| Instant::now() + t);
let parker = parking::Parker::new();
let unparker = parker.unparker();
let waker = waker_fn(move || {
unparker.unpark();
});
let mut cx = Context::from_waker(&waker);
if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
break true;
return true;
}
let park_deadline = |deadline: Instant| {
// Timer expirations are only delivered every ~15.6 milliseconds by default on Windows.
// We increase the resolution during this wait so that short timeouts stay reasonably short.
let _timer_guard = self.dispatcher.increase_timer_resolution();
parker.park_deadline(deadline)
};
loop {
match deadline {
Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => {
return false;
}
Some(_) => (),
None => parker.park(),
}
if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
break true;
}
}
}
}

View file

@ -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};

View file

@ -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

View file

@ -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>,

View file

@ -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`,

View file

@ -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>>>,

View file

@ -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);

View file

@ -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.

View file

@ -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;

View file

@ -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 }

View file

@ -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());

View file

@ -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;

View file

@ -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"

View file

@ -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"))]

View file

@ -0,0 +1,12 @@
[package]
name = "gpui_util"
version = "0.1.0"
publish.workspace = true
edition.workspace = true
[dependencies]
log.workspace = true
anyhow.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

292
crates/gpui_util/src/lib.rs Normal file
View file

@ -0,0 +1,292 @@
// FluentBuilder
// pub use gpui_util::{FutureExt, Timeout, arc_cow::ArcCow};
use std::{
env,
ops::AddAssign,
panic::Location,
pin::Pin,
sync::OnceLock,
task::{Context, Poll},
time::Instant,
};
pub mod arc_cow;
pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
let prev = *value;
*value += T::from(1);
prev
}
pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
static ZED_MEASUREMENTS: OnceLock<bool> = OnceLock::new();
let zed_measurements = ZED_MEASUREMENTS.get_or_init(|| {
env::var("ZED_MEASUREMENTS")
.map(|measurements| measurements == "1" || measurements == "true")
.unwrap_or(false)
});
if *zed_measurements {
let start = Instant::now();
let result = f();
let elapsed = start.elapsed();
eprintln!("{}: {:?}", label, elapsed);
result
} else {
f()
}
}
#[macro_export]
macro_rules! debug_panic {
( $($fmt_arg:tt)* ) => {
if cfg!(debug_assertions) {
panic!( $($fmt_arg)* );
} else {
let backtrace = std::backtrace::Backtrace::capture();
log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace);
}
};
}
#[track_caller]
pub fn some_or_debug_panic<T>(option: Option<T>) -> Option<T> {
#[cfg(debug_assertions)]
if option.is_none() {
panic!("Unexpected None");
}
option
}
/// Expands to an immediately-invoked function expression. Good for using the ? operator
/// in functions which do not return an Option or Result.
///
/// Accepts a normal block, an async block, or an async move block.
#[macro_export]
macro_rules! maybe {
($block:block) => {
(|| $block)()
};
(async $block:block) => {
(async || $block)()
};
(async move $block:block) => {
(async move || $block)()
};
}
pub trait ResultExt<E> {
type Ok;
fn log_err(self) -> Option<Self::Ok>;
/// Assert that this result should never be an error in development or tests.
fn debug_assert_ok(self, reason: &str) -> Self;
fn warn_on_err(self) -> Option<Self::Ok>;
fn log_with_level(self, level: log::Level) -> Option<Self::Ok>;
fn anyhow(self) -> anyhow::Result<Self::Ok>
where
E: Into<anyhow::Error>;
}
impl<T, E> ResultExt<E> for Result<T, E>
where
E: std::fmt::Debug,
{
type Ok = T;
#[track_caller]
fn log_err(self) -> Option<T> {
self.log_with_level(log::Level::Error)
}
#[track_caller]
fn debug_assert_ok(self, reason: &str) -> Self {
if let Err(error) = &self {
debug_panic!("{reason} - {error:?}");
}
self
}
#[track_caller]
fn warn_on_err(self) -> Option<T> {
self.log_with_level(log::Level::Warn)
}
#[track_caller]
fn log_with_level(self, level: log::Level) -> Option<T> {
match self {
Ok(value) => Some(value),
Err(error) => {
log_error_with_caller(*Location::caller(), error, level);
None
}
}
}
fn anyhow(self) -> anyhow::Result<T>
where
E: Into<anyhow::Error>,
{
self.map_err(Into::into)
}
}
fn log_error_with_caller<E>(caller: core::panic::Location<'_>, error: E, level: log::Level)
where
E: std::fmt::Debug,
{
#[cfg(not(windows))]
let file = caller.file();
#[cfg(windows)]
let file = caller.file().replace('\\', "/");
// In this codebase all crates reside in a `crates` directory,
// so discard the prefix up to that segment to find the crate name
let file = file.split_once("crates/");
let target = file.as_ref().and_then(|(_, s)| s.split_once("/src/"));
let module_path = target.map(|(krate, module)| {
if module.starts_with(krate) {
module.trim_end_matches(".rs").replace('/', "::")
} else {
krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::")
}
});
let file = file.map(|(_, file)| format!("crates/{file}"));
log::logger().log(
&log::Record::builder()
.target(module_path.as_deref().unwrap_or(""))
.module_path(file.as_deref())
.args(format_args!("{:?}", error))
.file(Some(caller.file()))
.line(Some(caller.line()))
.level(level)
.build(),
);
}
pub fn log_err<E: std::fmt::Debug>(error: &E) {
log_error_with_caller(*Location::caller(), error, log::Level::Error);
}
pub trait TryFutureExt {
fn log_err(self) -> LogErrorFuture<Self>
where
Self: Sized;
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
where
Self: Sized;
fn warn_on_err(self) -> LogErrorFuture<Self>
where
Self: Sized;
fn unwrap(self) -> UnwrapFuture<Self>
where
Self: Sized;
}
impl<F, T, E> TryFutureExt for F
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
#[track_caller]
fn log_err(self) -> LogErrorFuture<Self>
where
Self: Sized,
{
let location = Location::caller();
LogErrorFuture(self, log::Level::Error, *location)
}
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
where
Self: Sized,
{
LogErrorFuture(self, log::Level::Error, location)
}
#[track_caller]
fn warn_on_err(self) -> LogErrorFuture<Self>
where
Self: Sized,
{
let location = Location::caller();
LogErrorFuture(self, log::Level::Warn, *location)
}
fn unwrap(self) -> UnwrapFuture<Self>
where
Self: Sized,
{
UnwrapFuture(self)
}
}
#[must_use]
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
impl<F, T, E> Future for LogErrorFuture<F>
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
type Output = Option<T>;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let level = self.1;
let location = self.2;
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
match inner.poll(cx) {
Poll::Ready(output) => Poll::Ready(match output {
Ok(output) => Some(output),
Err(error) => {
log_error_with_caller(location, error, level);
None
}
}),
Poll::Pending => Poll::Pending,
}
}
}
pub struct UnwrapFuture<F>(F);
impl<F, T, E> Future for UnwrapFuture<F>
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
match inner.poll(cx) {
Poll::Ready(result) => Poll::Ready(result.unwrap()),
Poll::Pending => Poll::Pending,
}
}
}
pub struct Deferred<F: FnOnce()>(Option<F>);
impl<F: FnOnce()> Deferred<F> {
/// Drop without running the deferred function.
pub fn abort(mut self) {
self.0.take();
}
}
impl<F: FnOnce()> Drop for Deferred<F> {
fn drop(&mut self) {
if let Some(f) = self.0.take() {
f()
}
}
}
/// Run the given function when the returned value is dropped (unless it's cancelled).
#[must_use]
pub fn defer<F: FnOnce()>(f: F) -> Deferred<F> {
Deferred(Some(f))
}

View file

@ -0,0 +1,61 @@
[package]
name = "gpui_web"
version = "0.1.0"
publish.workspace = true
edition.workspace = true
license = "Apache-2.0"
autoexamples = false
[lints]
workspace = true
[lib]
path = "src/gpui_web.rs"
[target.'cfg(target_family = "wasm")'.dependencies]
gpui.workspace = true
parking_lot = { workspace = true, features = ["nightly"] }
gpui_wgpu.workspace = true
anyhow.workspace = true
futures.workspace = true
log.workspace = true
smallvec.workspace = true
uuid.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures = "0.4"
web-time.workspace = true
console_error_panic_hook = "0.1.7"
js-sys = "0.3"
raw-window-handle = "0.6"
wasm_thread = { version = "0.3", features = ["es_modules"] }
web-sys = { version = "0.3", features = [
"console",
"CssStyleDeclaration",
"DataTransfer",
"Document",
"DomRect",
"DragEvent",
"Element",
"EventTarget",
"File",
"FileList",
"HtmlCanvasElement",
"HtmlElement",
"HtmlInputElement",
"KeyboardEvent",
"MediaQueryList",
"MediaQueryListEvent",
"MouseEvent",
"Navigator",
"PointerEvent",
"ResizeObserver",
"ResizeObserverBoxOptions",
"ResizeObserverEntry",
"ResizeObserverSize",
"ResizeObserverOptions",
"Screen",
"Storage",
"VisualViewport",
"WheelEvent",
"Window",
] }

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

View file

@ -0,0 +1,14 @@
[target.wasm32-unknown-unknown]
rustflags = [
"-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
"-C", "link-arg=--shared-memory",
"-C", "link-arg=--max-memory=1073741824",
"-C", "link-arg=--import-memory",
"-C", "link-arg=--export=__wasm_init_tls",
"-C", "link-arg=--export=__tls_size",
"-C", "link-arg=--export=__tls_align",
"-C", "link-arg=--export=__tls_base",
]
[unstable]
build-std = ["std,panic_abort"]

View file

@ -0,0 +1,3 @@
/dist
/target
Cargo.lock

View file

@ -0,0 +1,16 @@
[workspace]
[package]
name = "hello_web"
version = "0.1.0"
edition = "2024"
publish = false
[[bin]]
name = "hello_web"
path = "main.rs"
[dependencies]
gpui = { path = "../../../gpui" }
gpui_platform = { path = "../../../gpui_platform" }
web-time = "1"

View file

@ -0,0 +1 @@
../../../../LICENSE-APACHE

View file

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=0" />
<title>GPUI Web: hello_web</title>
<link data-trunk rel="rust" data-bin="hello_web" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
}
canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
outline: none;
-webkit-user-select: none;
user-select: none;
}
</style>
</head>
<body></body>
</html>

View file

@ -0,0 +1,422 @@
use gpui::prelude::*;
use gpui::{
App, Bounds, Context, ElementId, SharedString, Task, Window, WindowBounds, WindowOptions, div,
px, rgb, size,
};
// ---------------------------------------------------------------------------
// Prime counting (intentionally brute-force so it hammers the CPU)
// ---------------------------------------------------------------------------
fn is_prime(n: u64) -> bool {
if n < 2 {
return false;
}
if n < 4 {
return true;
}
if n % 2 == 0 || n % 3 == 0 {
return false;
}
let mut i = 5;
while i * i <= n {
if n % i == 0 || n % (i + 2) == 0 {
return false;
}
i += 6;
}
true
}
fn count_primes_in_range(start: u64, end: u64) -> u64 {
let mut count = 0;
for n in start..end {
if is_prime(n) {
count += 1;
}
}
count
}
// ---------------------------------------------------------------------------
// App state
// ---------------------------------------------------------------------------
const NUM_CHUNKS: u64 = 12;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Preset {
TenMillion,
FiftyMillion,
HundredMillion,
}
impl Preset {
fn label(self) -> &'static str {
match self {
Preset::TenMillion => "10 M",
Preset::FiftyMillion => "50 M",
Preset::HundredMillion => "100 M",
}
}
fn value(self) -> u64 {
match self {
Preset::TenMillion => 10_000_000,
Preset::FiftyMillion => 50_000_000,
Preset::HundredMillion => 100_000_000,
}
}
const ALL: [Preset; 3] = [
Preset::TenMillion,
Preset::FiftyMillion,
Preset::HundredMillion,
];
}
struct ChunkResult {
count: u64,
}
struct Run {
limit: u64,
chunks_done: u64,
chunk_results: Vec<ChunkResult>,
total: Option<u64>,
elapsed: Option<f64>,
}
struct HelloWeb {
selected_preset: Preset,
current_run: Option<Run>,
history: Vec<SharedString>,
_tasks: Vec<Task<()>>,
}
impl HelloWeb {
fn new(_cx: &mut Context<Self>) -> Self {
Self {
selected_preset: Preset::TenMillion,
current_run: None,
history: Vec::new(),
_tasks: Vec::new(),
}
}
fn start_search(&mut self, cx: &mut Context<Self>) {
let limit = self.selected_preset.value();
let chunk_size = limit / NUM_CHUNKS;
self.current_run = Some(Run {
limit,
chunks_done: 0,
chunk_results: Vec::new(),
total: None,
elapsed: None,
});
self._tasks.clear();
cx.notify();
let start_time = web_time::Instant::now();
for i in 0..NUM_CHUNKS {
let range_start = i * chunk_size;
let range_end = if i == NUM_CHUNKS - 1 {
limit
} else {
range_start + chunk_size
};
let task = cx.spawn(async move |this, cx| {
let count = cx
.background_spawn(async move { count_primes_in_range(range_start, range_end) })
.await;
this.update(cx, |this, cx| {
if let Some(run) = &mut this.current_run {
run.chunk_results.push(ChunkResult { count });
run.chunks_done += 1;
if run.chunks_done == NUM_CHUNKS {
let total: u64 = run.chunk_results.iter().map(|r| r.count).sum();
let elapsed_ms = start_time.elapsed().as_secs_f64() * 1000.0;
run.total = Some(total);
run.elapsed = Some(elapsed_ms);
this.history.push(
format!(
"π({}) = {} ({:.0} ms, {} chunks)",
format_number(run.limit),
format_number(total),
elapsed_ms,
NUM_CHUNKS,
)
.into(),
);
}
cx.notify();
}
})
.ok();
});
self._tasks.push(task);
}
}
}
fn format_number(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
for (i, ch) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(ch);
}
result.chars().rev().collect()
}
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
const BG_BASE: u32 = 0x1e1e2e;
const BG_SURFACE: u32 = 0x313244;
const BG_OVERLAY: u32 = 0x45475a;
const TEXT_PRIMARY: u32 = 0xcdd6f4;
const TEXT_SECONDARY: u32 = 0xa6adc8;
const TEXT_DIM: u32 = 0x6c7086;
const ACCENT_YELLOW: u32 = 0xf9e2af;
const ACCENT_GREEN: u32 = 0xa6e3a1;
const ACCENT_BLUE: u32 = 0x89b4fa;
const ACCENT_MAUVE: u32 = 0xcba6f7;
impl Render for HelloWeb {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_running = self.current_run.as_ref().is_some_and(|r| r.total.is_none());
// -- Preset buttons --
let preset_row = Preset::ALL.iter().enumerate().fold(
div().flex().flex_row().gap_2(),
|row, (index, &preset)| {
let is_selected = preset == self.selected_preset;
let (bg, text_color) = if is_selected {
(ACCENT_BLUE, BG_BASE)
} else {
(BG_OVERLAY, TEXT_SECONDARY)
};
row.child(
div()
.id(ElementId::NamedInteger("preset".into(), index as u64))
.px_3()
.py_1()
.rounded_md()
.bg(rgb(bg))
.text_color(rgb(text_color))
.text_sm()
.cursor_pointer()
.when(!is_running, |this| {
this.on_click(cx.listener(move |this, _event, _window, _cx| {
this.selected_preset = preset;
}))
})
.child(preset.label()),
)
},
);
// -- Go button --
let (go_bg, go_text, go_label) = if is_running {
(BG_OVERLAY, TEXT_DIM, "Running…")
} else {
(ACCENT_GREEN, BG_BASE, "Count Primes")
};
let go_button = div()
.id("go")
.px_4()
.py(px(6.))
.rounded_md()
.bg(rgb(go_bg))
.text_color(rgb(go_text))
.cursor_pointer()
.when(!is_running, |this| {
this.on_click(cx.listener(|this, _event, _window, cx| {
this.start_search(cx);
}))
})
.child(go_label);
// -- Progress / result area --
let status_area = if let Some(run) = &self.current_run {
let progress_fraction = run.chunks_done as f32 / NUM_CHUNKS as f32;
let progress_pct = (progress_fraction * 100.0) as u32;
let status_text: SharedString = if let Some(total) = run.total {
format!(
"Found {} primes below {} in {:.0} ms",
format_number(total),
format_number(run.limit),
run.elapsed.unwrap_or(0.0),
)
.into()
} else {
format!(
"Searching up to {} … {}/{} chunks ({}%)",
format_number(run.limit),
run.chunks_done,
NUM_CHUNKS,
progress_pct,
)
.into()
};
let bar_color = if run.total.is_some() {
ACCENT_GREEN
} else {
ACCENT_BLUE
};
let chunk_dots =
(0..NUM_CHUNKS as usize).fold(div().flex().flex_row().gap_1().mt_2(), |row, i| {
let done = i < run.chunks_done as usize;
let color = if done { ACCENT_MAUVE } else { BG_OVERLAY };
row.child(div().size(px(10.)).rounded_sm().bg(rgb(color)))
});
div()
.flex()
.flex_col()
.w_full()
.gap_2()
.child(div().text_color(rgb(TEXT_PRIMARY)).child(status_text))
.child(
div()
.w_full()
.h(px(8.))
.rounded_sm()
.bg(rgb(BG_OVERLAY))
.child(
div()
.h_full()
.rounded_sm()
.bg(rgb(bar_color))
.w(gpui::relative(progress_fraction)),
),
)
.child(chunk_dots)
} else {
div().flex().flex_col().w_full().child(
div()
.text_color(rgb(TEXT_DIM))
.child("Select a range and press Count Primes to begin."),
)
};
// -- History log --
let history_section = if self.history.is_empty() {
div()
} else {
self.history
.iter()
.rev()
.fold(div().flex().flex_col().gap_1(), |col, entry| {
col.child(
div()
.text_sm()
.text_color(rgb(TEXT_SECONDARY))
.child(entry.clone()),
)
})
};
// -- Layout --
div()
.flex()
.flex_col()
.size_full()
.bg(rgb(BG_BASE))
.justify_center()
.items_center()
.gap_4()
.p_4()
// Title
.child(
div()
.text_xl()
.text_color(rgb(TEXT_PRIMARY))
.child("Prime Sieve — GPUI Web"),
)
.child(div().text_sm().text_color(rgb(TEXT_DIM)).child(format!(
"Background threads: {} · Chunks per run: {}",
std::thread::available_parallelism().map_or(2, |n| n.get().max(2)),
NUM_CHUNKS,
)))
// Controls
.child(
div()
.flex()
.flex_col()
.items_center()
.gap_3()
.p_4()
.w(px(500.))
.rounded_lg()
.bg(rgb(BG_SURFACE))
.child(
div()
.text_sm()
.text_color(rgb(ACCENT_YELLOW))
.child("Count primes below:"),
)
.child(preset_row)
.child(go_button),
)
// Status
.child(
div()
.flex()
.flex_col()
.w(px(500.))
.p_4()
.rounded_lg()
.bg(rgb(BG_SURFACE))
.child(status_area),
)
// History
.when(!self.history.is_empty(), |this| {
this.child(
div()
.flex()
.flex_col()
.w(px(500.))
.p_4()
.rounded_lg()
.bg(rgb(BG_SURFACE))
.gap_2()
.child(div().text_sm().text_color(rgb(TEXT_DIM)).child("History"))
.child(history_section),
)
})
}
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
fn main() {
gpui_platform::web_init();
gpui_platform::application().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(640.), px(560.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(HelloWeb::new),
)
.expect("failed to open window");
cx.activate(true);
});
}

View file

@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
targets = ["wasm32-unknown-unknown"]
components = ["rust-src", "rustfmt", "clippy"]

View file

@ -0,0 +1,7 @@
[serve]
addresses = ["127.0.0.1"]
port = 8080
open = true
# Headers required for WebGPU / SharedArrayBuffer support.
headers = { "Cross-Origin-Embedder-Policy" = "require-corp", "Cross-Origin-Opener-Policy" = "same-origin" }

View file

@ -0,0 +1,333 @@
use gpui::{
PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant,
ThreadTaskTimings,
};
use std::sync::Arc;
use std::sync::atomic::AtomicI32;
use std::time::Duration;
use wasm_bindgen::prelude::*;
use web_time::Instant;
const MIN_BACKGROUND_THREADS: usize = 2;
fn shared_memory_supported() -> bool {
let global = js_sys::global();
let has_shared_array_buffer =
js_sys::Reflect::has(&global, &JsValue::from_str("SharedArrayBuffer")).unwrap_or(false);
let has_atomics = js_sys::Reflect::has(&global, &JsValue::from_str("Atomics")).unwrap_or(false);
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
let buffer = memory.buffer();
let is_shared_buffer = buffer.is_instance_of::<js_sys::SharedArrayBuffer>();
has_shared_array_buffer && has_atomics && is_shared_buffer
}
enum MainThreadItem {
Runnable(RunnableVariant),
Delayed {
runnable: RunnableVariant,
millis: i32,
},
// TODO-Wasm: Shouldn't these run on their own dedicated thread?
RealtimeFunction(Box<dyn FnOnce() + Send>),
}
struct MainThreadMailbox {
sender: PriorityQueueSender<MainThreadItem>,
receiver: parking_lot::Mutex<PriorityQueueReceiver<MainThreadItem>>,
signal: AtomicI32,
}
impl MainThreadMailbox {
fn new() -> Self {
let (sender, receiver) = PriorityQueueReceiver::new();
Self {
sender,
receiver: parking_lot::Mutex::new(receiver),
signal: AtomicI32::new(0),
}
}
fn post(&self, priority: Priority, item: MainThreadItem) {
if self.sender.spin_send(priority, item).is_err() {
log::error!("MainThreadMailbox::send failed: receiver disconnected");
}
// TODO-Wasm: Verify this lock-free protocol
let view = self.signal_view();
js_sys::Atomics::store(&view, 0, 1).ok();
js_sys::Atomics::notify(&view, 0).ok();
}
fn drain(&self, window: &web_sys::Window) {
let mut receiver = self.receiver.lock();
loop {
// We need these `spin` variants because we can't acquire a lock on the main thread.
// TODO-WASM: Should we do something different?
match receiver.spin_try_pop() {
Ok(Some(item)) => execute_on_main_thread(window, item),
Ok(None) => break,
Err(_) => break,
}
}
}
fn signal_view(&self) -> js_sys::Int32Array {
let byte_offset = self.signal.as_ptr() as u32;
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
js_sys::Int32Array::new_with_byte_offset_and_length(&memory.buffer(), byte_offset, 1)
}
fn run_waker_loop(self: &Arc<Self>, window: web_sys::Window) {
if !shared_memory_supported() {
log::warn!("SharedArrayBuffer not available; main thread mailbox waker loop disabled");
return;
}
let mailbox = Arc::clone(self);
wasm_bindgen_futures::spawn_local(async move {
let view = mailbox.signal_view();
loop {
js_sys::Atomics::store(&view, 0, 0).expect("Atomics.store failed");
let result = match js_sys::Atomics::wait_async(&view, 0, 0) {
Ok(result) => result,
Err(error) => {
log::error!("Atomics.waitAsync failed: {error:?}");
break;
}
};
let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !is_async {
log::error!("Atomics.waitAsync returned synchronously; waker loop exiting");
break;
}
let promise: js_sys::Promise =
js_sys::Reflect::get(&result, &JsValue::from_str("value"))
.expect("waitAsync result missing 'value'")
.unchecked_into();
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
mailbox.drain(&window);
}
});
}
}
pub struct WebDispatcher {
main_thread_id: std::thread::ThreadId,
browser_window: web_sys::Window,
background_sender: PriorityQueueSender<RunnableVariant>,
main_thread_mailbox: Arc<MainThreadMailbox>,
supports_threads: bool,
_background_threads: Vec<wasm_thread::JoinHandle<()>>,
}
// Safety: `web_sys::Window` is only accessed from the main thread
// All other fields are `Send + Sync` by construction.
unsafe impl Send for WebDispatcher {}
unsafe impl Sync for WebDispatcher {}
impl WebDispatcher {
pub fn new(browser_window: web_sys::Window) -> Self {
let (background_sender, background_receiver) = PriorityQueueReceiver::new();
let main_thread_mailbox = Arc::new(MainThreadMailbox::new());
let supports_threads = shared_memory_supported();
if supports_threads {
main_thread_mailbox.run_waker_loop(browser_window.clone());
} else {
log::warn!(
"SharedArrayBuffer not available; falling back to single-threaded dispatcher"
);
}
let background_threads = if supports_threads {
let thread_count = browser_window
.navigator()
.hardware_concurrency()
.max(MIN_BACKGROUND_THREADS as f64) as usize;
// TODO-Wasm: Is it bad to have web workers blocking for a long time like this?
(0..thread_count)
.map(|i| {
let mut receiver = background_receiver.clone();
wasm_thread::Builder::new()
.name(format!("background-worker-{i}"))
.spawn(move || {
loop {
let runnable: RunnableVariant = match receiver.pop() {
Ok(runnable) => runnable,
Err(_) => {
log::info!(
"background-worker-{i}: channel disconnected, exiting"
);
break;
}
};
if runnable.metadata().is_closed() {
continue;
}
runnable.run();
}
})
.expect("failed to spawn background worker thread")
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
Self {
main_thread_id: std::thread::current().id(),
browser_window,
background_sender,
main_thread_mailbox,
supports_threads,
_background_threads: background_threads,
}
}
fn on_main_thread(&self) -> bool {
std::thread::current().id() == self.main_thread_id
}
}
impl PlatformDispatcher for WebDispatcher {
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
// TODO-Wasm: should we panic here?
Vec::new()
}
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
ThreadTaskTimings {
thread_name: None,
thread_id: std::thread::current().id(),
timings: Vec::new(),
total_pushed: 0,
}
}
fn is_main_thread(&self) -> bool {
self.on_main_thread()
}
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
if !self.supports_threads {
self.dispatch_on_main_thread(runnable, priority);
return;
}
let result = if self.on_main_thread() {
self.background_sender.spin_send(priority, runnable)
} else {
self.background_sender.send(priority, runnable)
};
if let Err(error) = result {
log::error!("dispatch: failed to send to background queue: {error:?}");
}
}
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
if self.on_main_thread() {
schedule_runnable(&self.browser_window, runnable, priority);
} else {
self.main_thread_mailbox
.post(priority, MainThreadItem::Runnable(runnable));
}
}
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
let millis = duration.as_millis().min(i32::MAX as u128) as i32;
if self.on_main_thread() {
let callback = Closure::once_into_js(move || {
if !runnable.metadata().is_closed() {
runnable.run();
}
});
self.browser_window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.unchecked_ref(),
millis,
)
.ok();
} else {
self.main_thread_mailbox
.post(Priority::High, MainThreadItem::Delayed { runnable, millis });
}
}
fn spawn_realtime(&self, function: Box<dyn FnOnce() + Send>) {
if self.on_main_thread() {
let callback = Closure::once_into_js(move || {
function();
});
self.browser_window
.queue_microtask(callback.unchecked_ref());
} else {
self.main_thread_mailbox
.post(Priority::High, MainThreadItem::RealtimeFunction(function));
}
}
fn now(&self) -> Instant {
Instant::now()
}
}
fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
match item {
MainThreadItem::Runnable(runnable) => {
if !runnable.metadata().is_closed() {
runnable.run();
}
}
MainThreadItem::Delayed { runnable, millis } => {
let callback = Closure::once_into_js(move || {
if !runnable.metadata().is_closed() {
runnable.run();
}
});
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.unchecked_ref(),
millis,
)
.ok();
}
MainThreadItem::RealtimeFunction(function) => {
function();
}
}
}
fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) {
let callback = Closure::once_into_js(move || {
if !runnable.metadata().is_closed() {
runnable.run();
}
});
let callback: &js_sys::Function = callback.unchecked_ref();
match priority {
Priority::RealtimeAudio => {
window.queue_microtask(callback);
}
_ => {
// TODO-Wasm: this ought to enqueue so we can dequeue with proper priority
window
.set_timeout_with_callback_and_timeout_and_arguments_0(callback, 0)
.ok();
}
}
}

View file

@ -0,0 +1,98 @@
use anyhow::Result;
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px};
#[derive(Debug)]
pub struct WebDisplay {
id: DisplayId,
uuid: uuid::Uuid,
browser_window: web_sys::Window,
}
// Safety: WASM is single-threaded — there is no concurrent access to `web_sys::Window`.
unsafe impl Send for WebDisplay {}
unsafe impl Sync for WebDisplay {}
impl WebDisplay {
pub fn new(browser_window: web_sys::Window) -> Self {
WebDisplay {
id: DisplayId::new(1),
uuid: uuid::Uuid::new_v4(),
browser_window,
}
}
fn screen_size(&self) -> Size<Pixels> {
let Some(screen) = self.browser_window.screen().ok() else {
return Size {
width: px(1920.),
height: px(1080.),
};
};
let width = screen.width().unwrap_or(1920) as f32;
let height = screen.height().unwrap_or(1080) as f32;
Size {
width: px(width),
height: px(height),
}
}
fn viewport_size(&self) -> Size<Pixels> {
let width = self
.browser_window
.inner_width()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(1920.0) as f32;
let height = self
.browser_window
.inner_height()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(1080.0) as f32;
Size {
width: px(width),
height: px(height),
}
}
}
impl PlatformDisplay for WebDisplay {
fn id(&self) -> DisplayId {
self.id
}
fn uuid(&self) -> Result<uuid::Uuid> {
Ok(self.uuid)
}
fn bounds(&self) -> Bounds<Pixels> {
let size = self.screen_size();
Bounds {
origin: Point::default(),
size,
}
}
fn visible_bounds(&self) -> Bounds<Pixels> {
let size = self.viewport_size();
Bounds {
origin: Point::default(),
size,
}
}
fn default_bounds(&self) -> Bounds<Pixels> {
let visible = self.visible_bounds();
let width = visible.size.width * 0.75;
let height = visible.size.height * 0.75;
let origin_x = (visible.size.width - width) / 2.0;
let origin_y = (visible.size.height - height) / 2.0;
Bounds {
origin: Point::new(origin_x, origin_y),
size: Size { width, height },
}
}
}

View file

@ -0,0 +1,615 @@
use std::rc::Rc;
use gpui::{
Capslock, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent,
TouchPhase, point, px,
};
use smallvec::smallvec;
use wasm_bindgen::prelude::*;
use crate::window::WebWindowInner;
pub struct WebEventListeners {
#[allow(dead_code)]
closures: Vec<Closure<dyn FnMut(JsValue)>>,
}
pub(crate) struct ClickState {
last_position: Point<Pixels>,
last_time: f64,
current_count: usize,
}
impl Default for ClickState {
fn default() -> Self {
Self {
last_position: Point::default(),
last_time: 0.0,
current_count: 0,
}
}
}
impl ClickState {
fn register_click(&mut self, position: Point<Pixels>, time: f64) -> usize {
let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2)
+ (f32::from(position.y) - f32::from(self.last_position.y)).powi(2))
.sqrt();
if (time - self.last_time) < 400.0 && distance < 5.0 {
self.current_count += 1;
} else {
self.current_count = 1;
}
self.last_position = position;
self.last_time = time;
self.current_count
}
}
impl WebWindowInner {
pub fn register_event_listeners(self: &Rc<Self>) -> WebEventListeners {
let mut closures = vec![
self.register_pointer_down(),
self.register_pointer_up(),
self.register_pointer_move(),
self.register_pointer_leave(),
self.register_wheel(),
self.register_context_menu(),
self.register_dragover(),
self.register_drop(),
self.register_dragleave(),
self.register_key_down(),
self.register_key_up(),
self.register_focus(),
self.register_blur(),
self.register_pointer_enter(),
self.register_pointer_leave_hover(),
];
closures.extend(self.register_visibility_change());
closures.extend(self.register_appearance_change());
WebEventListeners { closures }
}
fn listen(
self: &Rc<Self>,
event_name: &str,
handler: impl FnMut(JsValue) + 'static,
) -> Closure<dyn FnMut(JsValue)> {
let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
self.canvas
.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
.ok();
closure
}
/// Registers a listener with `{passive: false}` so that `preventDefault()` works.
/// Needed for events like `wheel` which are passive by default in modern browsers.
fn listen_non_passive(
self: &Rc<Self>,
event_name: &str,
handler: impl FnMut(JsValue) + 'static,
) -> Closure<dyn FnMut(JsValue)> {
let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
let canvas_js: &JsValue = self.canvas.as_ref();
let callback_js: &JsValue = closure.as_ref();
let options = js_sys::Object::new();
js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok();
if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) {
if let Ok(add_fn) = add_fn_val.dyn_into::<js_sys::Function>() {
add_fn
.call3(canvas_js, &event_name.into(), callback_js, &options)
.ok();
}
}
closure
}
fn dispatch_input(&self, input: PlatformInput) {
let mut borrowed = self.callbacks.borrow_mut();
if let Some(ref mut callback) = borrowed.input {
callback(input);
}
}
fn register_pointer_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("pointerdown", move |event: JsValue| {
let event: web_sys::PointerEvent = event.unchecked_into();
event.prevent_default();
this.canvas.focus().ok();
let button = dom_mouse_button_to_gpui(event.button());
let position = pointer_position_in_element(&event);
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
let time = js_sys::Date::now();
this.pressed_button.set(Some(button));
let click_count = this.click_state.borrow_mut().register_click(position, time);
{
let mut current_state = this.state.borrow_mut();
current_state.mouse_position = position;
current_state.modifiers = modifiers;
}
this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent {
button,
position,
modifiers,
click_count,
first_mouse: false,
}));
})
}
fn register_pointer_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("pointerup", move |event: JsValue| {
let event: web_sys::PointerEvent = event.unchecked_into();
event.prevent_default();
let button = dom_mouse_button_to_gpui(event.button());
let position = pointer_position_in_element(&event);
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
this.pressed_button.set(None);
let click_count = this.click_state.borrow().current_count;
{
let mut current_state = this.state.borrow_mut();
current_state.mouse_position = position;
current_state.modifiers = modifiers;
}
this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent {
button,
position,
modifiers,
click_count,
}));
})
}
fn register_pointer_move(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("pointermove", move |event: JsValue| {
let event: web_sys::PointerEvent = event.unchecked_into();
event.prevent_default();
let position = pointer_position_in_element(&event);
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
let current_pressed = this.pressed_button.get();
{
let mut current_state = this.state.borrow_mut();
current_state.mouse_position = position;
current_state.modifiers = modifiers;
}
this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent {
position,
pressed_button: current_pressed,
modifiers,
}));
})
}
fn register_pointer_leave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("pointerleave", move |event: JsValue| {
let event: web_sys::PointerEvent = event.unchecked_into();
let position = pointer_position_in_element(&event);
let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
let current_pressed = this.pressed_button.get();
{
let mut current_state = this.state.borrow_mut();
current_state.mouse_position = position;
current_state.modifiers = modifiers;
}
this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent {
position,
pressed_button: current_pressed,
modifiers,
}));
})
}
fn register_wheel(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen_non_passive("wheel", move |event: JsValue| {
let event: web_sys::WheelEvent = event.unchecked_into();
event.prevent_default();
let mouse_event: &web_sys::MouseEvent = event.as_ref();
let position = mouse_position_in_element(mouse_event);
let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac);
let delta_mode = event.delta_mode();
let delta = if delta_mode == 1 {
ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32))
} else {
ScrollDelta::Pixels(point(
px(-event.delta_x() as f32),
px(-event.delta_y() as f32),
))
};
{
let mut current_state = this.state.borrow_mut();
current_state.modifiers = modifiers;
}
this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent {
position,
delta,
modifiers,
touch_phase: TouchPhase::Moved,
}));
})
}
fn register_context_menu(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
self.listen("contextmenu", move |event: JsValue| {
let event: web_sys::Event = event.unchecked_into();
event.prevent_default();
})
}
fn register_dragover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("dragover", move |event: JsValue| {
let event: web_sys::DragEvent = event.unchecked_into();
event.prevent_default();
let mouse_event: &web_sys::MouseEvent = event.as_ref();
let position = mouse_position_in_element(mouse_event);
{
let mut current_state = this.state.borrow_mut();
current_state.mouse_position = position;
}
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position }));
})
}
fn register_drop(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("drop", move |event: JsValue| {
let event: web_sys::DragEvent = event.unchecked_into();
event.prevent_default();
let mouse_event: &web_sys::MouseEvent = event.as_ref();
let position = mouse_position_in_element(mouse_event);
{
let mut current_state = this.state.borrow_mut();
current_state.mouse_position = position;
}
let paths = extract_file_paths_from_drag(&event);
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered {
position,
paths: ExternalPaths(paths),
}));
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position }));
})
}
fn register_dragleave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("dragleave", move |_event: JsValue| {
this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited));
})
}
fn register_key_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("keydown", move |event: JsValue| {
let event: web_sys::KeyboardEvent = event.unchecked_into();
let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
let capslock = capslock_from_keyboard_event(&event);
{
let mut current_state = this.state.borrow_mut();
current_state.modifiers = modifiers;
current_state.capslock = capslock;
}
this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers,
capslock,
}));
let key = dom_key_to_gpui_key(&event);
if is_modifier_only_key(&key) {
return;
}
event.prevent_default();
let is_held = event.repeat();
let key_char = compute_key_char(&event, &key, &modifiers);
let keystroke = Keystroke {
modifiers,
key,
key_char,
};
this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held,
prefer_character_input: false,
}));
})
}
fn register_key_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("keyup", move |event: JsValue| {
let event: web_sys::KeyboardEvent = event.unchecked_into();
let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
let capslock = capslock_from_keyboard_event(&event);
{
let mut current_state = this.state.borrow_mut();
current_state.modifiers = modifiers;
current_state.capslock = capslock;
}
this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers,
capslock,
}));
let key = dom_key_to_gpui_key(&event);
if is_modifier_only_key(&key) {
return;
}
event.prevent_default();
let key_char = compute_key_char(&event, &key, &modifiers);
let keystroke = Keystroke {
modifiers,
key,
key_char,
};
this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke }));
})
}
fn register_focus(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("focus", move |_event: JsValue| {
{
let mut state = this.state.borrow_mut();
state.is_active = true;
}
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.active_status_change {
callback(true);
}
})
}
fn register_blur(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("blur", move |_event: JsValue| {
{
let mut state = this.state.borrow_mut();
state.is_active = false;
}
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.active_status_change {
callback(false);
}
})
}
fn register_pointer_enter(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("pointerenter", move |_event: JsValue| {
{
let mut state = this.state.borrow_mut();
state.is_hovered = true;
}
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.hover_status_change {
callback(true);
}
})
}
fn register_pointer_leave_hover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
let this = Rc::clone(self);
self.listen("pointerleave", move |_event: JsValue| {
{
let mut state = this.state.borrow_mut();
state.is_hovered = false;
}
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.hover_status_change {
callback(false);
}
})
}
}
fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String {
let key = event.key();
match key.as_str() {
"Enter" => "enter".to_string(),
"Backspace" => "backspace".to_string(),
"Tab" => "tab".to_string(),
"Escape" => "escape".to_string(),
"Delete" => "delete".to_string(),
" " => "space".to_string(),
"ArrowLeft" => "left".to_string(),
"ArrowRight" => "right".to_string(),
"ArrowUp" => "up".to_string(),
"ArrowDown" => "down".to_string(),
"Home" => "home".to_string(),
"End" => "end".to_string(),
"PageUp" => "pageup".to_string(),
"PageDown" => "pagedown".to_string(),
"Insert" => "insert".to_string(),
"Control" => "control".to_string(),
"Alt" => "alt".to_string(),
"Shift" => "shift".to_string(),
"Meta" => "platform".to_string(),
"CapsLock" => "capslock".to_string(),
other => {
if let Some(rest) = other.strip_prefix('F') {
if let Ok(number) = rest.parse::<u8>() {
if (1..=35).contains(&number) {
return format!("f{number}");
}
}
}
other.to_lowercase()
}
}
}
fn dom_mouse_button_to_gpui(button: i16) -> MouseButton {
match button {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
3 => MouseButton::Navigate(NavigationDirection::Back),
4 => MouseButton::Navigate(NavigationDirection::Forward),
_ => MouseButton::Left,
}
}
fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers {
Modifiers {
control: event.ctrl_key(),
alt: event.alt_key(),
shift: event.shift_key(),
platform: event.meta_key(),
function: false,
}
}
fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers {
let mouse_event: &web_sys::MouseEvent = event.as_ref();
Modifiers {
control: mouse_event.ctrl_key(),
alt: mouse_event.alt_key(),
shift: mouse_event.shift_key(),
platform: mouse_event.meta_key(),
function: false,
}
}
fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers {
Modifiers {
control: event.ctrl_key(),
alt: event.alt_key(),
shift: event.shift_key(),
platform: event.meta_key(),
function: false,
}
}
fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock {
Capslock {
on: event.get_modifier_state("CapsLock"),
}
}
pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool {
let navigator = browser_window.navigator();
#[allow(deprecated)]
// navigator.platform() is deprecated but navigator.userAgentData is not widely available yet
if let Ok(platform) = navigator.platform() {
if platform.contains("Mac") {
return true;
}
}
if let Ok(user_agent) = navigator.user_agent() {
return user_agent.contains("Mac");
}
false
}
fn is_modifier_only_key(key: &str) -> bool {
matches!(key, "control" | "alt" | "shift" | "platform" | "capslock")
}
fn compute_key_char(
event: &web_sys::KeyboardEvent,
gpui_key: &str,
modifiers: &Modifiers,
) -> Option<String> {
if modifiers.platform || modifiers.control {
return None;
}
if is_modifier_only_key(gpui_key) {
return None;
}
if gpui_key == "space" {
return Some(" ".to_string());
}
let raw_key = event.key();
if raw_key.len() == 1 {
return Some(raw_key);
}
None
}
fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point<Pixels> {
let mouse_event: &web_sys::MouseEvent = event.as_ref();
mouse_position_in_element(mouse_event)
}
fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point<Pixels> {
// offset_x/offset_y give position relative to the target element's padding edge
point(px(event.offset_x() as f32), px(event.offset_y() as f32))
}
fn extract_file_paths_from_drag(
event: &web_sys::DragEvent,
) -> smallvec::SmallVec<[std::path::PathBuf; 2]> {
let mut paths = smallvec![];
let Some(data_transfer) = event.data_transfer() else {
return paths;
};
let file_list = data_transfer.files();
let Some(files) = file_list else {
return paths;
};
for index in 0..files.length() {
if let Some(file) = files.get(index) {
paths.push(std::path::PathBuf::from(file.name()));
}
}
paths
}

View file

@ -0,0 +1,16 @@
#![cfg(target_family = "wasm")]
mod dispatcher;
mod display;
mod events;
mod keyboard;
mod logging;
mod platform;
mod window;
pub use dispatcher::WebDispatcher;
pub use display::WebDisplay;
pub use keyboard::WebKeyboardLayout;
pub use logging::init_logging;
pub use platform::WebPlatform;
pub use window::WebWindow;

View file

@ -0,0 +1,19 @@
use gpui::PlatformKeyboardLayout;
pub struct WebKeyboardLayout;
impl WebKeyboardLayout {
pub fn new() -> Self {
WebKeyboardLayout
}
}
impl PlatformKeyboardLayout for WebKeyboardLayout {
fn id(&self) -> &str {
"us"
}
fn name(&self) -> &str {
"US"
}
}

View file

@ -0,0 +1,37 @@
use log::{Level, Log, Metadata, Record};
struct ConsoleLogger;
impl Log for ConsoleLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let message = format!(
"[{}] {}: {}",
record.level(),
record.target(),
record.args()
);
let js_string = wasm_bindgen::JsValue::from_str(&message);
match record.level() {
Level::Error => web_sys::console::error_1(&js_string),
Level::Warn => web_sys::console::warn_1(&js_string),
Level::Info => web_sys::console::info_1(&js_string),
Level::Debug | Level::Trace => web_sys::console::log_1(&js_string),
}
}
fn flush(&self) {}
}
pub fn init_logging() {
log::set_logger(&ConsoleLogger).ok();
log::set_max_level(log::LevelFilter::Info);
}

View file

@ -0,0 +1,341 @@
use crate::dispatcher::WebDispatcher;
use crate::display::WebDisplay;
use crate::keyboard::WebKeyboardLayout;
use crate::window::WebWindow;
use anyhow::Result;
use futures::channel::oneshot;
use gpui::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper,
ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
ThermalState, WindowAppearance, WindowParams,
};
use gpui_wgpu::WgpuContext;
use std::{
borrow::Cow,
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
static BUNDLED_FONTS: &[&[u8]] = &[
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
];
pub struct WebPlatform {
browser_window: web_sys::Window,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<dyn PlatformTextSystem>,
active_window: RefCell<Option<AnyWindowHandle>>,
active_display: Rc<dyn PlatformDisplay>,
callbacks: RefCell<WebPlatformCallbacks>,
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
}
#[derive(Default)]
struct WebPlatformCallbacks {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
quit: Option<Box<dyn FnMut()>>,
reopen: Option<Box<dyn FnMut()>>,
app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
will_open_app_menu: Option<Box<dyn FnMut()>>,
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
keyboard_layout_change: Option<Box<dyn FnMut()>>,
thermal_state_change: Option<Box<dyn FnMut()>>,
}
impl WebPlatform {
pub fn new() -> Self {
let browser_window =
web_sys::window().expect("must be running in a browser window context");
let dispatcher = Arc::new(WebDispatcher::new(browser_window.clone()));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
"IBM Plex Sans",
));
let fonts = BUNDLED_FONTS
.iter()
.map(|bytes| Cow::Borrowed(*bytes))
.collect();
if let Err(error) = text_system.add_fonts(fonts) {
log::error!("failed to load bundled fonts: {error:#}");
}
let text_system: Arc<dyn PlatformTextSystem> = text_system;
let active_display: Rc<dyn PlatformDisplay> =
Rc::new(WebDisplay::new(browser_window.clone()));
Self {
browser_window,
background_executor,
foreground_executor,
text_system,
active_window: RefCell::new(None),
active_display,
callbacks: RefCell::new(WebPlatformCallbacks::default()),
wgpu_context: Rc::new(RefCell::new(None)),
}
}
}
impl Platform for WebPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.foreground_executor.clone()
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.text_system.clone()
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
let wgpu_context = self.wgpu_context.clone();
wasm_bindgen_futures::spawn_local(async move {
match WgpuContext::new_web().await {
Ok(context) => {
log::info!("WebGPU context initialized successfully");
*wgpu_context.borrow_mut() = Some(context);
on_finish_launching();
}
Err(err) => {
log::error!("Failed to initialize WebGPU context: {err:#}");
on_finish_launching();
}
}
});
}
fn quit(&self) {
log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
}
fn restart(&self, _binary_path: Option<PathBuf>) {}
fn activate(&self, _ignoring_other_apps: bool) {}
fn hide(&self) {}
fn hide_other_apps(&self) {}
fn unhide_other_apps(&self) {}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![self.active_display.clone()]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.active_display.clone())
}
fn active_window(&self) -> Option<AnyWindowHandle> {
*self.active_window.borrow()
}
fn open_window(
&self,
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let context_ref = self.wgpu_context.borrow();
let context = context_ref.as_ref().ok_or_else(|| {
anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
})?;
let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
*self.active_window.borrow_mut() = Some(handle);
Ok(Box::new(window))
}
fn window_appearance(&self) -> WindowAppearance {
let Ok(Some(media_query)) = self
.browser_window
.match_media("(prefers-color-scheme: dark)")
else {
return WindowAppearance::Light;
};
if media_query.matches() {
WindowAppearance::Dark
} else {
WindowAppearance::Light
}
}
fn open_url(&self, url: &str) {
if let Err(error) = self.browser_window.open_with_url(url) {
log::warn!("Failed to open URL '{url}': {error:?}");
}
}
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
self.callbacks.borrow_mut().open_urls = Some(callback);
}
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn prompt_for_paths(
&self,
_options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
let (tx, rx) = oneshot::channel();
tx.send(Err(anyhow::anyhow!(
"prompt_for_paths is not supported on the web"
)))
.ok();
rx
}
fn prompt_for_new_path(
&self,
_directory: &Path,
_suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
let (sender, receiver) = oneshot::channel();
sender
.send(Err(anyhow::anyhow!(
"prompt_for_new_path is not supported on the web"
)))
.ok();
receiver
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
false
}
fn reveal_path(&self, _path: &Path) {}
fn open_with_system(&self, _path: &Path) {}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().quit = Some(callback);
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().reopen = Some(callback);
}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.callbacks.borrow_mut().app_menu_action = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
}
fn thermal_state(&self) -> ThermalState {
ThermalState::Nominal
}
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().thermal_state_change = Some(callback);
}
fn compositor_name(&self) -> &'static str {
"Web"
}
fn app_path(&self) -> Result<PathBuf> {
Err(anyhow::anyhow!("app_path is not available on the web"))
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
Err(anyhow::anyhow!(
"path_for_auxiliary_executable is not available on the web"
))
}
fn set_cursor_style(&self, style: CursorStyle) {
let css_cursor = match style {
CursorStyle::Arrow => "default",
CursorStyle::IBeam => "text",
CursorStyle::Crosshair => "crosshair",
CursorStyle::ClosedHand => "grabbing",
CursorStyle::OpenHand => "grab",
CursorStyle::PointingHand => "pointer",
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
"ew-resize"
}
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
"ns-resize"
}
CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
CursorStyle::ResizeColumn => "col-resize",
CursorStyle::ResizeRow => "row-resize",
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
CursorStyle::OperationNotAllowed => "not-allowed",
CursorStyle::DragLink => "alias",
CursorStyle::DragCopy => "copy",
CursorStyle::ContextualMenu => "context-menu",
CursorStyle::None => "none",
};
if let Some(document) = self.browser_window.document() {
if let Some(body) = document.body() {
if let Err(error) = body.style().set_property("cursor", css_cursor) {
log::warn!("Failed to set cursor style: {error:?}");
}
}
}
}
fn should_auto_hide_scrollbars(&self) -> bool {
true
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
None
}
fn write_to_clipboard(&self, _item: ClipboardItem) {}
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
Task::ready(Ok(None))
}
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(WebKeyboardLayout)
}
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(DummyKeyboardMapper)
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
}
}

View file

@ -0,0 +1,689 @@
use crate::display::WebDisplay;
use crate::events::{ClickState, WebEventListeners, is_mac_platform};
use std::sync::Arc;
use std::{cell::Cell, cell::RefCell, rc::Rc};
use gpui::{
AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs,
Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowControls, WindowDecorations, WindowParams, px,
};
use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
use wasm_bindgen::prelude::*;
#[derive(Default)]
pub(crate) struct WebWindowCallbacks {
pub(crate) request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
pub(crate) input: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
pub(crate) active_status_change: Option<Box<dyn FnMut(bool)>>,
pub(crate) hover_status_change: Option<Box<dyn FnMut(bool)>>,
pub(crate) resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
pub(crate) moved: Option<Box<dyn FnMut()>>,
pub(crate) should_close: Option<Box<dyn FnMut() -> bool>>,
pub(crate) close: Option<Box<dyn FnOnce()>>,
pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
pub(crate) hit_test_window_control: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
}
pub(crate) struct WebWindowMutableState {
pub(crate) renderer: WgpuRenderer,
pub(crate) bounds: Bounds<Pixels>,
pub(crate) scale_factor: f32,
pub(crate) max_texture_dimension: u32,
pub(crate) title: String,
pub(crate) input_handler: Option<PlatformInputHandler>,
pub(crate) is_fullscreen: bool,
pub(crate) is_active: bool,
pub(crate) is_hovered: bool,
pub(crate) mouse_position: Point<Pixels>,
pub(crate) modifiers: Modifiers,
pub(crate) capslock: Capslock,
}
pub(crate) struct WebWindowInner {
pub(crate) browser_window: web_sys::Window,
pub(crate) canvas: web_sys::HtmlCanvasElement,
pub(crate) has_device_pixel_support: bool,
pub(crate) is_mac: bool,
pub(crate) state: RefCell<WebWindowMutableState>,
pub(crate) callbacks: RefCell<WebWindowCallbacks>,
pub(crate) click_state: RefCell<ClickState>,
pub(crate) pressed_button: Cell<Option<MouseButton>>,
pub(crate) last_physical_size: Cell<(u32, u32)>,
pub(crate) notify_scale: Cell<bool>,
mql_handle: RefCell<Option<MqlHandle>>,
}
pub struct WebWindow {
inner: Rc<WebWindowInner>,
display: Rc<dyn PlatformDisplay>,
#[allow(dead_code)]
handle: AnyWindowHandle,
_raf_closure: Closure<dyn FnMut()>,
_resize_observer: Option<web_sys::ResizeObserver>,
_resize_observer_closure: Closure<dyn FnMut(js_sys::Array)>,
_event_listeners: WebEventListeners,
}
impl WebWindow {
pub fn new(
handle: AnyWindowHandle,
_params: WindowParams,
context: &WgpuContext,
browser_window: web_sys::Window,
) -> anyhow::Result<Self> {
let document = browser_window
.document()
.ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?;
let canvas: web_sys::HtmlCanvasElement = document
.create_element("canvas")
.map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))?
.dyn_into()
.map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?;
let dpr = browser_window.device_pixel_ratio() as f32;
let max_texture_dimension = context.device.limits().max_texture_dimension_2d;
let has_device_pixel_support = check_device_pixel_support();
canvas.set_tab_index(0);
let style = canvas.style();
style
.set_property("width", "100%")
.map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?;
style
.set_property("height", "100%")
.map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?;
style
.set_property("display", "block")
.map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?;
style
.set_property("outline", "none")
.map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?;
style
.set_property("touch-action", "none")
.map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?;
let body = document
.body()
.ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?;
body.append_child(&canvas)
.map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?;
canvas.focus().ok();
let device_size = Size {
width: DevicePixels(0),
height: DevicePixels(0),
};
let renderer_config = WgpuSurfaceConfig {
size: device_size,
transparent: false,
};
let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?;
let display: Rc<dyn PlatformDisplay> = Rc::new(WebDisplay::new(browser_window.clone()));
let initial_bounds = Bounds {
origin: Point::default(),
size: Size::default(),
};
let mutable_state = WebWindowMutableState {
renderer,
bounds: initial_bounds,
scale_factor: dpr,
max_texture_dimension,
title: String::new(),
input_handler: None,
is_fullscreen: false,
is_active: true,
is_hovered: false,
mouse_position: Point::default(),
modifiers: Modifiers::default(),
capslock: Capslock::default(),
};
let is_mac = is_mac_platform(&browser_window);
let inner = Rc::new(WebWindowInner {
browser_window,
canvas,
has_device_pixel_support,
is_mac,
state: RefCell::new(mutable_state),
callbacks: RefCell::new(WebWindowCallbacks::default()),
click_state: RefCell::new(ClickState::default()),
pressed_button: Cell::new(None),
last_physical_size: Cell::new((0, 0)),
notify_scale: Cell::new(false),
mql_handle: RefCell::new(None),
});
let raf_closure = inner.create_raf_closure();
inner.schedule_raf(&raf_closure);
let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner));
let resize_observer =
web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok();
if let Some(ref observer) = resize_observer {
inner.observe_canvas(observer);
inner.watch_dpr_changes(observer);
}
let event_listeners = inner.register_event_listeners();
Ok(Self {
inner,
display,
handle,
_raf_closure: raf_closure,
_resize_observer: resize_observer,
_resize_observer_closure: resize_observer_closure,
_event_listeners: event_listeners,
})
}
fn create_resize_observer_closure(
inner: Rc<WebWindowInner>,
) -> Closure<dyn FnMut(js_sys::Array)> {
Closure::new(move |entries: js_sys::Array| {
let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() {
Some(entry) => entry,
None => return,
};
let dpr = inner.browser_window.device_pixel_ratio();
let dpr_f32 = dpr as f32;
let (physical_width, physical_height, logical_width, logical_height) =
if inner.has_device_pixel_support {
let size: web_sys::ResizeObserverSize = entry
.device_pixel_content_box_size()
.get(0)
.unchecked_into();
let pw = size.inline_size() as u32;
let ph = size.block_size() as u32;
let lw = pw as f64 / dpr;
let lh = ph as f64 / dpr;
(pw, ph, lw as f32, lh as f32)
} else {
// Safari fallback: use contentRect (always CSS px).
let rect = entry.content_rect();
let lw = rect.width() as f32;
let lh = rect.height() as f32;
let pw = (lw as f64 * dpr).round() as u32;
let ph = (lh as f64 * dpr).round() as u32;
(pw, ph, lw, lh)
};
let scale_changed = inner.notify_scale.replace(false);
let prev = inner.last_physical_size.get();
let size_changed = prev != (physical_width, physical_height);
if !scale_changed && !size_changed {
return;
}
inner
.last_physical_size
.set((physical_width, physical_height));
// Skip rendering to a zero-size canvas (e.g. display:none).
if physical_width == 0 || physical_height == 0 {
let mut s = inner.state.borrow_mut();
s.bounds.size = Size::default();
s.scale_factor = dpr_f32;
// Still fire the callback so GPUI knows the window is gone.
drop(s);
let mut cbs = inner.callbacks.borrow_mut();
if let Some(ref mut callback) = cbs.resize {
callback(Size::default(), dpr_f32);
}
return;
}
let max_texture_dimension = inner.state.borrow().max_texture_dimension;
let clamped_width = physical_width.min(max_texture_dimension);
let clamped_height = physical_height.min(max_texture_dimension);
inner.canvas.set_width(clamped_width);
inner.canvas.set_height(clamped_height);
{
let mut s = inner.state.borrow_mut();
s.bounds.size = Size {
width: px(logical_width),
height: px(logical_height),
};
s.scale_factor = dpr_f32;
s.renderer.update_drawable_size(Size {
width: DevicePixels(clamped_width as i32),
height: DevicePixels(clamped_height as i32),
});
}
let new_size = Size {
width: px(logical_width),
height: px(logical_height),
};
let mut cbs = inner.callbacks.borrow_mut();
if let Some(ref mut callback) = cbs.resize {
callback(new_size, dpr_f32);
}
})
}
}
impl WebWindowInner {
fn create_raf_closure(self: &Rc<Self>) -> Closure<dyn FnMut()> {
let raf_handle: Rc<RefCell<Option<js_sys::Function>>> = Rc::new(RefCell::new(None));
let raf_handle_inner = Rc::clone(&raf_handle);
let this = Rc::clone(self);
let closure = Closure::new(move || {
{
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.request_frame {
callback(RequestFrameOptions {
require_presentation: true,
force_render: false,
});
}
}
// Re-schedule for the next frame
if let Some(ref func) = *raf_handle_inner.borrow() {
this.browser_window.request_animation_frame(func).ok();
}
});
let js_func: js_sys::Function =
closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
*raf_handle.borrow_mut() = Some(js_func);
closure
}
fn schedule_raf(&self, closure: &Closure<dyn FnMut()>) {
self.browser_window
.request_animation_frame(closure.as_ref().unchecked_ref())
.ok();
}
fn observe_canvas(&self, observer: &web_sys::ResizeObserver) {
observer.unobserve(&self.canvas);
if self.has_device_pixel_support {
let options = web_sys::ResizeObserverOptions::new();
options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox);
observer.observe_with_options(&self.canvas, &options);
} else {
observer.observe(&self.canvas);
}
}
fn watch_dpr_changes(self: &Rc<Self>, observer: &web_sys::ResizeObserver) {
let current_dpr = self.browser_window.device_pixel_ratio();
let media_query =
format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})");
let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else {
return;
};
let this = Rc::clone(self);
let observer = observer.clone();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
this.notify_scale.set(true);
this.observe_canvas(&observer);
this.watch_dpr_changes(&observer);
});
mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
.ok();
*self.mql_handle.borrow_mut() = Some(MqlHandle {
mql,
_closure: closure,
});
}
pub(crate) fn register_visibility_change(
self: &Rc<Self>,
) -> Option<Closure<dyn FnMut(JsValue)>> {
let document = self.browser_window.document()?;
let this = Rc::clone(self);
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
let is_visible = this
.browser_window
.document()
.map(|doc| {
let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into())
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
state_str == "visible"
})
.unwrap_or(true);
{
let mut state = this.state.borrow_mut();
state.is_active = is_visible;
}
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.active_status_change {
callback(is_visible);
}
});
document
.add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref())
.ok();
Some(closure)
}
pub(crate) fn register_appearance_change(
self: &Rc<Self>,
) -> Option<Closure<dyn FnMut(JsValue)>> {
let mql = self
.browser_window
.match_media("(prefers-color-scheme: dark)")
.ok()??;
let this = Rc::clone(self);
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.appearance_changed {
callback();
}
});
mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
.ok();
Some(closure)
}
}
fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance {
let is_dark = browser_window
.match_media("(prefers-color-scheme: dark)")
.ok()
.flatten()
.map(|mql| mql.matches())
.unwrap_or(false);
if is_dark {
WindowAppearance::Dark
} else {
WindowAppearance::Light
}
}
struct MqlHandle {
mql: web_sys::MediaQueryList,
_closure: Closure<dyn FnMut(JsValue)>,
}
impl Drop for MqlHandle {
fn drop(&mut self) {
self.mql
.remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref())
.ok();
}
}
// Safari does not support `devicePixelContentBoxSize`, so detect whether it's available.
fn check_device_pixel_support() -> bool {
let global: JsValue = js_sys::global().into();
let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else {
return false;
};
let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else {
return false;
};
let descriptor = js_sys::Object::get_own_property_descriptor(
&prototype.unchecked_into::<js_sys::Object>(),
&"devicePixelContentBoxSize".into(),
);
!descriptor.is_undefined()
}
impl raw_window_handle::HasWindowHandle for WebWindow {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
let canvas_ref: &JsValue = self.inner.canvas.as_ref();
let obj = std::ptr::NonNull::from(canvas_ref).cast::<std::ffi::c_void>();
let handle = raw_window_handle::WebCanvasWindowHandle::new(obj);
Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) })
}
}
impl raw_window_handle::HasDisplayHandle for WebWindow {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
Ok(raw_window_handle::DisplayHandle::web())
}
}
impl PlatformWindow for WebWindow {
fn bounds(&self) -> Bounds<Pixels> {
self.inner.state.borrow().bounds
}
fn is_maximized(&self) -> bool {
false
}
fn window_bounds(&self) -> WindowBounds {
WindowBounds::Windowed(self.bounds())
}
fn content_size(&self) -> Size<Pixels> {
self.inner.state.borrow().bounds.size
}
fn resize(&mut self, size: Size<Pixels>) {
let style = self.inner.canvas.style();
style
.set_property("width", &format!("{}px", f32::from(size.width)))
.ok();
style
.set_property("height", &format!("{}px", f32::from(size.height)))
.ok();
}
fn scale_factor(&self) -> f32 {
self.inner.state.borrow().scale_factor
}
fn appearance(&self) -> WindowAppearance {
current_appearance(&self.inner.browser_window)
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.display.clone())
}
fn mouse_position(&self) -> Point<Pixels> {
self.inner.state.borrow().mouse_position
}
fn modifiers(&self) -> Modifiers {
self.inner.state.borrow().modifiers
}
fn capslock(&self) -> Capslock {
self.inner.state.borrow().capslock
}
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
self.inner.state.borrow_mut().input_handler = Some(input_handler);
}
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
self.inner.state.borrow_mut().input_handler.take()
}
fn prompt(
&self,
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[PromptButton],
) -> Option<futures::channel::oneshot::Receiver<usize>> {
None
}
fn activate(&self) {
self.inner.state.borrow_mut().is_active = true;
}
fn is_active(&self) -> bool {
self.inner.state.borrow().is_active
}
fn is_hovered(&self) -> bool {
self.inner.state.borrow().is_hovered
}
fn background_appearance(&self) -> WindowBackgroundAppearance {
WindowBackgroundAppearance::Opaque
}
fn set_title(&mut self, title: &str) {
self.inner.state.borrow_mut().title = title.to_owned();
if let Some(document) = self.inner.browser_window.document() {
document.set_title(title);
}
}
fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
fn minimize(&self) {
log::warn!("WebWindow::minimize is not supported in the browser");
}
fn zoom(&self) {
log::warn!("WebWindow::zoom is not supported in the browser");
}
fn toggle_fullscreen(&self) {
let mut state = self.inner.state.borrow_mut();
state.is_fullscreen = !state.is_fullscreen;
if state.is_fullscreen {
let canvas: &web_sys::Element = self.inner.canvas.as_ref();
canvas.request_fullscreen().ok();
} else {
if let Some(document) = self.inner.browser_window.document() {
document.exit_fullscreen();
}
}
}
fn is_fullscreen(&self) -> bool {
self.inner.state.borrow().is_fullscreen
}
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
self.inner.callbacks.borrow_mut().request_frame = Some(callback);
}
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
self.inner.callbacks.borrow_mut().input = Some(callback);
}
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.inner.callbacks.borrow_mut().active_status_change = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.inner.callbacks.borrow_mut().hover_status_change = Some(callback);
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.inner.callbacks.borrow_mut().resize = Some(callback);
}
fn on_moved(&self, callback: Box<dyn FnMut()>) {
self.inner.callbacks.borrow_mut().moved = Some(callback);
}
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
self.inner.callbacks.borrow_mut().should_close = Some(callback);
}
fn on_close(&self, callback: Box<dyn FnOnce()>) {
self.inner.callbacks.borrow_mut().close = Some(callback);
}
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback);
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.inner.callbacks.borrow_mut().appearance_changed = Some(callback);
}
fn draw(&self, scene: &Scene) {
self.inner.state.borrow_mut().renderer.draw(scene);
}
fn completed_frame(&self) {
// On web, presentation happens automatically via wgpu surface present
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.inner.state.borrow().renderer.sprite_atlas().clone()
}
fn is_subpixel_rendering_supported(&self) -> bool {
self.inner
.state
.borrow()
.renderer
.supports_dual_source_blending()
}
fn gpu_specs(&self) -> Option<GpuSpecs> {
Some(self.inner.state.borrow().renderer.gpu_specs())
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
fn request_decorations(&self, _decorations: WindowDecorations) {}
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_window_move(&self) {}
fn start_window_resize(&self, _edge: ResizeEdge) {}
fn window_decorations(&self) -> Decorations {
Decorations::Server
}
fn set_app_id(&mut self, _app_id: &str) {}
fn window_controls(&self) -> WindowControls {
WindowControls {
fullscreen: true,
maximize: false,
minimize: false,
window_menu: false,
}
}
fn set_client_inset(&self, _inset: Pixels) {}
}

View file

@ -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"

View file

@ -0,0 +1,645 @@
use anyhow::{Context as _, Ok, Result};
use collections::HashMap;
use cosmic_text::{
Attrs, AttrsList, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
FontSystem, ShapeBuffer, ShapeLine,
};
use gpui::{
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, GlyphId, LineLayout,
Pixels, PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y,
ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point, size,
};
use itertools::Itertools;
use parking_lot::RwLock;
use smallvec::SmallVec;
use std::{borrow::Cow, sync::Arc};
use swash::{
scale::{Render, ScaleContext, Source, StrikeWith},
zeno::{Format, Vector},
};
pub struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct FontKey {
family: SharedString,
features: FontFeatures,
}
impl FontKey {
fn new(family: SharedString, features: FontFeatures) -> Self {
Self { family, features }
}
}
struct CosmicTextSystemState {
font_system: FontSystem,
scratch: ShapeBuffer,
swash_scale_context: ScaleContext,
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
loaded_fonts: Vec<LoadedFont>,
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
/// for every font face in a family.
font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
system_font_fallback: String,
}
struct LoadedFont {
font: Arc<CosmicTextFont>,
features: CosmicFontFeatures,
is_known_emoji_font: bool,
}
impl CosmicTextSystem {
pub fn new(system_font_fallback: &str) -> Self {
let font_system = FontSystem::new();
Self(RwLock::new(CosmicTextSystemState {
font_system,
scratch: ShapeBuffer::default(),
swash_scale_context: ScaleContext::new(),
loaded_fonts: Vec::new(),
font_ids_by_family_cache: HashMap::default(),
system_font_fallback: system_font_fallback.to_string(),
}))
}
pub fn new_without_system_fonts(system_font_fallback: &str) -> Self {
let font_system = FontSystem::new_with_locale_and_db(
"en-US".to_string(),
cosmic_text::fontdb::Database::new(),
);
Self(RwLock::new(CosmicTextSystemState {
font_system,
scratch: ShapeBuffer::default(),
swash_scale_context: ScaleContext::new(),
loaded_fonts: Vec::new(),
font_ids_by_family_cache: HashMap::default(),
system_font_fallback: system_font_fallback.to_string(),
}))
}
}
impl PlatformTextSystem for CosmicTextSystem {
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
self.0.write().add_fonts(fonts)
}
fn all_font_names(&self) -> Vec<String> {
let mut result = self
.0
.read()
.font_system
.db()
.faces()
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
.collect_vec();
result.sort();
result.dedup();
result
}
fn font_id(&self, font: &Font) -> Result<FontId> {
let mut state = self.0.write();
let key = FontKey::new(font.family.clone(), font.features.clone());
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
font_ids.as_slice()
} else {
let font_ids = state.load_family(&font.family, &font.features)?;
state.font_ids_by_family_cache.insert(key.clone(), font_ids);
state.font_ids_by_family_cache[&key].as_ref()
};
let ix = find_best_match(font, candidates, &state)?;
Ok(candidates[ix])
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
let metrics = self
.0
.read()
.loaded_font(font_id)
.font
.as_swash()
.metrics(&[]);
FontMetrics {
units_per_em: metrics.units_per_em as u32,
ascent: metrics.ascent,
descent: -metrics.descent,
line_gap: metrics.leading,
underline_position: metrics.underline_offset,
underline_thickness: metrics.stroke_size,
cap_height: metrics.cap_height,
x_height: metrics.x_height,
bounding_box: Bounds {
origin: point(0.0, 0.0),
size: size(metrics.max_width, metrics.ascent + metrics.descent),
},
}
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
let lock = self.0.read();
let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
let glyph_id = glyph_id.0 as u16;
Ok(Bounds {
origin: point(0.0, 0.0),
size: size(
glyph_metrics.advance_width(glyph_id),
glyph_metrics.advance_height(glyph_id),
),
})
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
self.0.read().advance(font_id, glyph_id)
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.0.read().glyph_for_char(font_id, ch)
}
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
self.0.write().raster_bounds(params)
}
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.0.write().rasterize_glyph(params, raster_bounds)
}
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
self.0.write().layout_line(text, font_size, runs)
}
fn recommended_rendering_mode(
&self,
_font_id: FontId,
_font_size: Pixels,
) -> TextRenderingMode {
TextRenderingMode::Subpixel
}
}
impl CosmicTextSystemState {
fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
&self.loaded_fonts[font_id.0]
}
#[profiling::function]
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
let db = self.font_system.db_mut();
for bytes in fonts {
match bytes {
Cow::Borrowed(embedded_font) => {
db.load_font_data(embedded_font.to_vec());
}
Cow::Owned(bytes) => {
db.load_font_data(bytes);
}
}
}
Ok(())
}
#[profiling::function]
fn load_family(
&mut self,
name: &str,
features: &FontFeatures,
) -> Result<SmallVec<[FontId; 4]>> {
let name = gpui::font_name_with_fallbacks(name, &self.system_font_fallback);
let families = self
.font_system
.db()
.faces()
.filter(|face| face.families.iter().any(|family| *name == family.0))
.map(|face| (face.id, face.post_script_name.clone()))
.collect::<SmallVec<[_; 4]>>();
let mut loaded_font_ids = SmallVec::new();
for (font_id, postscript_name) in families {
let font = self
.font_system
.get_font(font_id, cosmic_text::Weight::NORMAL)
.context("Could not load font")?;
// HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
let allowed_bad_font_names = [
"SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
"Segoe Fluent Icons",
];
if font.as_swash().charmap().map('m') == 0
&& !allowed_bad_font_names.contains(&postscript_name.as_str())
{
self.font_system.db_mut().remove_face(font.id());
continue;
};
let font_id = FontId(self.loaded_fonts.len());
loaded_font_ids.push(font_id);
self.loaded_fonts.push(LoadedFont {
font,
features: cosmic_font_features(features)?,
is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
});
}
Ok(loaded_font_ids)
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
Ok(Size {
width: glyph_metrics.advance_width(glyph_id.0 as u16),
height: glyph_metrics.advance_height(glyph_id.0 as u16),
})
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
if glyph_id == 0 {
None
} else {
Some(GlyphId(glyph_id.into()))
}
}
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let image = self.render_glyph_image(params)?;
Ok(Bounds {
origin: point(image.placement.left.into(), (-image.placement.top).into()),
size: size(image.placement.width.into(), image.placement.height.into()),
})
}
#[profiling::function]
fn rasterize_glyph(
&mut self,
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
anyhow::bail!("glyph bounds are empty");
}
let mut image = self.render_glyph_image(params)?;
let bitmap_size = glyph_bounds.size;
match image.content {
swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
// Convert from RGBA to BGRA.
for pixel in image.data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
Ok((bitmap_size, image.data))
}
swash::scale::image::Content::Mask => Ok((bitmap_size, image.data)),
}
}
fn render_glyph_image(
&mut self,
params: &RenderGlyphParams,
) -> Result<swash::scale::image::Image> {
let loaded_font = &self.loaded_fonts[params.font_id.0];
let font_ref = loaded_font.font.as_swash();
let pixel_size = f32::from(params.font_size);
let subpixel_offset = Vector::new(
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
);
let mut scaler = self
.swash_scale_context
.builder(font_ref)
.size(pixel_size * params.scale_factor)
.hint(true)
.build();
let sources: &[Source] = if params.is_emoji {
&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
]
} else {
&[Source::Outline]
};
let mut renderer = Render::new(sources);
if params.subpixel_rendering {
// There seems to be a bug in Swash where the B and R values are swapped.
renderer
.format(Format::subpixel_bgra())
.offset(subpixel_offset);
} else {
renderer.format(Format::Alpha).offset(subpixel_offset);
}
let glyph_id: u16 = params.glyph_id.0.try_into()?;
renderer
.render(&mut scaler, glyph_id)
.with_context(|| format!("unable to render glyph via swash for {params:?}"))
}
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
/// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
/// yet have an entry for this fallback font, and so one is added.
///
/// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
/// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
/// current use of this field is for the *input* of `layout_line`, and so it's fine to use
/// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> Result<FontId> {
if let Some(ix) = self
.loaded_fonts
.iter()
.position(|loaded_font| loaded_font.font.id() == id)
{
Ok(FontId(ix))
} else {
let font = self
.font_system
.get_font(id, cosmic_text::Weight::NORMAL)
.context("failed to get fallback font from cosmic-text font system")?;
let face = self
.font_system
.db()
.face(id)
.context("fallback font face not found in cosmic-text database")?;
let font_id = FontId(self.loaded_fonts.len());
self.loaded_fonts.push(LoadedFont {
font,
features: CosmicFontFeatures::new(),
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
});
Ok(font_id)
}
}
#[profiling::function]
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
let mut attrs_list = AttrsList::new(&Attrs::new());
let mut offs = 0;
for run in font_runs {
let loaded_font = self.loaded_font(run.font_id);
let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
log::warn!(
"font face not found in database for font_id {:?}",
run.font_id
);
offs += run.len;
continue;
};
let Some(first_family) = face.families.first() else {
log::warn!(
"font face has no family names for font_id {:?}",
run.font_id
);
offs += run.len;
continue;
};
attrs_list.add_span(
offs..(offs + run.len),
&Attrs::new()
.metadata(run.font_id.0)
.family(Family::Name(&first_family.0))
.stretch(face.stretch)
.style(face.style)
.weight(face.weight)
.font_features(loaded_font.features.clone()),
);
offs += run.len;
}
let line = ShapeLine::new(
&mut self.font_system,
text,
&attrs_list,
cosmic_text::Shaping::Advanced,
4,
);
let mut layout_lines = Vec::with_capacity(1);
line.layout_to_buffer(
&mut self.scratch,
f32::from(font_size),
None, // We do our own wrapping
cosmic_text::Wrap::None,
None,
&mut layout_lines,
None,
cosmic_text::Hinting::Disabled,
);
let Some(layout) = layout_lines.first() else {
return LineLayout {
font_size,
width: Pixels::ZERO,
ascent: Pixels::ZERO,
descent: Pixels::ZERO,
runs: Vec::new(),
len: text.len(),
};
};
let mut runs: Vec<ShapedRun> = Vec::new();
for glyph in &layout.glyphs {
let mut font_id = FontId(glyph.metadata);
let mut loaded_font = self.loaded_font(font_id);
if loaded_font.font.id() != glyph.font_id {
match self.font_id_for_cosmic_id(glyph.font_id) {
std::result::Result::Ok(resolved_id) => {
font_id = resolved_id;
loaded_font = self.loaded_font(font_id);
}
Err(error) => {
log::warn!(
"failed to resolve cosmic font id {:?}: {error:#}",
glyph.font_id
);
continue;
}
}
}
let is_emoji = loaded_font.is_known_emoji_font;
// HACK: Prevent crash caused by variation selectors.
if glyph.glyph_id == 3 && is_emoji {
continue;
}
let shaped_glyph = ShapedGlyph {
id: GlyphId(glyph.glyph_id as u32),
position: point(glyph.x.into(), glyph.y.into()),
index: glyph.start,
is_emoji,
};
if let Some(last_run) = runs
.last_mut()
.filter(|last_run| last_run.font_id == font_id)
{
last_run.glyphs.push(shaped_glyph);
} else {
runs.push(ShapedRun {
font_id,
glyphs: vec![shaped_glyph],
});
}
}
LineLayout {
font_size,
width: layout.w.into(),
ascent: layout.max_ascent.into(),
descent: layout.max_descent.into(),
runs,
len: text.len(),
}
}
}
#[cfg(feature = "font-kit")]
fn find_best_match(
font: &Font,
candidates: &[FontId],
state: &CosmicTextSystemState,
) -> Result<usize> {
let candidate_properties = candidates
.iter()
.map(|font_id| {
let database_id = state.loaded_font(*font_id).font.id();
let face_info = state
.font_system
.db()
.face(database_id)
.context("font face not found in database")?;
Ok(face_info_into_properties(face_info))
})
.collect::<Result<SmallVec<[_; 4]>>>()?;
let ix =
font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
.context("requested font family contains no font matching the other parameters")?;
Ok(ix)
}
#[cfg(not(feature = "font-kit"))]
fn find_best_match(
font: &Font,
candidates: &[FontId],
state: &CosmicTextSystemState,
) -> Result<usize> {
if candidates.is_empty() {
anyhow::bail!("requested font family contains no font matching the other parameters");
}
if candidates.len() == 1 {
return Ok(0);
}
let target_weight = font.weight.0;
let target_italic = matches!(
font.style,
gpui::FontStyle::Italic | gpui::FontStyle::Oblique
);
let mut best_index = 0;
let mut best_score = u32::MAX;
for (index, font_id) in candidates.iter().enumerate() {
let database_id = state.loaded_font(*font_id).font.id();
let face_info = state
.font_system
.db()
.face(database_id)
.context("font face not found in database")?;
let is_italic = matches!(
face_info.style,
cosmic_text::Style::Italic | cosmic_text::Style::Oblique
);
let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 };
let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs();
let score = style_penalty + weight_diff;
if score < best_score {
best_score = score;
best_index = index;
}
}
Ok(best_index)
}
fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
let mut result = CosmicFontFeatures::new();
for feature in features.0.iter() {
let name_bytes: [u8; 4] = feature
.0
.as_bytes()
.try_into()
.context("Incorrect feature flag format")?;
let tag = cosmic_text::FeatureTag::new(&name_bytes);
result.set(tag, feature.1);
}
Ok(result)
}
#[cfg(feature = "font-kit")]
fn font_into_properties(font: &gpui::Font) -> font_kit::properties::Properties {
font_kit::properties::Properties {
style: match font.style {
gpui::FontStyle::Normal => font_kit::properties::Style::Normal,
gpui::FontStyle::Italic => font_kit::properties::Style::Italic,
gpui::FontStyle::Oblique => font_kit::properties::Style::Oblique,
},
weight: font_kit::properties::Weight(font.weight.0),
stretch: Default::default(),
}
}
#[cfg(feature = "font-kit")]
fn face_info_into_properties(
face_info: &cosmic_text::fontdb::FaceInfo,
) -> font_kit::properties::Properties {
font_kit::properties::Properties {
style: match face_info.style {
cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
},
weight: font_kit::properties::Weight(face_info.weight.0.into()),
stretch: match face_info.stretch {
cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
},
}
}
fn check_is_known_emoji_font(postscript_name: &str) -> bool {
// TODO: Include other common emoji fonts
postscript_name == "NotoColorEmoji"
}

View file

@ -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::*;

View file

@ -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;
}

View file

@ -0,0 +1,53 @@
// --- subpixel sprites --- //
struct SubpixelSprite {
order: u32,
pad: u32,
bounds: Bounds,
content_mask: Bounds,
color: Hsla,
tile: AtlasTile,
transformation: TransformationMatrix,
}
@group(1) @binding(0) var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
struct SubpixelSpriteOutput {
@builtin(position) position: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) color: vec4<f32>,
@location(3) clip_distances: vec4<f32>,
}
struct SubpixelSpriteFragmentOutput {
@location(0) @blend_src(0) foreground: vec4<f32>,
@location(0) @blend_src(1) alpha: vec4<f32>,
}
@vertex
fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
let sprite = b_subpixel_sprites[instance_id];
var out = SubpixelSpriteOutput();
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.color = hsla_to_rgba(sprite.color);
out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
return out;
}
@fragment
fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
let sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios);
// Alpha clip after using the derivatives.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
}
var out = SubpixelSpriteFragmentOutput();
out.foreground = vec4<f32>(input.color.rgb, 1.0);
out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
return out;
}

View file

@ -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();

View file

@ -5,6 +5,7 @@ use gpui::{
PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, SubpixelSprite,
Underline, get_gamma_correction_ratios,
};
#[cfg(not(target_family = "wasm"))]
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use std::num::NonZeroU64;
use std::sync::Arc;
@ -123,6 +124,7 @@ impl WgpuRenderer {
/// # Safety
/// The caller must ensure that the window handle remains valid for the lifetime
/// of the returned renderer.
#[cfg(not(target_family = "wasm"))]
pub fn new<W: HasWindowHandle + HasDisplayHandle>(
gpu_context: &mut Option<WgpuContext>,
window: &W,
@ -165,6 +167,27 @@ impl WgpuRenderer {
None => gpu_context.insert(WgpuContext::new(instance, &surface)?),
};
Self::new_with_surface(context, surface, config)
}
#[cfg(target_family = "wasm")]
pub fn new_from_canvas(
context: &WgpuContext,
canvas: &web_sys::HtmlCanvasElement,
config: WgpuSurfaceConfig,
) -> anyhow::Result<Self> {
let surface = context
.instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?;
Self::new_with_surface(context, surface, config)
}
pub fn new_with_surface(
context: &WgpuContext,
surface: wgpu::Surface<'static>,
config: WgpuSurfaceConfig,
) -> anyhow::Result<Self> {
let surface_caps = surface.get_capabilities(&context.adapter);
let preferred_formats = [
wgpu::TextureFormat::Bgra8Unorm,
@ -497,12 +520,25 @@ impl WgpuRenderer {
path_sample_count: u32,
dual_source_blending: bool,
) -> WgpuPipelines {
let shader_source = include_str!("shaders.wgsl");
let base_shader_source = include_str!("shaders.wgsl");
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("gpui_shaders"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(base_shader_source)),
});
let subpixel_shader_source = include_str!("shaders_subpixel.wgsl");
let subpixel_shader_module = if dual_source_blending {
let combined = format!(
"enable dual_source_blending;\n{base_shader_source}\n{subpixel_shader_source}"
);
Some(device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("gpui_subpixel_shaders"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(combined)),
}))
} else {
None
};
let blend_mode = match alpha_mode {
wgpu::CompositeAlphaMode::PreMultiplied => {
wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING
@ -523,7 +559,8 @@ impl WgpuRenderer {
data_layout: &wgpu::BindGroupLayout,
topology: wgpu::PrimitiveTopology,
color_targets: &[Option<wgpu::ColorTargetState>],
sample_count: u32| {
sample_count: u32,
module: &wgpu::ShaderModule| {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(&format!("{name}_layout")),
bind_group_layouts: &[globals_layout, data_layout],
@ -534,13 +571,13 @@ impl WgpuRenderer {
label: Some(name),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader_module,
module,
entry_point: Some(vs_entry),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader_module,
module,
entry_point: Some(fs_entry),
targets: color_targets,
compilation_options: wgpu::PipelineCompilationOptions::default(),
@ -574,6 +611,7 @@ impl WgpuRenderer {
wgpu::PrimitiveTopology::TriangleStrip,
&[Some(color_target.clone())],
1,
&shader_module,
);
let shadows = create_pipeline(
@ -585,6 +623,7 @@ impl WgpuRenderer {
wgpu::PrimitiveTopology::TriangleStrip,
&[Some(color_target.clone())],
1,
&shader_module,
);
let path_rasterization = create_pipeline(
@ -600,6 +639,7 @@ impl WgpuRenderer {
write_mask: wgpu::ColorWrites::ALL,
})],
path_sample_count,
&shader_module,
);
let paths_blend = wgpu::BlendState {
@ -628,6 +668,7 @@ impl WgpuRenderer {
write_mask: wgpu::ColorWrites::ALL,
})],
1,
&shader_module,
);
let underlines = create_pipeline(
@ -639,6 +680,7 @@ impl WgpuRenderer {
wgpu::PrimitiveTopology::TriangleStrip,
&[Some(color_target.clone())],
1,
&shader_module,
);
let mono_sprites = create_pipeline(
@ -650,9 +692,10 @@ impl WgpuRenderer {
wgpu::PrimitiveTopology::TriangleStrip,
&[Some(color_target.clone())],
1,
&shader_module,
);
let subpixel_sprites = if dual_source_blending {
let subpixel_sprites = if let Some(subpixel_module) = &subpixel_shader_module {
let subpixel_blend = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Src1,
@ -679,6 +722,7 @@ impl WgpuRenderer {
write_mask: wgpu::ColorWrites::COLOR,
})],
1,
subpixel_module,
))
} else {
None
@ -693,6 +737,7 @@ impl WgpuRenderer {
wgpu::PrimitiveTopology::TriangleStrip,
&[Some(color_target.clone())],
1,
&shader_module,
);
let surfaces = create_pipeline(
@ -704,6 +749,7 @@ impl WgpuRenderer {
wgpu::PrimitiveTopology::TriangleStrip,
&[Some(color_target)],
1,
&shader_module,
);
WgpuPipelines {
@ -837,6 +883,10 @@ impl WgpuRenderer {
&self.atlas
}
pub fn supports_dual_source_blending(&self) -> bool {
self.dual_source_blending
}
pub fn gpu_specs(&self) -> GpuSpecs {
GpuSpecs {
is_software_emulated: self.adapter_info.device_type == wgpu::DeviceType::Cpu,

Some files were not shown because too many files have changed in this diff Show more