diff --git a/Cargo.lock b/Cargo.lock index 2bbc846044f24..8c2a2d4e7dfcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,12 +267,38 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "ascii" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + +[[package]] +name = "ash-window" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b912285a7c29f3a8f87ca6f55afc48768624e5e33ec17dbd2f2075903f5e35ab" +dependencies = [ + "ash", + "raw-window-handle 0.5.2", + "raw-window-metal", +] + [[package]] name = "assets" version = "0.1.0" @@ -908,6 +934,46 @@ dependencies = [ "wyz", ] +[[package]] +name = "blade-graphics" +version = "0.3.0" +source = "git+https://github.com/kvark/blade?rev=f35bc605154e210ab6190291235889b6ddad73f1#f35bc605154e210ab6190291235889b6ddad73f1" +dependencies = [ + "ash", + "ash-window", + "bitflags 2.4.1", + "block", + "bytemuck", + "codespan-reporting", + "core-graphics-types", + "glow", + "gpu-alloc", + "gpu-alloc-ash", + "hidden-trait", + "js-sys", + "khronos-egl", + "libloading 0.8.0", + "log", + "metal 0.25.0", + "mint", + "naga", + "objc", + "raw-window-handle 0.5.2", + "slab", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "blade-macros" +version = "0.2.1" +source = "git+https://github.com/kvark/blade?rev=f35bc605154e210ab6190291235889b6ddad73f1#f35bc605154e210ab6190291235889b6ddad73f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "block" version = "0.1.6" @@ -1065,6 +1131,20 @@ name = "bytemuck" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] [[package]] name = "byteorder" @@ -1438,6 +1518,16 @@ dependencies = [ "objc", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "collab" version = "0.44.0" @@ -2751,6 +2841,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", + "nanorand", "spin 0.9.8", ] @@ -3124,8 +3215,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -3213,6 +3306,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "go_to_line" version = "0.1.0" @@ -3230,16 +3335,50 @@ dependencies = [ "workspace", ] +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.4.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-ash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2424bc9be88170e1a56e57c25d3d0e2dfdd22e8f328e892786aeb4da1415732" +dependencies = [ + "ash", + "gpu-alloc-types", + "tinyvec", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.4.1", +] + [[package]] name = "gpui" version = "0.1.0" dependencies = [ "anyhow", + "as-raw-xcb-connection", "async-task", "backtrace", "bindgen 0.65.1", "bitflags 2.4.1", + "blade-graphics", + "blade-macros", "block", + "bytemuck", "cbindgen", "cocoa", "collections", @@ -3251,6 +3390,7 @@ dependencies = [ "dhat", "env_logger", "etagere", + "flume", "font-kit", "foreign-types 0.3.2", "futures 0.3.28", @@ -3261,7 +3401,7 @@ dependencies = [ "linkme", "log", "media", - "metal", + "metal 0.21.0", "num_cpus", "objc", "ordered-float 2.10.0", @@ -3271,6 +3411,7 @@ dependencies = [ "png", "postage", "rand 0.8.5", + "raw-window-handle 0.5.2", "raw-window-handle 0.6.0", "refineable", "resvg", @@ -3292,6 +3433,7 @@ dependencies = [ "util", "uuid 1.4.1", "waker-fn", + "xcb", ] [[package]] @@ -3428,6 +3570,23 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hidden-trait" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed9e850438ac849bec07e7d09fbe9309cbd396a5988c30b010580ce08860df" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "hkdf" version = "0.12.3" @@ -3911,6 +4070,16 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "khronos-egl" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1382b16c04aeb821453d6215a3c80ba78f24c6595c5aa85653378aabe0c83e3" +dependencies = [ + "libc", + "libloading 0.8.0", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -4216,8 +4385,10 @@ dependencies = [ "block", "byteorder", "bytes 1.5.0", + "cocoa", "collections", "core-foundation", + "core-graphics 0.22.3", "foreign-types 0.3.2", "futures 0.3.28", "gpui", @@ -4227,6 +4398,7 @@ dependencies = [ "log", "media", "nanoid", + "objc", "parking_lot 0.11.2", "postage", "serde", @@ -4414,7 +4586,7 @@ dependencies = [ "bytes 1.5.0", "core-foundation", "foreign-types 0.3.2", - "metal", + "metal 0.21.0", "objc", ] @@ -4482,6 +4654,21 @@ dependencies = [ "objc", ] +[[package]] +name = "metal" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550b24b0cd4cf923f36bae78eca457b3a10d8a6a14a9c84cb2687b527e6a84af" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-graphics-types", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + [[package]] name = "mimalloc" version = "0.1.39" @@ -4531,6 +4718,12 @@ dependencies = [ "adler", ] +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + [[package]] name = "mintex" version = "0.1.2" @@ -4658,6 +4851,26 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "naga" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e" +dependencies = [ + "bit-set", + "bitflags 2.4.1", + "codespan-reporting", + "hexf-parse", + "indexmap 2.0.0", + "log", + "num-traits", + "rustc-hash", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + [[package]] name = "nanoid" version = "0.4.0" @@ -4667,6 +4880,15 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.10", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -5600,9 +5822,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plist" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" +checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa" dependencies = [ "base64 0.21.4", "indexmap 1.9.3", @@ -6069,9 +6291,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", ] @@ -6187,6 +6409,18 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +[[package]] +name = "raw-window-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4ea493258d54c24cb46aa9345d099e58e2ea3f30dd63667fc54fc892f18e76" +dependencies = [ + "cocoa", + "core-graphics 0.23.1", + "objc", + "raw-window-handle 0.5.2", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -7528,6 +7762,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spirv" +version = "0.2.0+1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" +dependencies = [ + "bitflags 1.3.2", + "num-traits", +] + [[package]] name = "spki" version = "0.7.2" @@ -9284,6 +9528,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -10284,6 +10534,18 @@ dependencies = [ "libc", ] +[[package]] +name = "xcb" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d27b37e69b8c05bfadcd968eb1a4fe27c9c52565b727f88512f43b89567e262" +dependencies = [ + "as-raw-xcb-connection", + "bitflags 1.3.2", + "libc", + "quick-xml", +] + [[package]] name = "xmlparser" version = "0.13.5" diff --git a/Cargo.toml b/Cargo.toml index c7e40436f41f8..4633b0c122b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -277,6 +277,11 @@ wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0. split-debuginfo = "unpacked" debug = "limited" +# todo!(linux) - Remove this +[profile.dev.package.blade-graphics] +split-debuginfo = "off" +debug = "full" + [profile.dev.package.taffy] opt-level = 3 diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index edd3da101cb8a..bdb32d7d76eb7 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -287,12 +287,17 @@ impl Fs for RealFs { ) -> Pin>>> { let (tx, rx) = smol::channel::unbounded(); + if !path.exists() { + log::error!("watch path does not exist: {}", path.display()); + return Box::pin(rx); + } + let mut watcher = notify::recommended_watcher(move |res| match res { Ok(event) => { let _ = tx.try_send(vec![event]); } Err(err) => { - eprintln!("watch error: {:?}", err); + log::error!("watch error: {}", err); } }) .unwrap(); diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 9e250acda9358..0c7605eb36195 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -33,6 +33,7 @@ dhat = { version = "0.3", optional = true } env_logger = { version = "0.9", optional = true } etagere = "0.2" futures.workspace = true +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "d97147f" } gpui_macros.workspace = true image = "0.23" itertools = "0.10" @@ -46,7 +47,8 @@ parking_lot.workspace = true pathfinder_geometry = "0.5" postage.workspace = true rand.workspace = true -raw-window-handle = "0.6.0" +raw-window-handle = "0.6" +blade-rwh = { package = "raw-window-handle", version = "0.5" } refineable.workspace = true resvg = "0.14" schemars.workspace = true @@ -86,9 +88,17 @@ cocoa = "0.25" core-foundation = { version = "0.9.3", features = ["with-uuid"] } core-graphics = "0.22.3" core-text = "19.2" -font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "d97147f" } foreign-types = "0.3" log.workspace = true media.workspace = true metal = "0.21.0" objc = "0.2" + +[target.'cfg(target_os = "linux")'.dependencies] +flume = "0.11" +xcb = { version = "1.3", features = ["as-raw-xcb-connection"] } +as-raw-xcb-connection = "1" +#TODO: use these on all platforms +blade-graphics = { git = "https://github.com/kvark/blade", rev = "f35bc605154e210ab6190291235889b6ddad73f1" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "f35bc605154e210ab6190291235889b6ddad73f1" } +bytemuck = "1" diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 41505ec6783eb..d8447de65eef0 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(target_os = "macos"), allow(unused))] + use std::{ env, path::{Path, PathBuf}, @@ -6,10 +8,14 @@ use std::{ use cbindgen::Config; fn main() { + #[cfg(target_os = "macos")] generate_dispatch_bindings(); + #[cfg(target_os = "macos")] let header_path = generate_shader_bindings(); + #[cfg(target_os = "macos")] #[cfg(feature = "runtime_shaders")] emit_stitched_shaders(&header_path); + #[cfg(target_os = "macos")] #[cfg(not(feature = "runtime_shaders"))] compile_metal_shaders(&header_path); } diff --git a/crates/gpui/examples/hello_world.rs b/crates/gpui/examples/hello_world.rs index 736fd1445020b..d0578e66811a6 100644 --- a/crates/gpui/examples/hello_world.rs +++ b/crates/gpui/examples/hello_world.rs @@ -9,9 +9,12 @@ impl Render for HelloWorld { div() .flex() .bg(rgb(0x2e7d32)) - .size_full() + .size(Length::Definite(Pixels(300.0).into())) .justify_center() .items_center() + .shadow_lg() + .border() + .border_color(rgb(0x0000ff)) .text_xl() .text_color(rgb(0xffffff)) .child(format!("Hello, {}!", &self.text)) diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index cb97309d36770..32009e04db59b 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -7,6 +7,7 @@ use crate::{ StyleRefinement, Styled, UriOrPath, }; use futures::FutureExt; +#[cfg(target_os = "macos")] use media::core_video::CVImageBuffer; use util::ResultExt; @@ -21,6 +22,7 @@ pub enum ImageSource { Data(Arc), // TODO: move surface definitions into mac platform module /// A CoreVideo image buffer + #[cfg(target_os = "macos")] Surface(CVImageBuffer), } @@ -54,6 +56,7 @@ impl From> for ImageSource { } } +#[cfg(target_os = "macos")] impl From for ImageSource { fn from(value: CVImageBuffer) -> Self { Self::Surface(value) @@ -144,6 +147,7 @@ impl Element for Img { .log_err(); } + #[cfg(target_os = "macos")] ImageSource::Surface(surface) => { let size = size(surface.width().into(), surface.height().into()); let new_bounds = preserve_aspect_ratio(bounds, size); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 4fe5f621118fa..4fa2f525a7caf 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -6,8 +6,7 @@ //! ## Getting Started //! //! GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io. -//! You'll also need to use the latest version of stable rust and be on macOS. Add the following to your -//! Cargo.toml: +//! You'll also need to use the latest version of stable rust. Add the following to your Cargo.toml: //! //! ``` //! gpui = { git = "https://github.com/zed-industries/zed" } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 612a2711111b3..b7a76525796e2 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1,5 +1,7 @@ mod app_menu; mod keystroke; +#[cfg(target_os = "linux")] +mod linux; #[cfg(target_os = "macos")] mod mac; #[cfg(any(test, feature = "test-support"))] @@ -33,6 +35,8 @@ use uuid::Uuid; pub use app_menu::*; pub use keystroke::*; +#[cfg(target_os = "linux")] +pub(crate) use linux::*; #[cfg(target_os = "macos")] pub(crate) use mac::*; #[cfg(any(test, feature = "test-support"))] @@ -44,6 +48,10 @@ pub use util::SemanticVersion; pub(crate) fn current_platform() -> Rc { Rc::new(MacPlatform::new()) } +#[cfg(target_os = "linux")] +pub(crate) fn current_platform() -> Rc { + Rc::new(LinuxPlatform::new()) +} pub(crate) trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; @@ -298,6 +306,7 @@ pub(crate) trait PlatformAtlas: Send + Sync { pub(crate) struct AtlasTile { pub(crate) texture_id: AtlasTextureId, pub(crate) tile_id: TileId, + pub(crate) padding: u32, pub(crate) bounds: Bounds, } diff --git a/crates/gpui/src/platform/linux.rs b/crates/gpui/src/platform/linux.rs new file mode 100644 index 0000000000000..f76e791a56187 --- /dev/null +++ b/crates/gpui/src/platform/linux.rs @@ -0,0 +1,18 @@ +mod blade_atlas; +mod blade_belt; +mod blade_renderer; +mod dispatcher; +mod display; +mod platform; +mod text_system; +mod window; + +pub(crate) use blade_atlas::*; +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use platform::*; +pub(crate) use text_system::*; +pub(crate) use window::*; + +use blade_belt::*; +use blade_renderer::*; diff --git a/crates/gpui/src/platform/linux/blade_atlas.rs b/crates/gpui/src/platform/linux/blade_atlas.rs new file mode 100644 index 0000000000000..188a554dbe39d --- /dev/null +++ b/crates/gpui/src/platform/linux/blade_atlas.rs @@ -0,0 +1,361 @@ +use super::{BladeBelt, BladeBeltDescriptor}; +use crate::{ + AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, + Point, Size, +}; +use anyhow::Result; +use blade_graphics as gpu; +use collections::FxHashMap; +use etagere::BucketedAtlasAllocator; +use parking_lot::Mutex; +use std::{borrow::Cow, ops, sync::Arc}; + +pub(crate) const PATH_TEXTURE_FORMAT: gpu::TextureFormat = gpu::TextureFormat::R16Float; + +pub(crate) struct BladeAtlas(Mutex); + +struct PendingUpload { + id: AtlasTextureId, + bounds: Bounds, + data: gpu::BufferPiece, +} + +struct BladeAtlasState { + gpu: Arc, + upload_belt: BladeBelt, + storage: BladeAtlasStorage, + tiles_by_key: FxHashMap, + initializations: Vec, + uploads: Vec, +} + +impl BladeAtlasState { + fn destroy(&mut self) { + self.storage.destroy(&self.gpu); + self.upload_belt.destroy(&self.gpu); + } +} + +pub struct BladeTextureInfo { + pub size: gpu::Extent, + pub raw_view: gpu::TextureView, +} + +impl BladeAtlas { + pub(crate) fn new(gpu: &Arc) -> Self { + BladeAtlas(Mutex::new(BladeAtlasState { + gpu: Arc::clone(gpu), + upload_belt: BladeBelt::new(BladeBeltDescriptor { + memory: gpu::Memory::Upload, + min_chunk_size: 0x10000, + alignment: 64, // Vulkan `optimalBufferCopyOffsetAlignment` on Intel XE + }), + storage: BladeAtlasStorage::default(), + tiles_by_key: Default::default(), + initializations: Vec::new(), + uploads: Vec::new(), + })) + } + + pub(crate) fn destroy(&self) { + self.0.lock().destroy(); + } + + pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { + let mut lock = self.0.lock(); + let textures = &mut lock.storage[texture_kind]; + for texture in textures { + texture.clear(); + } + } + + pub fn allocate(&self, size: Size, texture_kind: AtlasTextureKind) -> AtlasTile { + let mut lock = self.0.lock(); + lock.allocate(size, texture_kind) + } + + pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) { + let mut lock = self.0.lock(); + lock.flush(gpu_encoder); + } + + pub fn after_frame(&self, sync_point: &gpu::SyncPoint) { + let mut lock = self.0.lock(); + lock.upload_belt.flush(sync_point); + } + + pub fn get_texture_info(&self, id: AtlasTextureId) -> BladeTextureInfo { + let lock = self.0.lock(); + let texture = &lock.storage[id]; + let size = texture.allocator.size(); + BladeTextureInfo { + size: gpu::Extent { + width: size.width as u32, + height: size.height as u32, + depth: 1, + }, + raw_view: texture.raw_view, + } + } +} + +impl PlatformAtlas for BladeAtlas { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> Result<(Size, Cow<'a, [u8]>)>, + ) -> Result { + let mut lock = self.0.lock(); + if let Some(tile) = lock.tiles_by_key.get(key) { + Ok(tile.clone()) + } else { + let (size, bytes) = build()?; + let tile = lock.allocate(size, key.texture_kind()); + lock.upload_texture(tile.texture_id, tile.bounds, &bytes); + lock.tiles_by_key.insert(key.clone(), tile.clone()); + Ok(tile) + } + } +} + +impl BladeAtlasState { + fn allocate(&mut self, size: Size, texture_kind: AtlasTextureKind) -> AtlasTile { + let textures = &mut self.storage[texture_kind]; + textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + .unwrap_or_else(|| { + let texture = self.push_texture(size, texture_kind); + texture.allocate(size).unwrap() + }) + } + + fn push_texture( + &mut self, + min_size: Size, + kind: AtlasTextureKind, + ) -> &mut BladeAtlasTexture { + const DEFAULT_ATLAS_SIZE: Size = Size { + width: DevicePixels(1024), + height: DevicePixels(1024), + }; + + let size = min_size.max(&DEFAULT_ATLAS_SIZE); + let format; + let usage; + match kind { + AtlasTextureKind::Monochrome => { + format = gpu::TextureFormat::R8Unorm; + usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE; + } + AtlasTextureKind::Polychrome => { + format = gpu::TextureFormat::Bgra8Unorm; + usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE; + } + AtlasTextureKind::Path => { + format = PATH_TEXTURE_FORMAT; + usage = gpu::TextureUsage::COPY + | gpu::TextureUsage::RESOURCE + | gpu::TextureUsage::TARGET; + } + } + + let raw = self.gpu.create_texture(gpu::TextureDesc { + name: "atlas", + format, + size: gpu::Extent { + width: size.width.into(), + height: size.height.into(), + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + dimension: gpu::TextureDimension::D2, + usage, + }); + let raw_view = self.gpu.create_texture_view(gpu::TextureViewDesc { + name: "", + texture: raw, + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }); + + let textures = &mut self.storage[kind]; + let atlas_texture = BladeAtlasTexture { + id: AtlasTextureId { + index: textures.len() as u32, + kind, + }, + allocator: etagere::BucketedAtlasAllocator::new(size.into()), + format, + raw, + raw_view, + }; + + self.initializations.push(atlas_texture.id); + textures.push(atlas_texture); + textures.last_mut().unwrap() + } + + fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds, bytes: &[u8]) { + let data = self.upload_belt.alloc_data(bytes, &self.gpu); + self.uploads.push(PendingUpload { id, bounds, data }); + } + + fn flush(&mut self, encoder: &mut gpu::CommandEncoder) { + for id in self.initializations.drain(..) { + let texture = &self.storage[id]; + encoder.init_texture(texture.raw); + } + + let mut transfers = encoder.transfer(); + for upload in self.uploads.drain(..) { + let texture = &self.storage[upload.id]; + transfers.copy_buffer_to_texture( + upload.data, + upload.bounds.size.width.to_bytes(texture.bytes_per_pixel()), + gpu::TexturePiece { + texture: texture.raw, + mip_level: 0, + array_layer: 0, + origin: [ + upload.bounds.origin.x.into(), + upload.bounds.origin.y.into(), + 0, + ], + }, + gpu::Extent { + width: upload.bounds.size.width.into(), + height: upload.bounds.size.height.into(), + depth: 1, + }, + ); + } + } +} + +#[derive(Default)] +struct BladeAtlasStorage { + monochrome_textures: Vec, + polychrome_textures: Vec, + path_textures: Vec, +} + +impl ops::Index for BladeAtlasStorage { + type Output = Vec; + fn index(&self, kind: AtlasTextureKind) -> &Self::Output { + match kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, + } + } +} + +impl ops::IndexMut for BladeAtlasStorage { + fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output { + match kind { + crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + crate::AtlasTextureKind::Path => &mut self.path_textures, + } + } +} + +impl ops::Index for BladeAtlasStorage { + type Output = BladeAtlasTexture; + fn index(&self, id: AtlasTextureId) -> &Self::Output { + let textures = match id.kind { + crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, + crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, + }; + &textures[id.index as usize] + } +} + +impl BladeAtlasStorage { + fn destroy(&mut self, gpu: &gpu::Context) { + for mut texture in self.monochrome_textures.drain(..) { + texture.destroy(gpu); + } + for mut texture in self.polychrome_textures.drain(..) { + texture.destroy(gpu); + } + for mut texture in self.path_textures.drain(..) { + texture.destroy(gpu); + } + } +} + +struct BladeAtlasTexture { + id: AtlasTextureId, + allocator: BucketedAtlasAllocator, + raw: gpu::Texture, + raw_view: gpu::TextureView, + format: gpu::TextureFormat, +} + +impl BladeAtlasTexture { + fn clear(&mut self) { + self.allocator.clear(); + } + + fn allocate(&mut self, size: Size) -> Option { + let allocation = self.allocator.allocate(size.into())?; + let tile = AtlasTile { + texture_id: self.id, + tile_id: allocation.id.into(), + padding: 0, + bounds: Bounds { + origin: allocation.rectangle.min.into(), + size, + }, + }; + Some(tile) + } + + fn destroy(&mut self, gpu: &gpu::Context) { + gpu.destroy_texture(self.raw); + gpu.destroy_texture_view(self.raw_view); + } + + fn bytes_per_pixel(&self) -> u8 { + self.format.block_info().size + } +} + +impl From> for etagere::Size { + fn from(size: Size) -> Self { + etagere::Size::new(size.width.into(), size.height.into()) + } +} + +impl From for Point { + fn from(value: etagere::Point) -> Self { + Point { + x: DevicePixels::from(value.x), + y: DevicePixels::from(value.y), + } + } +} + +impl From for Size { + fn from(size: etagere::Size) -> Self { + Size { + width: DevicePixels::from(size.width), + height: DevicePixels::from(size.height), + } + } +} + +impl From for Bounds { + fn from(rectangle: etagere::Rectangle) -> Self { + Bounds { + origin: rectangle.min.into(), + size: rectangle.size().into(), + } + } +} diff --git a/crates/gpui/src/platform/linux/blade_belt.rs b/crates/gpui/src/platform/linux/blade_belt.rs new file mode 100644 index 0000000000000..2562097cbb2a4 --- /dev/null +++ b/crates/gpui/src/platform/linux/blade_belt.rs @@ -0,0 +1,100 @@ +use blade_graphics as gpu; +use std::mem; + +struct ReusableBuffer { + raw: gpu::Buffer, + size: u64, +} + +pub struct BladeBeltDescriptor { + pub memory: gpu::Memory, + pub min_chunk_size: u64, + pub alignment: u64, +} + +/// A belt of buffers, used by the BladeAtlas to cheaply +/// find staging space for uploads. +pub struct BladeBelt { + desc: BladeBeltDescriptor, + buffers: Vec<(ReusableBuffer, gpu::SyncPoint)>, + active: Vec<(ReusableBuffer, u64)>, +} + +impl BladeBelt { + pub fn new(desc: BladeBeltDescriptor) -> Self { + assert_ne!(desc.alignment, 0); + Self { + desc, + buffers: Vec::new(), + active: Vec::new(), + } + } + + pub fn destroy(&mut self, gpu: &gpu::Context) { + for (buffer, _) in self.buffers.drain(..) { + gpu.destroy_buffer(buffer.raw); + } + for (buffer, _) in self.active.drain(..) { + gpu.destroy_buffer(buffer.raw); + } + } + + pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece { + for &mut (ref rb, ref mut offset) in self.active.iter_mut() { + let aligned = offset.next_multiple_of(self.desc.alignment); + if aligned + size <= rb.size { + let piece = rb.raw.at(aligned); + *offset = aligned + size; + return piece; + } + } + + let index_maybe = self + .buffers + .iter() + .position(|&(ref rb, ref sp)| size <= rb.size && gpu.wait_for(sp, 0)); + if let Some(index) = index_maybe { + let (rb, _) = self.buffers.remove(index); + let piece = rb.raw.into(); + self.active.push((rb, size)); + return piece; + } + + let chunk_index = self.buffers.len() + self.active.len(); + let chunk_size = size.max(self.desc.min_chunk_size); + let chunk = gpu.create_buffer(gpu::BufferDesc { + name: &format!("chunk-{}", chunk_index), + size: chunk_size, + memory: self.desc.memory, + }); + let rb = ReusableBuffer { + raw: chunk, + size: chunk_size, + }; + self.active.push((rb, size)); + chunk.into() + } + + //todo!(linux): enforce T: bytemuck::Zeroable + pub fn alloc_data(&mut self, data: &[T], gpu: &gpu::Context) -> gpu::BufferPiece { + assert!(!data.is_empty()); + let type_alignment = mem::align_of::() as u64; + debug_assert_eq!( + self.desc.alignment % type_alignment, + 0, + "Type alignment {} is too big", + type_alignment + ); + let total_bytes = data.len() * mem::size_of::(); + let bp = self.alloc(total_bytes as u64, gpu); + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, bp.data(), total_bytes); + } + bp + } + + pub fn flush(&mut self, sp: &gpu::SyncPoint) { + self.buffers + .extend(self.active.drain(..).map(|(rb, _)| (rb, sp.clone()))); + } +} diff --git a/crates/gpui/src/platform/linux/blade_renderer.rs b/crates/gpui/src/platform/linux/blade_renderer.rs new file mode 100644 index 0000000000000..128b918ef7811 --- /dev/null +++ b/crates/gpui/src/platform/linux/blade_renderer.rs @@ -0,0 +1,506 @@ +// Doing `if let` gives you nice scoping with passes/encoders +#![allow(irrefutable_let_patterns)] + +use super::{BladeBelt, BladeBeltDescriptor}; +use crate::{ + AtlasTextureKind, AtlasTile, BladeAtlas, Bounds, ContentMask, Hsla, MonochromeSprite, Path, + PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, + Underline, PATH_TEXTURE_FORMAT, +}; +use bytemuck::{Pod, Zeroable}; +use collections::HashMap; + +use blade_graphics as gpu; +use std::{mem, sync::Arc}; + +const SURFACE_FRAME_COUNT: u32 = 3; +const MAX_FRAME_TIME_MS: u32 = 1000; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct GlobalParams { + viewport_size: [f32; 2], + pad: [u32; 2], +} + +#[derive(blade_macros::ShaderData)] +struct ShaderQuadsData { + globals: GlobalParams, + b_quads: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderShadowsData { + globals: GlobalParams, + b_shadows: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPathRasterizationData { + globals: GlobalParams, + b_path_vertices: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPathsData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_path_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderUnderlinesData { + globals: GlobalParams, + b_underlines: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderMonoSpritesData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_mono_sprites: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPolySpritesData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, + b_poly_sprites: gpu::BufferPiece, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +struct PathSprite { + bounds: Bounds, + color: Hsla, + tile: AtlasTile, +} + +struct BladePipelines { + quads: gpu::RenderPipeline, + shadows: gpu::RenderPipeline, + path_rasterization: gpu::RenderPipeline, + paths: gpu::RenderPipeline, + underlines: gpu::RenderPipeline, + mono_sprites: gpu::RenderPipeline, + poly_sprites: gpu::RenderPipeline, +} + +impl BladePipelines { + fn new(gpu: &gpu::Context, surface_format: gpu::TextureFormat) -> Self { + use gpu::ShaderData as _; + + let shader = gpu.create_shader(gpu::ShaderDesc { + source: include_str!("shaders.wgsl"), + }); + shader.check_struct_size::(); + shader.check_struct_size::(); + assert_eq!( + mem::size_of::>(), + shader.get_struct_size("PathVertex") as usize, + ); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + shader.check_struct_size::(); + + Self { + quads: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "quads", + data_layouts: &[&ShaderQuadsData::layout()], + vertex: shader.at("vs_quad"), + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_quad"), + color_targets: &[gpu::ColorTargetState { + format: surface_format, + blend: Some(gpu::BlendState::ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + }), + shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "shadows", + data_layouts: &[&ShaderShadowsData::layout()], + vertex: shader.at("vs_shadow"), + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_shadow"), + color_targets: &[gpu::ColorTargetState { + format: surface_format, + blend: Some(gpu::BlendState::ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + }), + path_rasterization: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "path_rasterization", + data_layouts: &[&ShaderPathRasterizationData::layout()], + vertex: shader.at("vs_path_rasterization"), + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_path_rasterization"), + color_targets: &[gpu::ColorTargetState { + format: PATH_TEXTURE_FORMAT, + blend: Some(gpu::BlendState::ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + }), + paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "paths", + data_layouts: &[&ShaderPathsData::layout()], + vertex: shader.at("vs_path"), + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_path"), + color_targets: &[gpu::ColorTargetState { + format: surface_format, + blend: Some(gpu::BlendState::ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + }), + underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "underlines", + data_layouts: &[&ShaderUnderlinesData::layout()], + vertex: shader.at("vs_underline"), + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_underline"), + color_targets: &[gpu::ColorTargetState { + format: surface_format, + blend: Some(gpu::BlendState::ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + }), + mono_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "mono-sprites", + data_layouts: &[&ShaderMonoSpritesData::layout()], + vertex: shader.at("vs_mono_sprite"), + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_mono_sprite"), + color_targets: &[gpu::ColorTargetState { + format: surface_format, + blend: Some(gpu::BlendState::ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + }), + poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "poly-sprites", + data_layouts: &[&ShaderPolySpritesData::layout()], + vertex: shader.at("vs_poly_sprite"), + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + fragment: shader.at("fs_poly_sprite"), + color_targets: &[gpu::ColorTargetState { + format: surface_format, + blend: Some(gpu::BlendState::ALPHA_BLENDING), + write_mask: gpu::ColorWrites::default(), + }], + }), + } + } +} + +pub struct BladeRenderer { + gpu: Arc, + command_encoder: gpu::CommandEncoder, + last_sync_point: Option, + pipelines: BladePipelines, + instance_belt: BladeBelt, + viewport_size: gpu::Extent, + path_tiles: HashMap, + atlas: Arc, + atlas_sampler: gpu::Sampler, +} + +impl BladeRenderer { + pub fn new(gpu: Arc, size: gpu::Extent) -> Self { + let surface_format = gpu.resize(gpu::SurfaceConfig { + size, + usage: gpu::TextureUsage::TARGET, + frame_count: SURFACE_FRAME_COUNT, + }); + let command_encoder = gpu.create_command_encoder(gpu::CommandEncoderDesc { + name: "main", + buffer_count: 2, + }); + let pipelines = BladePipelines::new(&gpu, surface_format); + let instance_belt = BladeBelt::new(BladeBeltDescriptor { + memory: gpu::Memory::Shared, + min_chunk_size: 0x1000, + alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe + }); + let atlas = Arc::new(BladeAtlas::new(&gpu)); + let atlas_sampler = gpu.create_sampler(gpu::SamplerDesc { + name: "atlas", + mag_filter: gpu::FilterMode::Linear, + min_filter: gpu::FilterMode::Linear, + ..Default::default() + }); + + Self { + gpu, + command_encoder, + last_sync_point: None, + pipelines, + instance_belt, + viewport_size: size, + path_tiles: HashMap::default(), + atlas, + atlas_sampler, + } + } + + fn wait_for_gpu(&mut self) { + if let Some(last_sp) = self.last_sync_point.take() { + if !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) { + panic!("GPU hung"); + } + } + } + + pub fn destroy(&mut self) { + self.wait_for_gpu(); + self.atlas.destroy(); + self.instance_belt.destroy(&self.gpu); + self.gpu.destroy_command_encoder(&mut self.command_encoder); + } + + pub fn resize(&mut self, size: gpu::Extent) { + self.wait_for_gpu(); + self.gpu.resize(gpu::SurfaceConfig { + size, + usage: gpu::TextureUsage::TARGET, + frame_count: SURFACE_FRAME_COUNT, + }); + self.viewport_size = size; + } + + pub fn viewport_size(&self) -> gpu::Extent { + self.viewport_size + } + + pub fn atlas(&self) -> &Arc { + &self.atlas + } + + fn rasterize_paths(&mut self, paths: &[Path]) { + self.path_tiles.clear(); + let mut vertices_by_texture_id = HashMap::default(); + + for path in paths { + let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds); + let tile = self + .atlas + .allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path); + vertices_by_texture_id + .entry(tile.texture_id) + .or_insert(Vec::new()) + .extend(path.vertices.iter().map(|vertex| PathVertex { + xy_position: vertex.xy_position - clipped_bounds.origin + + tile.bounds.origin.map(Into::into), + st_position: vertex.st_position, + content_mask: ContentMask { + bounds: tile.bounds.map(Into::into), + }, + })); + self.path_tiles.insert(path.id, tile); + } + + for (texture_id, vertices) in vertices_by_texture_id { + let tex_info = self.atlas.get_texture_info(texture_id); + let globals = GlobalParams { + viewport_size: [tex_info.size.width as f32, tex_info.size.height as f32], + pad: [0; 2], + }; + + let vertex_buf = self.instance_belt.alloc_data(&vertices, &self.gpu); + let mut pass = self.command_encoder.render(gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: tex_info.raw_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }); + + let mut encoder = pass.with(&self.pipelines.path_rasterization); + encoder.bind( + 0, + &ShaderPathRasterizationData { + globals, + b_path_vertices: vertex_buf, + }, + ); + encoder.draw(0, vertices.len() as u32, 0, 1); + } + } + + pub fn draw(&mut self, scene: &Scene) { + let frame = self.gpu.acquire_frame(); + self.command_encoder.start(); + self.command_encoder.init_texture(frame.texture()); + + self.atlas.before_frame(&mut self.command_encoder); + self.rasterize_paths(scene.paths()); + + let globals = GlobalParams { + viewport_size: [ + self.viewport_size.width as f32, + self.viewport_size.height as f32, + ], + pad: [0; 2], + }; + + if let mut pass = self.command_encoder.render(gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }) { + for batch in scene.batches() { + match batch { + PrimitiveBatch::Quads(quads) => { + let instance_buf = self.instance_belt.alloc_data(quads, &self.gpu); + let mut encoder = pass.with(&self.pipelines.quads); + encoder.bind( + 0, + &ShaderQuadsData { + globals, + b_quads: instance_buf, + }, + ); + encoder.draw(0, 4, 0, quads.len() as u32); + } + PrimitiveBatch::Shadows(shadows) => { + let instance_buf = self.instance_belt.alloc_data(shadows, &self.gpu); + let mut encoder = pass.with(&self.pipelines.shadows); + encoder.bind( + 0, + &ShaderShadowsData { + globals, + b_shadows: instance_buf, + }, + ); + encoder.draw(0, 4, 0, shadows.len() as u32); + } + PrimitiveBatch::Paths(paths) => { + let mut encoder = pass.with(&self.pipelines.paths); + //todo!(linux): group by texture ID + for path in paths { + let tile = &self.path_tiles[&path.id]; + let tex_info = self.atlas.get_texture_info(tile.texture_id); + let origin = path.bounds.intersect(&path.content_mask.bounds).origin; + let sprites = [PathSprite { + bounds: Bounds { + origin: origin.map(|p| p.floor()), + size: tile.bounds.size.map(Into::into), + }, + color: path.color, + tile: (*tile).clone(), + }]; + + let instance_buf = self.instance_belt.alloc_data(&sprites, &self.gpu); + encoder.bind( + 0, + &ShaderPathsData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_path_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + } + PrimitiveBatch::Underlines(underlines) => { + let instance_buf = self.instance_belt.alloc_data(underlines, &self.gpu); + let mut encoder = pass.with(&self.pipelines.underlines); + encoder.bind( + 0, + &ShaderUnderlinesData { + globals, + b_underlines: instance_buf, + }, + ); + encoder.draw(0, 4, 0, underlines.len() as u32); + } + PrimitiveBatch::MonochromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = self.instance_belt.alloc_data(&sprites, &self.gpu); + let mut encoder = pass.with(&self.pipelines.mono_sprites); + encoder.bind( + 0, + &ShaderMonoSpritesData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_mono_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::PolychromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = self.instance_belt.alloc_data(&sprites, &self.gpu); + let mut encoder = pass.with(&self.pipelines.poly_sprites); + encoder.bind( + 0, + &ShaderPolySpritesData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_poly_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::Surfaces { .. } => { + unimplemented!() + } + } + } + } + + self.command_encoder.present(frame); + let sync_point = self.gpu.submit(&mut self.command_encoder); + + self.instance_belt.flush(&sync_point); + self.atlas.after_frame(&sync_point); + self.atlas.clear_textures(AtlasTextureKind::Path); + + self.wait_for_gpu(); + self.last_sync_point = Some(sync_point); + } +} diff --git a/crates/gpui/src/platform/linux/dispatcher.rs b/crates/gpui/src/platform/linux/dispatcher.rs new file mode 100644 index 0000000000000..108b8ed354e4b --- /dev/null +++ b/crates/gpui/src/platform/linux/dispatcher.rs @@ -0,0 +1,134 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::{PlatformDispatcher, TaskLabel}; +use async_task::Runnable; +use parking::{Parker, Unparker}; +use parking_lot::Mutex; +use std::{ + panic, + sync::Arc, + thread, + time::{Duration, Instant}, +}; +use xcb::x; + +pub(crate) struct LinuxDispatcher { + xcb_connection: Arc, + x_listener_window: x::Window, + parker: Mutex, + timed_tasks: Mutex>, + main_sender: flume::Sender, + background_sender: flume::Sender, + _background_thread: thread::JoinHandle<()>, + main_thread_id: thread::ThreadId, +} + +impl LinuxDispatcher { + pub fn new( + main_sender: flume::Sender, + xcb_connection: &Arc, + x_root_index: i32, + ) -> Self { + let x_listener_window = xcb_connection.generate_id(); + let screen = xcb_connection + .get_setup() + .roots() + .nth(x_root_index as usize) + .unwrap(); + xcb_connection.send_request(&x::CreateWindow { + depth: 0, + wid: x_listener_window, + parent: screen.root(), + x: 0, + y: 0, + width: 1, + height: 1, + border_width: 0, + class: x::WindowClass::InputOnly, + visual: screen.root_visual(), + value_list: &[], + }); + + let (background_sender, background_receiver) = flume::unbounded::(); + let background_thread = thread::spawn(move || { + for runnable in background_receiver { + let _ignore_panic = panic::catch_unwind(|| runnable.run()); + } + }); + LinuxDispatcher { + xcb_connection: Arc::clone(xcb_connection), + x_listener_window, + parker: Mutex::new(Parker::new()), + timed_tasks: Mutex::new(Vec::new()), + main_sender, + background_sender, + _background_thread: background_thread, + main_thread_id: thread::current().id(), + } + } +} + +impl Drop for LinuxDispatcher { + fn drop(&mut self) { + self.xcb_connection.send_request(&x::DestroyWindow { + window: self.x_listener_window, + }); + } +} + +impl PlatformDispatcher for LinuxDispatcher { + fn is_main_thread(&self) -> bool { + thread::current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: Runnable, _: Option) { + self.background_sender.send(runnable).unwrap(); + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + self.main_sender.send(runnable).unwrap(); + // Send a message to the invisible window, forcing + // the main loop to wake up and dispatch the runnable. + self.xcb_connection.send_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(self.x_listener_window), + event_mask: x::EventMask::NO_EVENT, + event: &x::VisibilityNotifyEvent::new( + self.x_listener_window, + x::Visibility::Unobscured, + ), + }); + self.xcb_connection.flush().unwrap(); + } + + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + let moment = Instant::now() + duration; + let mut timed_tasks = self.timed_tasks.lock(); + timed_tasks.push((moment, runnable)); + timed_tasks.sort_unstable_by(|&(ref a, _), &(ref b, _)| b.cmp(a)); + } + + fn tick(&self, background_only: bool) -> bool { + let mut timed_tasks = self.timed_tasks.lock(); + let old_count = timed_tasks.len(); + while let Some(&(moment, _)) = timed_tasks.last() { + if moment <= Instant::now() { + let (_, runnable) = timed_tasks.pop().unwrap(); + runnable.run(); + } else { + break; + } + } + timed_tasks.len() != old_count + } + + fn park(&self) { + self.parker.lock().park() + } + + fn unparker(&self) -> Unparker { + self.parker.lock().unparker() + } +} diff --git a/crates/gpui/src/platform/linux/display.rs b/crates/gpui/src/platform/linux/display.rs new file mode 100644 index 0000000000000..cdca59c435549 --- /dev/null +++ b/crates/gpui/src/platform/linux/display.rs @@ -0,0 +1,41 @@ +use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Size}; +use anyhow::Result; +use uuid::Uuid; + +#[derive(Debug)] +pub(crate) struct LinuxDisplay { + x_screen_index: i32, + bounds: Bounds, + uuid: Uuid, +} + +impl LinuxDisplay { + pub(crate) fn new(xc: &xcb::Connection, x_screen_index: i32) -> Self { + let screen = xc.get_setup().roots().nth(x_screen_index as usize).unwrap(); + Self { + x_screen_index, + bounds: Bounds { + origin: Default::default(), + size: Size { + width: GlobalPixels(screen.width_in_pixels() as f32), + height: GlobalPixels(screen.height_in_pixels() as f32), + }, + }, + uuid: Uuid::from_bytes([0; 16]), + } + } +} + +impl PlatformDisplay for LinuxDisplay { + fn id(&self) -> DisplayId { + DisplayId(self.x_screen_index as u32) + } + + fn uuid(&self) -> Result { + Ok(self.uuid) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs new file mode 100644 index 0000000000000..30d2b66bf013c --- /dev/null +++ b/crates/gpui/src/platform/linux/platform.rs @@ -0,0 +1,381 @@ +#![allow(unused)] + +use crate::{ + Action, AnyWindowHandle, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DisplayId, + ForegroundExecutor, Keymap, LinuxDispatcher, LinuxDisplay, LinuxTextSystem, LinuxWindow, + LinuxWindowState, Menu, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, + PlatformTextSystem, PlatformWindow, Point, Result, SemanticVersion, Size, Task, WindowOptions, +}; + +use async_task::Runnable; +use collections::{HashMap, HashSet}; +use futures::channel::oneshot; +use parking_lot::Mutex; + +use std::{ + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::Duration, +}; +use time::UtcOffset; +use xcb::{x, Xid as _}; + +xcb::atoms_struct! { + #[derive(Debug)] + pub(crate) struct XcbAtoms { + pub wm_protocols => b"WM_PROTOCOLS", + pub wm_del_window => b"WM_DELETE_WINDOW", + wm_state => b"_NET_WM_STATE", + wm_state_maxv => b"_NET_WM_STATE_MAXIMIZED_VERT", + wm_state_maxh => b"_NET_WM_STATE_MAXIMIZED_HORZ", + } +} + +#[derive(Default)] +struct Callbacks { + open_urls: Option)>>, + become_active: Option>, + resign_active: Option>, + quit: Option>, + reopen: Option>, + event: Option bool>>, + app_menu_action: Option>, + will_open_app_menu: Option>, + validate_app_menu_command: Option bool>>, +} + +pub(crate) struct LinuxPlatform { + xcb_connection: Arc, + x_root_index: i32, + atoms: XcbAtoms, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + main_receiver: flume::Receiver, + text_system: Arc, + callbacks: Mutex, + state: Mutex, +} + +pub(crate) struct LinuxPlatformState { + quit_requested: bool, + windows: HashMap>, +} + +impl Default for LinuxPlatform { + fn default() -> Self { + Self::new() + } +} + +impl LinuxPlatform { + pub(crate) fn new() -> Self { + let (xcb_connection, x_root_index) = xcb::Connection::connect(None).unwrap(); + let atoms = XcbAtoms::intern_all(&xcb_connection).unwrap(); + + let xcb_connection = Arc::new(xcb_connection); + let (main_sender, main_receiver) = flume::unbounded::(); + let dispatcher = Arc::new(LinuxDispatcher::new( + main_sender, + &xcb_connection, + x_root_index, + )); + + Self { + xcb_connection, + x_root_index, + atoms, + background_executor: BackgroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher.clone()), + main_receiver, + text_system: Arc::new(LinuxTextSystem::new()), + callbacks: Mutex::new(Callbacks::default()), + state: Mutex::new(LinuxPlatformState { + quit_requested: false, + windows: HashMap::default(), + }), + } + } +} + +impl Platform for LinuxPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.text_system.clone() + } + + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); + //Note: here and below, don't keep the lock() open when calling + // into window functions as they may invoke callbacks that need + // to immediately access the platform (self). + while !self.state.lock().quit_requested { + let event = self.xcb_connection.wait_for_event().unwrap(); + match event { + xcb::Event::X(x::Event::ClientMessage(ev)) => { + if let x::ClientMessageData::Data32([atom, ..]) = ev.data() { + if atom == self.atoms.wm_del_window.resource_id() { + // window "x" button clicked by user, we gracefully exit + let window = self.state.lock().windows.remove(&ev.window()).unwrap(); + window.destroy(); + if self.state.lock().windows.is_empty() { + if let Some(ref mut fun) = self.callbacks.lock().quit { + fun(); + } + } + } + } + } + xcb::Event::X(x::Event::Expose(ev)) => { + let window = { + let state = self.state.lock(); + Arc::clone(&state.windows[&ev.window()]) + }; + window.expose(); + } + xcb::Event::X(x::Event::ConfigureNotify(ev)) => { + let bounds = Bounds { + origin: Point { + x: ev.x().into(), + y: ev.y().into(), + }, + size: Size { + width: ev.width().into(), + height: ev.height().into(), + }, + }; + let window = { + let state = self.state.lock(); + Arc::clone(&state.windows[&ev.window()]) + }; + window.configure(bounds) + } + _ => {} + } + + if let Ok(runnable) = self.main_receiver.try_recv() { + runnable.run(); + } + } + } + + fn quit(&self) { + self.state.lock().quit_requested = true; + } + + //todo!(linux) + fn restart(&self) {} + + //todo!(linux) + fn activate(&self, ignoring_other_apps: bool) {} + + //todo!(linux) + fn hide(&self) {} + + //todo!(linux) + fn hide_other_apps(&self) {} + + //todo!(linux) + fn unhide_other_apps(&self) {} + + fn displays(&self) -> Vec> { + let setup = self.xcb_connection.get_setup(); + setup + .roots() + .enumerate() + .map(|(root_id, _)| { + Rc::new(LinuxDisplay::new(&self.xcb_connection, root_id as i32)) + as Rc + }) + .collect() + } + + fn display(&self, id: DisplayId) -> Option> { + Some(Rc::new(LinuxDisplay::new( + &self.xcb_connection, + id.0 as i32, + ))) + } + + //todo!(linux) + fn active_window(&self) -> Option { + None + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowOptions, + ) -> Box { + let x_window = self.xcb_connection.generate_id(); + + let window_ptr = Arc::new(LinuxWindowState::new( + options, + &self.xcb_connection, + self.x_root_index, + x_window, + &self.atoms, + )); + + self.state + .lock() + .windows + .insert(x_window, Arc::clone(&window_ptr)); + Box::new(LinuxWindow(window_ptr)) + } + + fn set_display_link_output_callback( + &self, + display_id: DisplayId, + callback: Box, + ) { + log::warn!("unimplemented: set_display_link_output_callback"); + } + + fn start_display_link(&self, display_id: DisplayId) { + unimplemented!() + } + + fn stop_display_link(&self, display_id: DisplayId) { + unimplemented!() + } + + fn open_url(&self, url: &str) { + unimplemented!() + } + + fn on_open_urls(&self, callback: Box)>) { + self.callbacks.lock().open_urls = Some(callback); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + unimplemented!() + } + + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + unimplemented!() + } + + fn reveal_path(&self, path: &Path) { + unimplemented!() + } + + fn on_become_active(&self, callback: Box) { + self.callbacks.lock().become_active = Some(callback); + } + + fn on_resign_active(&self, callback: Box) { + self.callbacks.lock().resign_active = Some(callback); + } + + fn on_quit(&self, callback: Box) { + self.callbacks.lock().quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.callbacks.lock().reopen = Some(callback); + } + + fn on_event(&self, callback: Box bool>) { + self.callbacks.lock().event = Some(callback); + } + + fn on_app_menu_action(&self, callback: Box) { + self.callbacks.lock().app_menu_action = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.callbacks.lock().will_open_app_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.callbacks.lock().validate_app_menu_command = Some(callback); + } + + fn os_name(&self) -> &'static str { + "Linux" + } + + fn double_click_interval(&self) -> Duration { + Duration::default() + } + + fn os_version(&self) -> Result { + Ok(SemanticVersion { + major: 1, + minor: 0, + patch: 0, + }) + } + + fn app_version(&self) -> Result { + Ok(SemanticVersion { + major: 1, + minor: 0, + patch: 0, + }) + } + + fn app_path(&self) -> Result { + unimplemented!() + } + + //todo!(linux) + fn set_menus(&self, menus: Vec, keymap: &Keymap) {} + + fn local_timezone(&self) -> UtcOffset { + UtcOffset::UTC + } + + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + unimplemented!() + } + + //todo!(linux) + fn set_cursor_style(&self, style: CursorStyle) {} + + //todo!(linux) + fn should_auto_hide_scrollbars(&self) -> bool {} + + //todo!(linux) + fn write_to_clipboard(&self, item: ClipboardItem) {} + + //todo!(linux) + fn read_from_clipboard(&self) -> Option { + None + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + unimplemented!() + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + unimplemented!() + } + + fn delete_credentials(&self, url: &str) -> Task> { + unimplemented!() + } +} + +#[cfg(test)] +mod tests { + use crate::ClipboardItem; + + use super::*; + + fn build_platform() -> LinuxPlatform { + let platform = LinuxPlatform::new(); + platform + } +} diff --git a/crates/gpui/src/platform/linux/shaders.wgsl b/crates/gpui/src/platform/linux/shaders.wgsl new file mode 100644 index 0000000000000..8a9fd31e96413 --- /dev/null +++ b/crates/gpui/src/platform/linux/shaders.wgsl @@ -0,0 +1,569 @@ +struct Globals { + viewport_size: vec2, + pad: vec2, +} + +var globals: Globals; +var t_sprite: texture_2d; +var s_sprite: sampler; + +const M_PI_F: f32 = 3.1415926; +const GRAYSCALE_FACTORS: vec3 = vec3(0.2126, 0.7152, 0.0722); + +struct ViewId { + lo: u32, + hi: u32, +} + +struct Bounds { + origin: vec2, + size: vec2, +} +struct Corners { + top_left: f32, + top_right: f32, + bottom_right: f32, + bottom_left: f32, +} +struct Edges { + top: f32, + right: f32, + bottom: f32, + left: f32, +} +struct Hsla { + h: f32, + s: f32, + l: f32, + a: f32, +} + +struct AtlasTextureId { + index: u32, + kind: u32, +} + +struct AtlasBounds { + origin: vec2, + size: vec2, +} +struct AtlasTile { + texture_id: AtlasTextureId, + tile_id: u32, + padding: u32, + bounds: AtlasBounds, +} + +fn to_device_position_impl(position: vec2) -> vec4 { + let device_position = position / globals.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0); + return vec4(device_position, 0.0, 1.0); +} + +fn to_device_position(unit_vertex: vec2, bounds: Bounds) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + return to_device_position_impl(position); +} + +fn to_tile_position(unit_vertex: vec2, tile: AtlasTile) -> vec2 { + let atlas_size = vec2(textureDimensions(t_sprite, 0)); + return (vec2(tile.bounds.origin) + unit_vertex * vec2(tile.bounds.size)) / atlas_size; +} + +fn distance_from_clip_rect_impl(position: vec2, clip_bounds: Bounds) -> vec4 { + let tl = position - clip_bounds.origin; + let br = clip_bounds.origin + clip_bounds.size - position; + return vec4(tl.x, br.x, tl.y, br.y); +} + +fn distance_from_clip_rect(unit_vertex: vec2, bounds: Bounds, clip_bounds: Bounds) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + return distance_from_clip_rect_impl(position, clip_bounds); +} + +fn hsla_to_rgba(hsla: Hsla) -> vec4 { + let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range + let s = hsla.s; + let l = hsla.l; + let a = hsla.a; + + let c = (1.0 - abs(2.0 * l - 1.0)) * s; + let x = c * (1.0 - abs(h % 2.0 - 1.0)); + let m = l - c / 2.0; + + var color = vec4(m, m, m, a); + + if (h >= 0.0 && h < 1.0) { + color.r += c; + color.g += x; + } else if (h >= 1.0 && h < 2.0) { + color.r += x; + color.g += c; + } else if (h >= 2.0 && h < 3.0) { + color.g += c; + color.b += x; + } else if (h >= 3.0 && h < 4.0) { + color.g += x; + color.b += c; + } else if (h >= 4.0 && h < 5.0) { + color.r += x; + color.b += c; + } else { + color.r += c; + color.b += x; + } + + return color; +} + +fn over(below: vec4, above: vec4) -> vec4 { + let alpha = above.a + below.a * (1.0 - above.a); + let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; + return vec4(color, alpha); +} + +// A standard gaussian function, used for weighting samples +fn gaussian(x: f32, sigma: f32) -> f32{ + return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +fn erf(v: vec2) -> vec2 { + let s = sign(v); + let a = abs(v); + let r1 = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + let r2 = r1 * r1; + return s - s / (r2 * r2); +} + +fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2) -> f32 { + let delta = min(half_size.y - corner - abs(y), 0.0); + let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); + let integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +fn pick_corner_radius(point: vec2, radii: Corners) -> f32 { + if (point.x < 0.0) { + if (point.y < 0.0) { + return radii.top_left; + } else { + return radii.bottom_left; + } + } else { + if (point.y < 0.0) { + return radii.top_right; + } else { + return radii.bottom_right; + } + } +} + +fn quad_sdf(point: vec2, bounds: Bounds, corner_radii: Corners) -> f32 { + let half_size = bounds.size / 2.0; + let center = bounds.origin + half_size; + let center_to_point = point - center; + let corner_radius = pick_corner_radius(center_to_point, corner_radii); + let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius; + return length(max(vec2(0.0), rounded_edge_to_point)) + + min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - + corner_radius; +} + +// --- quads --- // + +struct Quad { + view_id: ViewId, + layer_id: u32, + order: u32, + bounds: Bounds, + content_mask: Bounds, + background: Hsla, + border_color: Hsla, + corner_radii: Corners, + border_widths: Edges, +} +var b_quads: array; + +struct QuadVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) background_color: vec4, + @location(1) @interpolate(flat) border_color: vec4, + @location(2) @interpolate(flat) quad_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let quad = b_quads[instance_id]; + + var out = QuadVarying(); + out.position = to_device_position(unit_vertex, quad.bounds); + out.background_color = hsla_to_rgba(quad.background); + out.border_color = hsla_to_rgba(quad.border_color); + out.quad_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); + return out; +} + +@fragment +fn fs_quad(input: QuadVarying) -> @location(0) vec4 { + // Alpha clip first, since we don't have `clip_distance`. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let quad = b_quads[input.quad_id]; + let half_size = quad.bounds.size / 2.0; + let center = quad.bounds.origin + half_size; + let center_to_point = input.position.xy - center; + + let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); + + let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius; + let distance = + length(max(vec2(0.0), rounded_edge_to_point)) + + min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - + corner_radius; + + let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0); + let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0); + let inset_size = half_size - corner_radius - vec2(vertical_border, horizontal_border); + let point_to_inset_corner = abs(center_to_point) - inset_size; + + var border_width = 0.0; + if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) { + border_width = 0.0; + } else if (point_to_inset_corner.y > point_to_inset_corner.x) { + border_width = horizontal_border; + } else { + border_width = vertical_border; + } + + var color = input.background_color; + if (border_width > 0.0) { + let inset_distance = distance + border_width; + // Blend the border on top of the background and then linearly interpolate + // between the two as we slide inside the background. + let blended_border = over(input.background_color, input.border_color); + color = mix(blended_border, input.background_color, + saturate(0.5 - inset_distance)); + } + + return color * vec4(1.0, 1.0, 1.0, saturate(0.5 - distance)); +} + +// --- shadows --- // + +struct Shadow { + view_id: ViewId, + layer_id: u32, + order: u32, + bounds: Bounds, + corner_radii: Corners, + content_mask: Bounds, + color: Hsla, + blur_radius: f32, + pad: u32, +} +var b_shadows: array; + +struct ShadowVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) color: vec4, + @location(1) @interpolate(flat) shadow_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + var shadow = b_shadows[instance_id]; + + let margin = 3.0 * shadow.blur_radius; + // Set the bounds of the shadow and adjust its size based on the shadow's + // spread radius to achieve the spreading effect + shadow.bounds.origin -= vec2(margin); + shadow.bounds.size += 2.0 * vec2(margin); + + var out = ShadowVarying(); + out.position = to_device_position(unit_vertex, shadow.bounds); + out.color = hsla_to_rgba(shadow.color); + out.shadow_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask); + return out; +} + +@fragment +fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { + // Alpha clip first, since we don't have `clip_distance`. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let shadow = b_shadows[input.shadow_id]; + let half_size = shadow.bounds.size / 2.0; + let center = shadow.bounds.origin + half_size; + let center_to_point = input.position.xy - center; + + let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii); + + // The signal is only non-zero in a limited range, so don't waste samples + let low = center_to_point.y - half_size.y; + let high = center_to_point.y + half_size.y; + let start = clamp(-3.0 * shadow.blur_radius, low, high); + let end = clamp(3.0 * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + let step = (end - start) / 4.0; + var y = start + step * 0.5; + var alpha = 0.0; + for (var i = 0; i < 4; i += 1) { + let blur = blur_along_x(center_to_point.x, center_to_point.y - y, + shadow.blur_radius, corner_radius, half_size); + alpha += blur * gaussian(y, shadow.blur_radius) * step; + y += step; + } + + return input.color * vec4(1.0, 1.0, 1.0, alpha); +} + +// --- path rasterization --- // + +struct PathVertex { + xy_position: vec2, + st_position: vec2, + content_mask: Bounds, +} +var b_path_vertices: array; + +struct PathRasterizationVarying { + @builtin(position) position: vec4, + @location(0) st_position: vec2, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying { + let v = b_path_vertices[vertex_id]; + + var out = PathRasterizationVarying(); + out.position = to_device_position_impl(v.xy_position); + out.st_position = v.st_position; + out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask); + return out; +} + +@fragment +fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 { + let dx = dpdx(input.st_position); + let dy = dpdy(input.st_position); + if (any(input.clip_distances < vec4(0.0))) { + return 0.0; + } + + let gradient = 2.0 * input.st_position * vec2(dx.x, dy.x) - vec2(dx.y, dy.y); + let f = input.st_position.x * input.st_position.x - input.st_position.y; + let distance = f / length(gradient); + return saturate(0.5 - distance); +} + +// --- paths --- // + +struct PathSprite { + bounds: Bounds, + color: Hsla, + tile: AtlasTile, +} +var b_path_sprites: array; + +struct PathVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) color: vec4, +} + +@vertex +fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_path_sprites[instance_id]; + // Don't apply content mask because it was already accounted for when rasterizing the path. + + var out = PathVarying(); + out.position = to_device_position(unit_vertex, sprite.bounds); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.color = hsla_to_rgba(sprite.color); + return out; +} + +@fragment +fn fs_path(input: PathVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; + let mask = 1.0 - abs(1.0 - sample % 2.0); + return input.color * mask; +} + +// --- underlines --- // + +struct Underline { + view_id: ViewId, + layer_id: u32, + order: u32, + bounds: Bounds, + content_mask: Bounds, + color: Hsla, + thickness: f32, + wavy: u32, +} +var b_underlines: array; + +struct UnderlineVarying { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) color: vec4, + @location(1) @interpolate(flat) underline_id: u32, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let underline = b_underlines[instance_id]; + + var out = UnderlineVarying(); + out.position = to_device_position(unit_vertex, underline.bounds); + out.color = hsla_to_rgba(underline.color); + out.underline_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask); + return out; +} + +@fragment +fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { + // Alpha clip first, since we don't have `clip_distance`. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let underline = b_underlines[input.underline_id]; + if ((underline.wavy & 0xFFu) == 0u) + { + return vec4(0.0); + } + + let half_thickness = underline.thickness * 0.5; + let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2(0.0, 0.5); + let frequency = M_PI_F * 3.0 * underline.thickness / 8.0; + let amplitude = 1.0 / (2.0 * underline.thickness); + let sine = sin(st.x * frequency) * amplitude; + let dSine = cos(st.x * frequency) * amplitude * frequency; + let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine); + let distance_in_pixels = distance * underline.bounds.size.y; + let distance_from_top_border = distance_in_pixels - half_thickness; + let distance_from_bottom_border = distance_in_pixels + half_thickness; + let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border)); + return input.color * vec4(1.0, 1.0, 1.0, alpha); +} + +// --- monochrome sprites --- // + +struct MonochromeSprite { + view_id: ViewId, + layer_id: u32, + order: u32, + bounds: Bounds, + content_mask: Bounds, + color: Hsla, + tile: AtlasTile, +} +var b_mono_sprites: array; + +struct MonoSpriteVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) color: vec4, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_mono_sprites[instance_id]; + + var out = MonoSpriteVarying(); + out.position = to_device_position(unit_vertex, sprite.bounds); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.color = hsla_to_rgba(sprite.color); + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + return out; +} + +@fragment +fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + return input.color * vec4(1.0, 1.0, 1.0, sample); +} + +// --- polychrome sprites --- // + +struct PolychromeSprite { + view_id: ViewId, + layer_id: u32, + order: u32, + bounds: Bounds, + content_mask: Bounds, + corner_radii: Corners, + tile: AtlasTile, + grayscale: u32, + pad: u32, +} +var b_poly_sprites: array; + +struct PolySpriteVarying { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) sprite_id: u32, + @location(3) clip_distances: vec4, +} + +@vertex +fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_poly_sprites[instance_id]; + + var out = PolySpriteVarying(); + out.position = to_device_position(unit_vertex, sprite.bounds); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.sprite_id = instance_id; + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + return out; +} + +@fragment +fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4 { + let sample = textureSample(t_sprite, s_sprite, input.tile_position); + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return vec4(0.0); + } + + let sprite = b_poly_sprites[input.sprite_id]; + let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); + + var color = sample; + if ((sprite.grayscale & 0xFFu) != 0u) { + let grayscale = dot(color.rgb, GRAYSCALE_FACTORS); + color = vec4(vec3(grayscale), sample.a); + } + color.a *= saturate(0.5 - distance); + return color;; +} + +// --- surface sprites --- // diff --git a/crates/gpui/src/platform/linux/text_system.rs b/crates/gpui/src/platform/linux/text_system.rs new file mode 100644 index 0000000000000..53faa936bc29b --- /dev/null +++ b/crates/gpui/src/platform/linux/text_system.rs @@ -0,0 +1,127 @@ +use crate::{ + Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels, + PlatformTextSystem, RenderGlyphParams, SharedString, Size, +}; +use anyhow::Result; +use collections::HashMap; +use font_kit::{ + font::Font as FontKitFont, + handle::Handle, + hinting::HintingOptions, + metrics::Metrics, + properties::{Style as FontkitStyle, Weight as FontkitWeight}, + source::SystemSource, + sources::mem::MemSource, +}; +use parking_lot::RwLock; +use smallvec::SmallVec; +use std::borrow::Cow; + +pub(crate) struct LinuxTextSystem(RwLock); + +struct LinuxTextSystemState { + memory_source: MemSource, + system_source: SystemSource, + fonts: Vec, + font_selections: HashMap, + font_ids_by_postscript_name: HashMap, + font_ids_by_family_name: HashMap>, + postscript_names_by_font_id: HashMap, +} + +// todo!(linux): Double check this +unsafe impl Send for LinuxTextSystemState {} +unsafe impl Sync for LinuxTextSystemState {} + +impl LinuxTextSystem { + pub(crate) fn new() -> Self { + Self(RwLock::new(LinuxTextSystemState { + memory_source: MemSource::empty(), + system_source: SystemSource::new(), + fonts: Vec::new(), + font_selections: HashMap::default(), + font_ids_by_postscript_name: HashMap::default(), + font_ids_by_family_name: HashMap::default(), + postscript_names_by_font_id: HashMap::default(), + })) + } +} + +impl Default for LinuxTextSystem { + fn default() -> Self { + Self::new() + } +} + +#[allow(unused)] +impl PlatformTextSystem for LinuxTextSystem { + // todo!(linux) + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + Ok(()) + } + + // todo!(linux) + fn all_font_names(&self) -> Vec { + Vec::new() + } + + // todo!(linux) + fn all_font_families(&self) -> Vec { + Vec::new() + } + + // todo!(linux) + fn font_id(&self, descriptor: &Font) -> Result { + Ok(FontId(0)) + } + + // todo!(linux) + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + unimplemented!() + } + + // todo!(linux) + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unimplemented!() + } + + // todo!(linux) + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unimplemented!() + } + + // todo!(linux) + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + None + } + + // todo!(linux) + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + unimplemented!() + } + + // todo!(linux) + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + unimplemented!() + } + + // todo!(linux) + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + LineLayout::default() //TODO + } + + // todo!(linux) + fn wrap_line( + &self, + text: &str, + font_id: FontId, + font_size: Pixels, + width: Pixels, + ) -> Vec { + unimplemented!() + } +} diff --git a/crates/gpui/src/platform/linux/window.rs b/crates/gpui/src/platform/linux/window.rs new file mode 100644 index 0000000000000..9ad02039e8a9e --- /dev/null +++ b/crates/gpui/src/platform/linux/window.rs @@ -0,0 +1,425 @@ +use super::BladeRenderer; +use crate::{ + Bounds, GlobalPixels, LinuxDisplay, Pixels, PlatformDisplay, PlatformInputHandler, + PlatformWindow, Point, Size, WindowAppearance, WindowBounds, WindowOptions, XcbAtoms, +}; +use blade_graphics as gpu; +use parking_lot::Mutex; +use raw_window_handle as rwh; +use std::{ + ffi::c_void, + mem, + num::NonZeroU32, + ptr::NonNull, + rc::Rc, + sync::{self, Arc}, +}; +use xcb::{x, Xid as _}; + +#[derive(Default)] +struct Callbacks { + request_frame: Option>, + input: Option bool>>, + active_status_change: Option>, + resize: Option, f32)>>, + fullscreen: Option>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, +} + +struct LinuxWindowInner { + bounds: Bounds, + scale_factor: f32, + renderer: BladeRenderer, +} + +impl LinuxWindowInner { + fn content_size(&self) -> Size { + let size = self.renderer.viewport_size(); + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +fn query_render_extent(xcb_connection: &xcb::Connection, x_window: x::Window) -> gpu::Extent { + let cookie = xcb_connection.send_request(&x::GetGeometry { + drawable: x::Drawable::Window(x_window), + }); + let reply = xcb_connection.wait_for_reply(cookie).unwrap(); + println!("Got geometry {:?}", reply); + gpu::Extent { + width: reply.width() as u32, + height: reply.height() as u32, + depth: 1, + } +} + +struct RawWindow { + connection: *mut c_void, + screen_id: i32, + window_id: u32, + visual_id: u32, +} + +pub(crate) struct LinuxWindowState { + xcb_connection: Arc, + display: Rc, + raw: RawWindow, + x_window: x::Window, + callbacks: Mutex, + inner: Mutex, +} + +#[derive(Clone)] +pub(crate) struct LinuxWindow(pub(crate) Arc); + +//todo!(linux): Remove other RawWindowHandle implementation +unsafe impl blade_rwh::HasRawWindowHandle for RawWindow { + fn raw_window_handle(&self) -> blade_rwh::RawWindowHandle { + let mut wh = blade_rwh::XcbWindowHandle::empty(); + wh.window = self.window_id; + wh.visual_id = self.visual_id; + wh.into() + } +} +unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow { + fn raw_display_handle(&self) -> blade_rwh::RawDisplayHandle { + let mut dh = blade_rwh::XcbDisplayHandle::empty(); + dh.connection = self.connection; + dh.screen = self.screen_id; + dh.into() + } +} + +impl rwh::HasWindowHandle for LinuxWindow { + fn window_handle(&self) -> Result { + Ok(unsafe { + let non_zero = NonZeroU32::new(self.0.raw.window_id).unwrap(); + let handle = rwh::XcbWindowHandle::new(non_zero); + rwh::WindowHandle::borrow_raw(handle.into()) + }) + } +} +impl rwh::HasDisplayHandle for LinuxWindow { + fn display_handle(&self) -> Result { + Ok(unsafe { + let non_zero = NonNull::new(self.0.raw.connection).unwrap(); + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.0.raw.screen_id); + rwh::DisplayHandle::borrow_raw(handle.into()) + }) + } +} + +impl LinuxWindowState { + pub fn new( + options: WindowOptions, + xcb_connection: &Arc, + x_main_screen_index: i32, + x_window: x::Window, + atoms: &XcbAtoms, + ) -> Self { + let x_screen_index = options + .display_id + .map_or(x_main_screen_index, |did| did.0 as i32); + let screen = xcb_connection + .get_setup() + .roots() + .nth(x_screen_index as usize) + .unwrap(); + + let xcb_values = [ + x::Cw::BackPixel(screen.white_pixel()), + x::Cw::EventMask( + x::EventMask::EXPOSURE | x::EventMask::STRUCTURE_NOTIFY | x::EventMask::KEY_PRESS, + ), + ]; + + let bounds = match options.bounds { + WindowBounds::Fullscreen | WindowBounds::Maximized => Bounds { + origin: Point::default(), + size: Size { + width: screen.width_in_pixels() as i32, + height: screen.height_in_pixels() as i32, + }, + }, + WindowBounds::Fixed(bounds) => bounds.map(|p| p.0 as i32), + }; + + xcb_connection.send_request(&x::CreateWindow { + depth: x::COPY_FROM_PARENT as u8, + wid: x_window, + parent: screen.root(), + x: bounds.origin.x as i16, + y: bounds.origin.y as i16, + width: bounds.size.width as u16, + height: bounds.size.height as u16, + border_width: 0, + class: x::WindowClass::InputOutput, + visual: screen.root_visual(), + value_list: &xcb_values, + }); + + if let Some(titlebar) = options.titlebar { + if let Some(title) = titlebar.title { + xcb_connection.send_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: x_window, + property: x::ATOM_WM_NAME, + r#type: x::ATOM_STRING, + data: title.as_bytes(), + }); + } + } + xcb_connection + .send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: x_window, + property: atoms.wm_protocols, + r#type: x::ATOM_ATOM, + data: &[atoms.wm_del_window], + }) + .unwrap(); + + xcb_connection.send_request(&x::MapWindow { window: x_window }); + xcb_connection.flush().unwrap(); + + //Warning: it looks like this reported size is immediately invalidated + // on some platforms, followed by a "ConfigureNotify" event. + let gpu_extent = query_render_extent(&xcb_connection, x_window); + + let raw = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( + xcb_connection, + ) as *mut _, + screen_id: x_screen_index, + window_id: x_window.resource_id(), + visual_id: screen.root_visual(), + }; + let gpu = Arc::new( + unsafe { + gpu::Context::init_windowed( + &raw, + gpu::ContextDesc { + validation: cfg!(debug_assertions), + capture: false, + }, + ) + } + .unwrap(), + ); + + Self { + xcb_connection: Arc::clone(xcb_connection), + display: Rc::new(LinuxDisplay::new(xcb_connection, x_screen_index)), + raw, + x_window, + callbacks: Mutex::new(Callbacks::default()), + inner: Mutex::new(LinuxWindowInner { + bounds, + scale_factor: 1.0, + renderer: BladeRenderer::new(gpu, gpu_extent), + }), + } + } + + pub fn destroy(&self) { + self.inner.lock().renderer.destroy(); + self.xcb_connection.send_request(&x::UnmapWindow { + window: self.x_window, + }); + self.xcb_connection.send_request(&x::DestroyWindow { + window: self.x_window, + }); + if let Some(fun) = self.callbacks.lock().close.take() { + fun(); + } + self.xcb_connection.flush().unwrap(); + } + + pub fn expose(&self) { + let mut cb = self.callbacks.lock(); + if let Some(ref mut fun) = cb.request_frame { + fun(); + } + } + + pub fn configure(&self, bounds: Bounds) { + let mut resize_args = None; + let do_move; + { + let mut inner = self.inner.lock(); + let old_bounds = mem::replace(&mut inner.bounds, bounds); + do_move = old_bounds.origin != bounds.origin; + let gpu_size = query_render_extent(&self.xcb_connection, self.x_window); + if inner.renderer.viewport_size() != gpu_size { + inner.renderer.resize(gpu_size); + resize_args = Some((inner.content_size(), inner.scale_factor)); + } + } + + let mut callbacks = self.callbacks.lock(); + if let Some((content_size, scale_factor)) = resize_args { + if let Some(ref mut fun) = callbacks.resize { + fun(content_size, scale_factor) + } + } + if do_move { + if let Some(ref mut fun) = callbacks.moved { + fun() + } + } + } +} + +impl PlatformWindow for LinuxWindow { + fn bounds(&self) -> WindowBounds { + WindowBounds::Fixed(self.0.inner.lock().bounds.map(|v| GlobalPixels(v as f32))) + } + + fn content_size(&self) -> Size { + self.0.inner.lock().content_size() + } + + fn scale_factor(&self) -> f32 { + self.0.inner.lock().scale_factor + } + + //todo!(linux) + fn titlebar_height(&self) -> Pixels { + unimplemented!() + } + + //todo!(linux) + fn appearance(&self) -> WindowAppearance { + unimplemented!() + } + + fn display(&self) -> Rc { + Rc::clone(&self.0.display) + } + + //todo!(linux) + fn mouse_position(&self) -> Point { + Point::default() + } + + //todo!(linux) + fn modifiers(&self) -> crate::Modifiers { + crate::Modifiers::default() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + //todo!(linux) + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {} + + //todo!(linux) + fn take_input_handler(&mut self) -> Option { + None + } + + //todo!(linux) + fn prompt( + &self, + _level: crate::PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[&str], + ) -> futures::channel::oneshot::Receiver { + unimplemented!() + } + + //todo!(linux) + fn activate(&self) {} + + //todo!(linux) + fn set_title(&mut self, title: &str) {} + + //todo!(linux) + fn set_edited(&mut self, edited: bool) {} + + //todo!(linux), this corresponds to `orderFrontCharacterPalette` on macOS, + // but it looks like the equivalent for Linux is GTK specific: + // + // https://docs.gtk.org/gtk3/signal.Entry.insert-emoji.html + // + // This API might need to change, or we might need to build an emoji picker into GPUI + fn show_character_palette(&self) { + unimplemented!() + } + + //todo!(linux) + fn minimize(&self) { + unimplemented!() + } + + //todo!(linux) + fn zoom(&self) { + unimplemented!() + } + + //todo!(linux) + fn toggle_full_screen(&self) { + unimplemented!() + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.lock().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box bool>) { + self.0.callbacks.lock().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.lock().active_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.lock().resize = Some(callback); + } + + fn on_fullscreen(&self, callback: Box) { + self.0.callbacks.lock().fullscreen = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.lock().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.lock().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.lock().close = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.lock().appearance_changed = Some(callback); + } + + //todo!(linux) + fn is_topmost_for_position(&self, _position: crate::Point) -> bool { + unimplemented!() + } + + //todo!(linux) + fn invalidate(&self) {} + + fn draw(&self, scene: &crate::Scene) { + let mut inner = self.0.inner.lock(); + inner.renderer.draw(scene); + } + + fn sprite_atlas(&self) -> sync::Arc { + let inner = self.0.inner.lock(); + inner.renderer.atlas().clone() + } +} diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 95f78a446539d..7c23fafcba131 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -174,6 +174,7 @@ impl MetalAtlasTexture { origin: allocation.rectangle.min.into(), size, }, + padding: 0, }; Some(tile) } diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index d17739239eede..3b08985737801 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -1,9 +1,12 @@ mod dispatcher; mod display; mod platform; +mod text_system; mod window; pub(crate) use dispatcher::*; pub(crate) use display::*; pub(crate) use platform::*; +#[cfg(not(target_os = "macos"))] +pub(crate) use text_system::*; pub(crate) use window::*; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index b7e1c99f30bec..6a17c731d6df4 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -120,7 +120,11 @@ impl Platform for TestPlatform { } fn text_system(&self) -> Arc { - Arc::new(crate::platform::mac::MacTextSystem::new()) + #[cfg(target_os = "linux")] + return Arc::new(crate::platform::test::TestTextSystem {}); + + #[cfg(target_os = "macos")] + return Arc::new(crate::platform::mac::MacTextSystem::new()); } fn run(&self, _on_finish_launching: Box) { diff --git a/crates/gpui/src/platform/test/text_system.rs b/crates/gpui/src/platform/test/text_system.rs new file mode 100644 index 0000000000000..0e877aabbdb80 --- /dev/null +++ b/crates/gpui/src/platform/test/text_system.rs @@ -0,0 +1,59 @@ +use crate::{ + Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels, + PlatformTextSystem, RenderGlyphParams, Size, +}; +use anyhow::Result; +use std::borrow::Cow; + +pub(crate) struct TestTextSystem {} + +//todo!(linux) +#[allow(unused)] +impl PlatformTextSystem for TestTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + unimplemented!() + } + fn all_font_names(&self) -> Vec { + unimplemented!() + } + fn all_font_families(&self) -> Vec { + unimplemented!() + } + fn font_id(&self, descriptor: &Font) -> Result { + unimplemented!() + } + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + unimplemented!() + } + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unimplemented!() + } + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + unimplemented!() + } + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + unimplemented!() + } + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + unimplemented!() + } + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + unimplemented!() + } + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + unimplemented!() + } + fn wrap_line( + &self, + text: &str, + font_id: FontId, + font_size: Pixels, + width: Pixels, + ) -> Vec { + unimplemented!() + } +} diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index d56851c8b9909..c67384a39297a 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -340,6 +340,7 @@ impl PlatformAtlas for TestAtlas { kind: crate::AtlasTextureKind::Path, }, tile_id: TileId(tile_id), + padding: 0, bounds: crate::Bounds { origin: Point::default(), size, diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index d5d717e278f3a..f7c85cd623947 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -543,8 +543,8 @@ pub(crate) struct Underline { pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, - pub thickness: ScaledPixels, pub color: Hsla, + pub thickness: ScaledPixels, pub wavy: bool, } @@ -577,6 +577,7 @@ pub(crate) struct Shadow { pub content_mask: ContentMask, pub color: Hsla, pub blur_radius: ScaledPixels, + pub pad: u32, // align to 8 bytes } impl Ord for Shadow { @@ -641,6 +642,7 @@ pub(crate) struct PolychromeSprite { pub corner_radii: Corners, pub tile: AtlasTile, pub grayscale: bool, + pub pad: u32, // align to 8 bytes } impl Ord for PolychromeSprite { @@ -671,6 +673,7 @@ pub(crate) struct Surface { pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, + #[cfg(target_os = "macos")] pub image_buffer: media::core_video::CVImageBuffer, } diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index e3d95de58d6b9..22ab59a84cb71 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/crates/gpui/src/window/element_cx.rs @@ -23,6 +23,7 @@ use std::{ use anyhow::Result; use collections::{FxHashMap, FxHashSet}; use derive_more::{Deref, DerefMut}; +#[cfg(target_os = "macos")] use media::core_video::CVImageBuffer; use smallvec::SmallVec; use util::post_inc; @@ -34,8 +35,8 @@ use crate::{ InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext, - StackingOrder, StrikethroughStyle, Style, Surface, TextStyleRefinement, Underline, - UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS, + StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline, UnderlineStyle, + Window, WindowContext, SUBPIXEL_VARIANTS, }; type AnyMouseListener = Box; @@ -676,6 +677,7 @@ impl<'a> ElementContext<'a> { corner_radii: corner_radii.scale(scale_factor), color: shadow.color, blur_radius: shadow.blur_radius.scale(scale_factor), + pad: 0, }, ); } @@ -751,8 +753,8 @@ impl<'a> ElementContext<'a> { order: 0, bounds: bounds.scale(scale_factor), content_mask: content_mask.scale(scale_factor), - thickness: style.thickness.scale(scale_factor), color: style.color.unwrap_or_default(), + thickness: style.thickness.scale(scale_factor), wavy: style.wavy, }, ); @@ -904,6 +906,7 @@ impl<'a> ElementContext<'a> { content_mask, tile, grayscale: false, + pad: 0, }, ); } @@ -988,12 +991,14 @@ impl<'a> ElementContext<'a> { corner_radii, tile, grayscale, + pad: 0, }, ); Ok(()) } /// Paint a surface into the scene for the next frame at the current z-index. + #[cfg(target_os = "macos")] pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { let scale_factor = self.scale_factor(); let bounds = bounds.scale(scale_factor); @@ -1002,7 +1007,7 @@ impl<'a> ElementContext<'a> { let window = &mut *self.window; window.next_frame.scene.insert( &window.next_frame.z_index_stack, - Surface { + crate::Surface { view_id: view_id.into(), layer_id: 0, order: 0, diff --git a/crates/live_kit_client/Cargo.toml b/crates/live_kit_client/Cargo.toml index 732cbde338535..f6ae36c93547b 100644 --- a/crates/live_kit_client/Cargo.toml +++ b/crates/live_kit_client/Cargo.toml @@ -66,6 +66,13 @@ serde_derive.workspace = true sha2 = "0.10" simplelog = "0.9" +[target.'cfg(target_os = "macos")'.dev-dependencies] +cocoa = "0.25" +core-foundation = "0.9.3" +core-graphics = "0.22.3" +foreign-types = "0.3" +objc = "0.2" + [build-dependencies] serde.workspace = true serde_derive.workspace = true diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 0de5bada341bf..677370c0b8321 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -3,10 +3,9 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::{BTreeMap, HashMap, HashSet}; use futures::Stream; -use gpui::BackgroundExecutor; +use gpui::{BackgroundExecutor, ImageSource}; use live_kit_server::{proto, token}; -#[cfg(target_os = "macos")] -use media::core_video::CVImageBuffer; + use parking_lot::Mutex; use postage::watch; use std::{ @@ -846,8 +845,7 @@ impl Frame { self.height } - #[cfg(target_os = "macos")] - pub fn image(&self) -> CVImageBuffer { + pub fn image(&self) -> ImageSource { unimplemented!("you can't call this in test mode") } } diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 351ae3725289a..bb9751e9daf52 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -13,10 +13,10 @@ doctest = false anyhow.workspace = true block = "0.1" bytes = "1.2" -foreign-types = "0.3" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.3" +foreign-types = "0.3" metal = "0.21.0" objc = "0.2" diff --git a/crates/media/src/media.rs b/crates/media/src/media.rs index 8d24e45cf215f..8757249c31940 100644 --- a/crates/media/src/media.rs +++ b/crates/media/src/media.rs @@ -8,6 +8,7 @@ use core_foundation::{ base::{CFTypeID, TCFType}, declare_TCFType, impl_CFTypeDescription, impl_TCFType, }; +#[cfg(target_os = "macos")] use std::ffi::c_void; #[cfg(target_os = "macos")] diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 0f314f50335ee..e125af052d790 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -7,8 +7,8 @@ use terminal::{ Terminal, TerminalBuilder, }; -#[cfg(target_os = "macos")] -use std::os::unix::ffi::OsStrExt; +// #[cfg(target_os = "macos")] +// use std::os::unix::ffi::OsStrExt; pub struct Terminals { pub(crate) local_handles: Vec>, @@ -124,7 +124,7 @@ impl Project { // Paths are not strings so we need to jump through some hoops to format the command without `format!` let mut command = Vec::from(activate_command.as_bytes()); command.push(b' '); - command.extend_from_slice(activate_script.as_os_str().as_bytes()); + command.extend_from_slice(activate_script.as_os_str().as_encoded_bytes()); command.push(b'\n'); terminal_handle.update(cx, |this, _| this.input_bytes(command)); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index dcf9270fb178e..45f430c52a28f 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -9,18 +9,59 @@ use serde::{Deserialize, Serialize}; lazy_static::lazy_static! { pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); - pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations"); - pub static ref EMBEDDINGS_DIR: PathBuf = HOME.join(".config/zed/embeddings"); - pub static ref THEMES_DIR: PathBuf = HOME.join(".config/zed/themes"); - pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); - pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); - pub static ref EXTENSIONS_DIR: PathBuf = HOME.join("Library/Application Support/Zed/extensions"); - pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); - pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot"); - pub static ref DEFAULT_PRETTIER_DIR: PathBuf = HOME.join("Library/Application Support/Zed/prettier"); - pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); - pub static ref CRASHES_DIR: PathBuf = HOME.join("Library/Logs/DiagnosticReports"); - pub static ref CRASHES_RETIRED_DIR: PathBuf = HOME.join("Library/Logs/DiagnosticReports/Retired"); + pub static ref CONVERSATIONS_DIR: PathBuf = CONFIG_DIR.join("conversations"); + pub static ref EMBEDDINGS_DIR: PathBuf = CONFIG_DIR.join("embeddings"); + pub static ref THEMES_DIR: PathBuf = CONFIG_DIR.join("themes"); + pub static ref LOGS_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Logs/Zed") + } else { + CONFIG_DIR.join("logs") + }; + pub static ref SUPPORT_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Application Support/Zed") + } else { + CONFIG_DIR.join("support") + }; + pub static ref EXTENSIONS_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Application Support/Zed") + } else { + CONFIG_DIR.join("extensions") + }; + pub static ref PLUGINS_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Application Support/Zed/plugins") + } else { + CONFIG_DIR.join("plugins") + }; + pub static ref LANGUAGES_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Application Support/Zed/languages") + } else { + CONFIG_DIR.join("languages") + }; + pub static ref COPILOT_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Application Support/Zed/copilot") + } else { + CONFIG_DIR.join("copilot") + }; + pub static ref DEFAULT_PRETTIER_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Application Support/Zed/prettier") + } else { + CONFIG_DIR.join("prettier") + }; + pub static ref DB_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Application Support/Zed/db") + } else { + CONFIG_DIR.join("db") + }; + pub static ref CRASHES_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Logs/DiagnosticReports") + } else { + CONFIG_DIR.join("crashes") + }; + pub static ref CRASHES_RETIRED_DIR: PathBuf = if cfg!(target_os="macos") { + HOME.join("Library/Logs/DiagnosticReports/Retired") + } else { + CRASHES_DIR.join("retired") + }; pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 0b13f5bd2f578..6905d492e1ef7 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -1,25 +1,27 @@ use std::process::Command; fn main() { - println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); - - println!("cargo:rerun-if-env-changed=ZED_BUNDLE"); - if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") { - // Find WebRTC.framework in the Frameworks folder when running as part of an application bundle. - println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks"); - } else { - // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle. - println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path"); - } + if cfg!(target_os = "macos") { + println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); + + println!("cargo:rerun-if-env-changed=ZED_BUNDLE"); + if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") { + // Find WebRTC.framework in the Frameworks folder when running as part of an application bundle. + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks"); + } else { + // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle. + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path"); + } - // Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+. - println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit"); + // Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+. + println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit"); - // Seems to be required to enable Swift concurrency - println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift"); + // Seems to be required to enable Swift concurrency + println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift"); - // Register exported Objective-C selectors, protocols, etc - println!("cargo:rustc-link-arg=-Wl,-ObjC"); + // Register exported Objective-C selectors, protocols, etc + println!("cargo:rustc-link-arg=-Wl,-ObjC"); + } // Populate git sha environment variable if git is available println!("cargo:rerun-if-changed=../../.git/logs/HEAD"); diff --git a/crates/zed/src/app_menus.rs b/crates/zed/src/app_menus.rs index fc063a620f18a..40d20d61ef309 100644 --- a/crates/zed/src/app_menus.rs +++ b/crates/zed/src/app_menus.rs @@ -1,6 +1,5 @@ use gpui::{Menu, MenuItem, OsAction}; -#[cfg(target_os = "macos")] pub fn app_menus() -> Vec> { use zed_actions::Quit; diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 2931d537632af..43e92fb0ae52b 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -264,6 +264,9 @@ pub fn init( ], ); + // Produces a link error on linux due to duplicated `state_new` symbol + // todo!(linux): Restore purescript + #[cfg(not(target_os = "linux"))] language( "purescript", vec![Arc::new(purescript::PurescriptLspAdapter::new( diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 83eaa1de10477..5cdc238777829 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -11,6 +11,7 @@ use db::kvp::KEY_VALUE_STORE; use editor::Editor; use env_logger::Builder; use fs::RealFs; +#[cfg(target_os = "macos")] use fsevent::StreamFlags; use futures::StreamExt; use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; @@ -176,6 +177,7 @@ fn main() { extension::init(fs.clone(), languages.clone(), ThemeRegistry::global(cx), cx); load_user_themes_in_background(fs.clone(), cx); + #[cfg(target_os = "macos")] watch_themes(fs.clone(), cx); cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) @@ -258,6 +260,8 @@ fn main() { initialize_workspace(app_state.clone(), cx); if stdout_is_a_pty() { + //todo!(linux): unblock this + #[cfg(not(target_os = "linux"))] upload_panics_and_crashes(http.clone(), cx); cx.activate(true); let urls = collect_url_args(); @@ -931,7 +935,9 @@ fn load_user_themes_in_background(fs: Arc, cx: &mut AppContext) { .detach_and_log_err(cx); } +//todo!(linux): Port fsevents to linux /// Spawns a background task to watch the themes directory for changes. +#[cfg(target_os = "macos")] fn watch_themes(fs: Arc, cx: &mut AppContext) { cx.spawn(|cx| async move { let mut events = fs diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5cdc76def26ae..cda382660135d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,4 +2,4 @@ channel = "1.75" profile = "minimal" components = [ "rustfmt", "clippy" ] -targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] +targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "wasm32-wasi" ] diff --git a/script/linux b/script/linux index 1acf7a6e1659d..f672ed8b85dbe 100755 --- a/script/linux +++ b/script/linux @@ -1,38 +1,61 @@ -#!/usr/bin/env bash +#!/usr/bin/bash -e # if not on Linux, do nothing [[ $(uname) == "Linux" ]] || exit 0 +# Copy assets to the user's home directory if they don't exist +mkdir -p "$HOME/.config/zed" + +mkdir -p "$HOME/.config/zed/plugins" + +mkdir -p "$HOME/.config/zed/themes" +cp -ruL ./assets/themes/*/*.json "$HOME/.config/zed/themes" + +test -f "$HOME/.config/zed/settings.json" || + cp -uL ./assets/settings/initial_user_settings.json "$HOME/.config/zed/settings.json" + +test -f "$HOME/.config/zed/keymap.json" || + cp -uL ./assets/keymaps/default.json "$HOME/.config/zed/keymap.json" + # if sudo is not installed, define an empty alias maysudo=$(command -v sudo || true) export maysudo # Ubuntu, Debian, etc. +# https://packages.ubuntu.com/ apt=$(command -v apt-get || true) -deps=( - libasound2-dev -) if [[ -n $apt ]]; then + deps=( + libasound2-dev + libfontconfig-dev + vulkan-validationlayers* + ) $maysudo "$apt" install -y "${deps[@]}" exit 0 fi # Fedora, CentOS, RHEL, etc. +# https://packages.fedoraproject.org/ dnf=$(command -v dnf || true) -deps=( - alsa-lib-devel -) if [[ -n $dnf ]]; then + deps=( + alsa-lib-devel + fontconfig-devel + vulkan-validation-layers + ) $maysudo "$dnf" install -y "${deps[@]}" exit 0 fi # Arch, Manjaro, etc. +# https://archlinux.org/packages pacman=$(command -v pacman || true) -deps=( - alsa-lib -) if [[ -n $pacman ]]; then + deps=( + alsa-lib + fontconfig + vulkan-validation-layers + ) $maysudo "$pacman" -S --noconfirm "${deps[@]}" exit 0 fi